Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fc7df95c1a | |||
| 088c9cace0 | |||
| 815810f841 | |||
| 4e04cdbc5b | |||
| 57368f0179 | |||
| 7078de6385 | |||
| 4af9afda61 | |||
| 50ab33f369 | |||
| bcbc45bb03 |
@@ -33,6 +33,25 @@ export const getPunchRecords = waybillId =>
|
||||
method: 'get',
|
||||
params: { waybillId },
|
||||
});
|
||||
|
||||
/** 运单车辆实时定位 */
|
||||
export const locateVehicle = id =>
|
||||
request({
|
||||
url: `${baseUrl}/locate`,
|
||||
method: 'post',
|
||||
params: { id },
|
||||
timeout: 60000,
|
||||
});
|
||||
|
||||
/** 运单车辆历史轨迹 */
|
||||
export const trackVehicle = (id, startDate, endDate) =>
|
||||
request({
|
||||
url: `${baseUrl}/track`,
|
||||
method: 'post',
|
||||
params: { id, startDate, endDate },
|
||||
timeout: 60000,
|
||||
});
|
||||
|
||||
export const roadLoading = ids =>
|
||||
request({
|
||||
url: `${baseUrl}/road-loading`,
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import request from '@/axios';
|
||||
|
||||
export const getMkPublicDetail = (bizType, id) => {
|
||||
return request({
|
||||
url: `/blade-transport/mk-process/public/${bizType}/detail`,
|
||||
method: 'get',
|
||||
meta: {
|
||||
isToken: false,
|
||||
},
|
||||
params: { id },
|
||||
});
|
||||
};
|
||||
|
||||
export const postMkPublicProcessMessage = (bizType, data) => {
|
||||
return request({
|
||||
url: `/blade-transport/mk-process/public/${bizType}/process-message`,
|
||||
method: 'post',
|
||||
meta: {
|
||||
isToken: false,
|
||||
},
|
||||
data,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import request from '@/axios';
|
||||
|
||||
/**
|
||||
* 调用 MK processSubmit 提交审核流
|
||||
* @param {Object} data
|
||||
* @param {string} data.templateCode 模板编码
|
||||
* @param {string} data.submitIdentity 提交人(手机号)
|
||||
* @param {string} data.loginName 登录账号(手机号)
|
||||
* @param {string} data.formInstanceId 业务表单实例 id
|
||||
* @param {string} [data.subject] 流程标题
|
||||
*/
|
||||
export const processSubmit = data => {
|
||||
return request({
|
||||
url: '/blade-system/businessProcess/processSubmit',
|
||||
method: 'post',
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 调用 MK processDelete 删除审核流(驳回后重新提交前使用)
|
||||
* @param {Object} data
|
||||
* @param {string} data.formInstanceId 业务表单实例 id
|
||||
*/
|
||||
export const processDelete = data => {
|
||||
return request({
|
||||
url: '/blade-system/businessProcess/processDelete',
|
||||
method: 'post',
|
||||
data,
|
||||
});
|
||||
};
|
||||
@@ -48,6 +48,32 @@ export const syncIamOrganizations = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const syncOaCompany = (current = 1, size = 20, signal) => {
|
||||
return request({
|
||||
url: '/blade-system/dept/sync-oa-company',
|
||||
method: 'post',
|
||||
params: {
|
||||
current,
|
||||
size,
|
||||
},
|
||||
timeout: 60000,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const syncOaDepartment = (current = 1, size = 20, signal) => {
|
||||
return request({
|
||||
url: '/blade-system/dept/sync-oa-department',
|
||||
method: 'post',
|
||||
params: {
|
||||
current,
|
||||
size,
|
||||
},
|
||||
timeout: 60000,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const update = row => {
|
||||
return request({
|
||||
url: '/blade-system/dept/submit',
|
||||
|
||||
@@ -31,10 +31,16 @@ export const add = row => {
|
||||
});
|
||||
};
|
||||
|
||||
export const syncIamAccounts = () => {
|
||||
export const syncIamAccounts = (current = 1, size = 50, signal) => {
|
||||
return request({
|
||||
url: '/blade-system/user/sync-iam-accounts',
|
||||
method: 'post',
|
||||
params: {
|
||||
current,
|
||||
size,
|
||||
},
|
||||
timeout: 60000,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -22,6 +22,19 @@ export const getDetail = id => {
|
||||
});
|
||||
};
|
||||
|
||||
export const getPublicDetail = id => {
|
||||
return request({
|
||||
url: '/blade-transport/customer-archive/public/detail',
|
||||
method: 'get',
|
||||
meta: {
|
||||
isToken: false,
|
||||
},
|
||||
params: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const getChangeRecordList = (customerId, current, size) => {
|
||||
return request({
|
||||
url: '/blade-transport/customer-archive/change-record/list',
|
||||
@@ -34,6 +47,32 @@ export const getChangeRecordList = (customerId, current, size) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const getPublicChangeRecordList = (customerId, current, size) => {
|
||||
return request({
|
||||
url: '/blade-transport/customer-archive/public/change-record/list',
|
||||
method: 'get',
|
||||
meta: {
|
||||
isToken: false,
|
||||
},
|
||||
params: {
|
||||
customerId,
|
||||
current,
|
||||
size,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const postPublicProcessMessage = data => {
|
||||
return request({
|
||||
url: '/blade-transport/customer-archive/public/process-message',
|
||||
method: 'post',
|
||||
meta: {
|
||||
isToken: false,
|
||||
},
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
export const submit = row => {
|
||||
return request({
|
||||
url: '/blade-transport/customer-archive/submit',
|
||||
|
||||
+56
-12
@@ -70,11 +70,44 @@ const redirectToLogin = () => {
|
||||
store.dispatch('FedLogOut').then(() => router.push({ path: '/login' }));
|
||||
};
|
||||
|
||||
/**
|
||||
* 统一弹出错误提示并返回带标记的 Error。
|
||||
* `_notified` 标记用于让业务层(批量导入等)识别「拦截器已经提示过」,
|
||||
* 避免同一条错误被弹两次。
|
||||
*/
|
||||
const notifiedError = message => {
|
||||
ElMessage({
|
||||
message: message,
|
||||
type: 'error',
|
||||
});
|
||||
const error = new Error(message);
|
||||
error._notified = true;
|
||||
return error;
|
||||
};
|
||||
|
||||
axios.defaults.timeout = 10000;
|
||||
//返回其他状态码
|
||||
axios.defaults.validateStatus = function (status) {
|
||||
return status >= 200 && status <= 500; // 默认的
|
||||
};
|
||||
|
||||
/**
|
||||
* 判断响应是否为 JSON。
|
||||
* 用于 blob 请求(导入/导出)的兜底:后端在业务校验失败时返回的是 JSON
|
||||
* (Content-Type: application/json),而不是文件流,需要解析后才能取到 msg。
|
||||
*/
|
||||
const isJsonContentType = res => {
|
||||
const headers = res.headers || {};
|
||||
const contentType = String(headers['content-type'] || res.data?.type || '').toLowerCase();
|
||||
return contentType.includes('application/json') || contentType.includes('text/json');
|
||||
};
|
||||
|
||||
/**
|
||||
* 判断响应体是否已是解析好的对象(无需再解析)。
|
||||
* 注意 Blob 也是 object,必须排除,否则会把待解析的文件流误判为已完成。
|
||||
*/
|
||||
const isPlainObject = value =>
|
||||
value !== null && typeof value === 'object' && !(value instanceof Blob);
|
||||
//跨域请求,允许保存cookie
|
||||
axios.defaults.withCredentials = true;
|
||||
// NProgress Configuration
|
||||
@@ -140,7 +173,7 @@ axios.interceptors.request.use(
|
||||
|
||||
//http response拦截
|
||||
axios.interceptors.response.use(
|
||||
res => {
|
||||
async res => {
|
||||
NProgress.done();
|
||||
//获取配置信息
|
||||
const config = res.config;
|
||||
@@ -149,6 +182,22 @@ axios.interceptors.response.use(
|
||||
if (cryptoData) {
|
||||
res.data = JSON.parse(crypto.decryptAES(res.data, crypto.aesKey));
|
||||
}
|
||||
// blob 请求(文件下载、导入)的响应体是 Blob,直接读 res.data.code / res.data.msg
|
||||
// 拿不到任何字段:状态码会退回 res.status、提示文案会落到「系统错误」兜底,
|
||||
// 用户就会以为系统崩了——而后端其实返回了明确的业务提示(如「未读取到数据行…」)。
|
||||
// 这里按 Content-Type 判断:JSON 才解析回对象;真·文件流(Excel 等)保持原样不动。
|
||||
// 兼容 Blob(浏览器 responseType='blob')与 string(非浏览器环境)两种形态。
|
||||
if (isJsonContentType(res) && !isPlainObject(res.data)) {
|
||||
try {
|
||||
const text = res.data instanceof Blob ? await res.data.text() : res.data;
|
||||
const parsed = typeof text === 'string' && text ? JSON.parse(text) : null;
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
res.data = parsed;
|
||||
}
|
||||
} catch (ignored) {
|
||||
// 声明是 JSON 却解析失败:保持原样,交给下游按文件处理
|
||||
}
|
||||
}
|
||||
//获取状态信息
|
||||
const status = res.data.error_code || res.data.code || res.status;
|
||||
const statusWhiteList = website.statusWhiteList || [];
|
||||
@@ -204,26 +253,21 @@ axios.interceptors.response.use(
|
||||
|
||||
// OAuth2 业务错误码提示
|
||||
if (status > 2000 && !validatenull(res.data.error_description)) {
|
||||
ElMessage({
|
||||
message: message,
|
||||
type: 'error',
|
||||
});
|
||||
return Promise.reject(new Error(message));
|
||||
return Promise.reject(notifiedError(message));
|
||||
}
|
||||
|
||||
// 其余非 200 响应统一提示
|
||||
if (status !== 200) {
|
||||
ElMessage({
|
||||
message: message,
|
||||
type: 'error',
|
||||
});
|
||||
return Promise.reject(new Error(message));
|
||||
return Promise.reject(notifiedError(message));
|
||||
}
|
||||
return res;
|
||||
},
|
||||
error => {
|
||||
NProgress.done();
|
||||
return Promise.reject(new Error(error));
|
||||
// 保留原始 axios 错误对象:下游(如导入失败明细解析)需要 error.response
|
||||
// 读取响应体。若包装成 new Error(error),response 会丢失、message 退化成
|
||||
// "[object Object]",用户只能看到无意义的通用提示。
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -38,6 +38,20 @@ export const createOption = () => ({
|
||||
menuWidth: 240,
|
||||
menuFixed: 'right',
|
||||
column: [
|
||||
{
|
||||
label: '计量单位编码',
|
||||
prop: 'unitCode',
|
||||
minWidth: 150,
|
||||
search: true,
|
||||
searchOrder: 4,
|
||||
searchSpan: 6,
|
||||
maxlength: 50,
|
||||
showWordLimit: true,
|
||||
rules: [
|
||||
{ required: true, message: '请输入计量单位编码', trigger: 'blur' },
|
||||
{ max: 50, message: '计量单位编码不能超过50个字', trigger: 'blur' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '计量单位',
|
||||
prop: 'unitName',
|
||||
|
||||
@@ -3,6 +3,8 @@ import { getToken } from '@/utils/auth';
|
||||
import store from '@/store';
|
||||
import { generateIframePath, processUrlForQuery, isURL } from './router';
|
||||
import { wrapViewLoader } from '@/utils/chunk-reload';
|
||||
|
||||
// 保持懒加载,避免 eager 与 store 循环依赖(Cannot access 'store' before initialization)
|
||||
const modules = import.meta.glob('../**/**/*.vue');
|
||||
|
||||
// 将多级路由扁平化为二级路由,支持 keep-alive 跨层级缓存
|
||||
|
||||
@@ -74,6 +74,82 @@ export default [
|
||||
isAuth: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/vehicle/customer-archive/public-view',
|
||||
name: '查看客商信息',
|
||||
component: () => import('@/views/vehicle/customer-archive-public-view.vue'),
|
||||
meta: {
|
||||
keepAlive: false,
|
||||
isTab: false,
|
||||
isAuth: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/business/project-apply/public-view',
|
||||
name: '查看项目信息',
|
||||
component: () => import('@/views/mk/public-biz-view.vue'),
|
||||
meta: {
|
||||
keepAlive: false,
|
||||
isTab: false,
|
||||
isAuth: false,
|
||||
bizType: 'project-apply',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/business/contract-manage/public-view',
|
||||
name: '查看合同信息',
|
||||
component: () => import('@/views/mk/public-biz-view.vue'),
|
||||
meta: {
|
||||
keepAlive: false,
|
||||
isTab: false,
|
||||
isAuth: false,
|
||||
bizType: 'contract-manage',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/business/waybill-manage/public-view',
|
||||
name: '查看运单信息',
|
||||
component: () => import('@/views/mk/public-biz-view.vue'),
|
||||
meta: {
|
||||
keepAlive: false,
|
||||
isTab: false,
|
||||
isAuth: false,
|
||||
bizType: 'waybill-manage',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/settlement/pre-settlement/public-view',
|
||||
name: '查看预结算信息',
|
||||
component: () => import('@/views/mk/public-biz-view.vue'),
|
||||
meta: {
|
||||
keepAlive: false,
|
||||
isTab: false,
|
||||
isAuth: false,
|
||||
bizType: 'pre-settlement',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/settlement/formal-settlement/public-view',
|
||||
name: '查看正式结算信息',
|
||||
component: () => import('@/views/mk/public-biz-view.vue'),
|
||||
meta: {
|
||||
keepAlive: false,
|
||||
isTab: false,
|
||||
isAuth: false,
|
||||
bizType: 'formal-settlement',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/payment/payment-application/public-view',
|
||||
name: '查看付款申请信息',
|
||||
component: () => import('@/views/mk/public-biz-view.vue'),
|
||||
meta: {
|
||||
keepAlive: false,
|
||||
isTab: false,
|
||||
isAuth: false,
|
||||
bizType: 'payment-application',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
name: '主页',
|
||||
|
||||
+47
-39
@@ -1,5 +1,8 @@
|
||||
import Layout from '@/page/index/index.vue';
|
||||
import Store from '@/store/';
|
||||
import { wrapViewLoader } from '@/utils/chunk-reload';
|
||||
|
||||
const loadView = loader => wrapViewLoader(loader);
|
||||
|
||||
export default [
|
||||
{
|
||||
@@ -10,7 +13,7 @@ export default [
|
||||
path: '',
|
||||
name: '汇票付款表单',
|
||||
meta: { keepAlive: false, activeMenu: '/payment/bill-payment' },
|
||||
component: () => import('@/views/payment/bill-payment-form.vue'),
|
||||
component: loadView(() => import('@/views/payment/bill-payment-form.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -22,7 +25,7 @@ export default [
|
||||
path: '',
|
||||
name: '汇票台账表单',
|
||||
meta: { keepAlive: false, activeMenu: '/payment/bill-ledger' },
|
||||
component: () => import('@/views/payment/bill-ledger-form.vue'),
|
||||
component: loadView(() => import('@/views/payment/bill-ledger-form.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -34,7 +37,7 @@ export default [
|
||||
path: '',
|
||||
name: '认领记录详情',
|
||||
meta: { keepAlive: false, activeMenu: '/payment/receipt-claim-record' },
|
||||
component: () => import('@/views/payment/receipt-claim-record-form.vue'),
|
||||
component: loadView(() => import('@/views/payment/receipt-claim-record-form.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -46,7 +49,7 @@ export default [
|
||||
path: '',
|
||||
name: '收款流水认领',
|
||||
meta: { keepAlive: false, activeMenu: '/payment/receipt-flow' },
|
||||
component: () => import('@/views/payment/receipt-flow-form.vue'),
|
||||
component: loadView(() => import('@/views/payment/receipt-flow-form.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -58,7 +61,7 @@ export default [
|
||||
path: '',
|
||||
name: '收票登记',
|
||||
meta: { keepAlive: false, activeMenu: '/payment/invoice-receipt' },
|
||||
component: () => import('@/views/payment/invoice-receipt-form.vue'),
|
||||
component: loadView(() => import('@/views/payment/invoice-receipt-form.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -70,7 +73,7 @@ export default [
|
||||
path: '',
|
||||
name: '开票申请',
|
||||
meta: { keepAlive: false, activeMenu: '/payment/invoice-application' },
|
||||
component: () => import('@/views/payment/invoice-application-form.vue'),
|
||||
component: loadView(() => import('@/views/payment/invoice-application-form.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -82,7 +85,7 @@ export default [
|
||||
path: '',
|
||||
name: '付款申请',
|
||||
meta: { keepAlive: false, activeMenu: '/payment/payment-application' },
|
||||
component: () => import('@/views/payment/payment-application-form.vue'),
|
||||
component: loadView(() => import('@/views/payment/payment-application-form.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -97,7 +100,7 @@ export default [
|
||||
keepAlive: false,
|
||||
activeMenu: '/settlement/pre-settlement',
|
||||
},
|
||||
component: () => import('@/views/settlement/pre-settlement-form.vue'),
|
||||
component: loadView(() => import('@/views/settlement/pre-settlement-form.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -112,7 +115,7 @@ export default [
|
||||
keepAlive: false,
|
||||
activeMenu: '/settlement/formal-settlement',
|
||||
},
|
||||
component: () => import('@/views/settlement/formal-settlement-form.vue'),
|
||||
component: loadView(() => import('@/views/settlement/formal-settlement-form.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -127,7 +130,7 @@ export default [
|
||||
keepAlive: false,
|
||||
activeMenu: '/settlement/transport-reconciliation',
|
||||
},
|
||||
component: () => import('@/views/settlement/transport-reconciliation-form.vue'),
|
||||
component: loadView(() => import('@/views/settlement/transport-reconciliation-form.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -139,7 +142,7 @@ export default [
|
||||
path: '',
|
||||
name: '执行凭证批次详情',
|
||||
meta: { keepAlive: false, activeMenu: '/business/voucher-manage' },
|
||||
component: () => import('@/views/business/voucher-manage-detail.vue'),
|
||||
component: loadView(() => import('@/views/business/voucher-manage-detail.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -151,7 +154,7 @@ export default [
|
||||
path: '',
|
||||
name: '新增合同管理',
|
||||
meta: { keepAlive: false },
|
||||
component: () => import('@/views/business/contract-manage.vue'),
|
||||
component: loadView(() => import('@/views/business/contract-manage.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -163,7 +166,7 @@ export default [
|
||||
path: '',
|
||||
name: '合同详情',
|
||||
meta: { keepAlive: false, activeMenu: '/business/contract-manage' },
|
||||
component: () => import('@/views/business/contract-manage.vue'),
|
||||
component: loadView(() => import('@/views/business/contract-manage.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -175,7 +178,7 @@ export default [
|
||||
path: '',
|
||||
name: '新增项目申请',
|
||||
meta: { keepAlive: false },
|
||||
component: () => import('@/views/business/project-apply.vue'),
|
||||
component: loadView(() => import('@/views/business/project-apply.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -187,7 +190,7 @@ export default [
|
||||
path: '',
|
||||
name: '新增编辑运单管理',
|
||||
meta: { keepAlive: false },
|
||||
component: () => import('@/views/business/waybill-manage.vue'),
|
||||
component: loadView(() => import('@/views/business/waybill-manage.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -199,7 +202,7 @@ export default [
|
||||
path: '',
|
||||
name: '运单管理详情',
|
||||
meta: { keepAlive: false, activeMenu: '/business/waybill-manage' },
|
||||
component: () => import('@/views/business/waybill-manage-detail.vue'),
|
||||
component: loadView(() => import('@/views/business/waybill-manage-detail.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -211,7 +214,7 @@ export default [
|
||||
path: '',
|
||||
name: '导入运单',
|
||||
meta: { keepAlive: false, activeMenu: '/business/waybill-import' },
|
||||
component: () => import('@/views/business/waybill-import.vue'),
|
||||
component: loadView(() => import('@/views/business/waybill-import.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -223,7 +226,7 @@ export default [
|
||||
path: '',
|
||||
name: '新建导入运单',
|
||||
meta: { keepAlive: false, activeMenu: '/business/waybill-import' },
|
||||
component: () => import('@/views/business/waybill-import-form.vue'),
|
||||
component: loadView(() => import('@/views/business/waybill-import-form.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -235,7 +238,7 @@ export default [
|
||||
path: '',
|
||||
name: '新增编辑配载管理',
|
||||
meta: { keepAlive: false },
|
||||
component: () => import('@/views/business/loading-manage.vue'),
|
||||
component: loadView(() => import('@/views/business/loading-manage.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -259,7 +262,7 @@ export default [
|
||||
path: '',
|
||||
name: '新增编辑运输计划',
|
||||
meta: { keepAlive: false },
|
||||
component: () => import('@/views/business/transport-plan.vue'),
|
||||
component: loadView(() => import('@/views/business/transport-plan.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -271,7 +274,7 @@ export default [
|
||||
path: '',
|
||||
name: '导入运输计划',
|
||||
meta: { keepAlive: false, activeMenu: '/business/transport-plan' },
|
||||
component: () => import('@/views/business/transport-plan-import.vue'),
|
||||
component: loadView(() => import('@/views/business/transport-plan-import.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -283,7 +286,7 @@ export default [
|
||||
path: '',
|
||||
name: '计划调度',
|
||||
meta: { keepAlive: false, activeMenu: '/business/transport-plan' },
|
||||
component: () => import('@/views/business/transport-plan-dispatch.vue'),
|
||||
component: loadView(() => import('@/views/business/transport-plan-dispatch.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -295,7 +298,7 @@ export default [
|
||||
path: '',
|
||||
name: '新增编辑运单模板',
|
||||
meta: { keepAlive: false },
|
||||
component: () => import('@/views/business/shipping-template.vue'),
|
||||
component: loadView(() => import('@/views/business/shipping-template.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -307,7 +310,7 @@ export default [
|
||||
path: '',
|
||||
name: '新增客商档案',
|
||||
meta: { keepAlive: false },
|
||||
component: () => import('@/views/vehicle/customer-archive.vue'),
|
||||
component: loadView(() => import('@/views/vehicle/customer-archive.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -319,7 +322,7 @@ export default [
|
||||
path: '',
|
||||
name: '合同变更',
|
||||
meta: { keepAlive: false },
|
||||
component: () => import('@/views/business/contract-manage-change.vue'),
|
||||
component: loadView(() => import('@/views/business/contract-manage-change.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -335,7 +338,7 @@ export default [
|
||||
meta: {
|
||||
i18n: 'dashboard',
|
||||
},
|
||||
component: () => import(/* webpackChunkName: "views" */ '@/views/wel/index.vue'),
|
||||
component: loadView(() => import(/* webpackChunkName: "views" */ '@/views/wel/index.vue')),
|
||||
},
|
||||
{
|
||||
path: 'dashboard',
|
||||
@@ -344,7 +347,7 @@ export default [
|
||||
i18n: 'dashboard',
|
||||
menu: false,
|
||||
},
|
||||
component: () => import(/* webpackChunkName: "views" */ '@/views/wel/dashboard.vue'),
|
||||
component: loadView(() => import(/* webpackChunkName: "views" */ '@/views/wel/dashboard.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -359,7 +362,7 @@ export default [
|
||||
meta: {
|
||||
i18n: 'test',
|
||||
},
|
||||
component: () => import(/* webpackChunkName: "views" */ '@/views/util/test.vue'),
|
||||
component: loadView(() => import(/* webpackChunkName: "views" */ '@/views/util/test.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -374,8 +377,9 @@ export default [
|
||||
meta: {
|
||||
i18n: 'dict',
|
||||
},
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "views" */ '@/views/util/demo/dict-horizontal.vue'),
|
||||
component: loadView(() =>
|
||||
import(/* webpackChunkName: "views" */ '@/views/util/demo/dict-horizontal.vue')
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -390,8 +394,9 @@ export default [
|
||||
meta: {
|
||||
i18n: 'dict',
|
||||
},
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "views" */ '@/views/util/demo/dict-vertical.vue'),
|
||||
component: loadView(() =>
|
||||
import(/* webpackChunkName: "views" */ '@/views/util/demo/dict-vertical.vue')
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -406,7 +411,7 @@ export default [
|
||||
meta: {
|
||||
i18n: 'info',
|
||||
},
|
||||
component: () => import(/* webpackChunkName: "views" */ '@/views/system/userinfo.vue'),
|
||||
component: loadView(() => import(/* webpackChunkName: "views" */ '@/views/system/userinfo.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -421,8 +426,9 @@ export default [
|
||||
meta: {
|
||||
i18n: 'work',
|
||||
},
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "views" */ '@/views/work/process/leave/form.vue'),
|
||||
component: loadView(() =>
|
||||
import(/* webpackChunkName: "views" */ '@/views/work/process/leave/form.vue')
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'handle/:taskId/:processInstanceId/:businessId',
|
||||
@@ -430,8 +436,9 @@ export default [
|
||||
meta: {
|
||||
i18n: 'work',
|
||||
},
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "views" */ '@/views/work/process/leave/handle.vue'),
|
||||
component: loadView(() =>
|
||||
import(/* webpackChunkName: "views" */ '@/views/work/process/leave/handle.vue')
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'detail/:processInstanceId/:businessId',
|
||||
@@ -439,8 +446,9 @@ export default [
|
||||
meta: {
|
||||
i18n: 'work',
|
||||
},
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "views" */ '@/views/work/process/leave/detail.vue'),
|
||||
component: loadView(() =>
|
||||
import(/* webpackChunkName: "views" */ '@/views/work/process/leave/detail.vue')
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -28,6 +28,34 @@ const parseBlobJson = async blob => {
|
||||
return text ? JSON.parse(text) : {};
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析导入失败时的错误提示文案。
|
||||
*
|
||||
* 背景:导入接口统一用 `responseType: 'blob'` 请求,因此**非 2xx 响应体也是 Blob**。
|
||||
* axios 的错误分支不做解析,直接 reject,`error.message` 只有
|
||||
* 「Request failed with status code 400」这类通用文案(或全局兜底的「系统错误」),
|
||||
* 用户会误以为系统崩了——而后端其实已返回了明确的中文业务提示(如「导入数据不能为空」)。
|
||||
*
|
||||
* 这里补上这一步:从错误响应的 Blob 里取出后端 `msg`,取不到再退回通用文案。
|
||||
*/
|
||||
const resolveErrorMessage = async error => {
|
||||
const blob = error?.response?.data ?? error?.data;
|
||||
if (blob instanceof Blob) {
|
||||
try {
|
||||
const result = await parseBlobJson(blob);
|
||||
if (result?.msg) return result.msg;
|
||||
} catch (ignored) {
|
||||
// 响应体不是 JSON(如网关返回的 HTML 错误页),退回通用文案
|
||||
}
|
||||
}
|
||||
const fallback = error?.response?.data?.msg || error?.message;
|
||||
// axios 的英文通用文案对用户没有意义,统一换成中文提示
|
||||
if (!fallback || /Request failed with status code/i.test(fallback)) {
|
||||
return '导入失败,请检查导入文件后重试';
|
||||
}
|
||||
return fallback;
|
||||
};
|
||||
|
||||
/**
|
||||
* 加工后端返回的导入失败明细 Excel。
|
||||
* 若传入 failDetailDecorator(函数),则动态加载 exceljs 读取 blob 流,
|
||||
@@ -125,7 +153,11 @@ export const handleImportExcel = async (vm, option, column, businessName, onSucc
|
||||
option.onSuccess(res);
|
||||
}
|
||||
} catch (error) {
|
||||
vm.$message.error(error.message || '导入失败');
|
||||
// 拦截器已提示过的错误(如后端 400 返回的业务提示)不再重复弹一次,
|
||||
// 只保留一次清晰的中文提示。
|
||||
if (!error?._notified) {
|
||||
vm.$message.error(await resolveErrorMessage(error));
|
||||
}
|
||||
if (typeof option.onError === 'function') {
|
||||
option.onError(error);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
import { processDelete, processSubmit } from '@/api/system/business-process';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
export const MK_BIZ = {
|
||||
'customer-archive': {
|
||||
dictName: '提交客商审核流',
|
||||
subjectPrefix: '客商准入审批',
|
||||
rejected: ['rejected'],
|
||||
},
|
||||
'project-apply': {
|
||||
dictName: '提交项目审核流',
|
||||
subjectPrefix: '项目审批',
|
||||
rejected: ['rejected', 'change_rejected', 'withdrawn'],
|
||||
},
|
||||
'contract-manage': {
|
||||
dictName: '提交合同审核流',
|
||||
subjectPrefix: '合同审批',
|
||||
rejected: ['rejected', 'change_rejected', 'withdrawn'],
|
||||
},
|
||||
'waybill-manage': {
|
||||
dictName: '提交运单审核流',
|
||||
subjectPrefix: '运单审批',
|
||||
rejected: ['rejected', 'returned'],
|
||||
},
|
||||
'pre-settlement': {
|
||||
dictName: '提交预结算审核流',
|
||||
subjectPrefix: '预结算审批',
|
||||
rejected: ['returned'],
|
||||
},
|
||||
'formal-settlement': {
|
||||
dictName: '提交正式结算审核流',
|
||||
subjectPrefix: '正式结算审批',
|
||||
rejected: ['returned'],
|
||||
},
|
||||
'payment-application': {
|
||||
dictName: '提交付款审核流',
|
||||
subjectPrefix: '付款审批',
|
||||
rejected: ['returned'],
|
||||
},
|
||||
};
|
||||
|
||||
export async function resolveMkTemplateCode(dictName) {
|
||||
const res = await getDictionary({ code: 'mk_template' });
|
||||
const list = res?.data?.data || [];
|
||||
const matched = list.find(item => String(item.dictValue || '').trim() === dictName);
|
||||
const templateCode = matched?.dictKey;
|
||||
if (!templateCode) {
|
||||
ElMessage.warning(`未配置业务字典 mk_template「${dictName}」,无法提交审核流`);
|
||||
return Promise.reject(new Error(`未配置业务字典 mk_template「${dictName}」`));
|
||||
}
|
||||
return String(templateCode);
|
||||
}
|
||||
|
||||
export async function submitMkApprovalFlow({
|
||||
bizType,
|
||||
formInstanceId,
|
||||
subjectName = '',
|
||||
approvalStatus = '',
|
||||
}) {
|
||||
const conf = MK_BIZ[bizType];
|
||||
if (!conf) {
|
||||
return Promise.reject(new Error(`不支持的MK业务类型:${bizType}`));
|
||||
}
|
||||
if ((conf.rejected || []).includes(approvalStatus)) {
|
||||
await processDelete({ formInstanceId: String(formInstanceId) });
|
||||
}
|
||||
const templateCode = await resolveMkTemplateCode(conf.dictName);
|
||||
const subject = subjectName
|
||||
? `${conf.subjectPrefix}:${subjectName}`
|
||||
: `${conf.subjectPrefix}:${formInstanceId}`;
|
||||
return processSubmit({
|
||||
templateCode,
|
||||
formInstanceId: String(formInstanceId),
|
||||
subject,
|
||||
bizType,
|
||||
});
|
||||
}
|
||||
@@ -504,7 +504,7 @@ export default {
|
||||
propsHttp: {
|
||||
res: 'data',
|
||||
},
|
||||
tip: '请上传 .xls,.xlsx 标准格式文件',
|
||||
tip: '请上传 .xls,.xlsx 标准格式文件;日期支持 2026-08-02、2026-8-2、2026/8/2 等写法',
|
||||
accept: '.xls,.xlsx',
|
||||
action: '/blade-system/airport-master/import-airport-master',
|
||||
},
|
||||
|
||||
@@ -274,7 +274,7 @@ export default {
|
||||
res: 'data',
|
||||
},
|
||||
headers: getUploadHeaders(),
|
||||
tip: '请上传 .xls,.xlsx 标准格式文件',
|
||||
tip: '请上传 .xls,.xlsx 标准格式文件;日期支持 2026-08-02、2026-8-2、2026/8/2 等写法',
|
||||
action: '/blade-system/cargo-type/import-cargo-type',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -326,7 +326,7 @@ export default {
|
||||
propsHttp: {
|
||||
res: 'data',
|
||||
},
|
||||
tip: '请上传 .xls,.xlsx 标准格式文件',
|
||||
tip: '请上传 .xls,.xlsx 标准格式文件;日期支持 2026-08-02、2026-8-2、2026/8/2 等写法',
|
||||
action: '/blade-system/currency/import-currency',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -119,6 +119,7 @@ export default {
|
||||
return this.isAdmin || this.permission?.[code] === true;
|
||||
},
|
||||
normalizeRow(row) {
|
||||
row.unitCode = String(row.unitCode || '').trim();
|
||||
row.unitName = String(row.unitName || '').trim();
|
||||
row.dimension = String(row.dimension || '').trim();
|
||||
row.remark = String(row.remark || '').trim();
|
||||
|
||||
@@ -673,7 +673,7 @@ export default {
|
||||
propsHttp: {
|
||||
res: 'data',
|
||||
},
|
||||
tip: '请上传 .xls,.xlsx 标准格式文件',
|
||||
tip: '请上传 .xls,.xlsx 标准格式文件;日期支持 2026-08-02、2026-8-2、2026/8/2 等写法',
|
||||
accept: '.xls,.xlsx',
|
||||
action: '/blade-system/port-terminal/import-port-terminal',
|
||||
},
|
||||
|
||||
@@ -475,7 +475,7 @@ export default {
|
||||
propsHttp: {
|
||||
res: 'data',
|
||||
},
|
||||
tip: '请上传 .xls,.xlsx 标准格式文件',
|
||||
tip: '请上传 .xls,.xlsx 标准格式文件;日期支持 2026-08-02、2026-8-2、2026/8/2 等写法',
|
||||
accept: '.xls,.xlsx',
|
||||
action: '/blade-system/railway-station/import-railway-station',
|
||||
},
|
||||
|
||||
@@ -309,7 +309,7 @@ export default {
|
||||
propsHttp: {
|
||||
res: 'data',
|
||||
},
|
||||
tip: '请上传 .xls,.xlsx 标准格式文件',
|
||||
tip: '请上传 .xls,.xlsx 标准格式文件;日期支持 2026-08-02、2026-8-2、2026/8/2 等写法',
|
||||
action: '/blade-system/region/import-region',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -157,12 +157,19 @@
|
||||
<el-table-column label="计费单位" width="150"
|
||||
><template #default="{ row }"
|
||||
><span v-if="readonly">{{ displayValue(row.billingUnit) }}</span
|
||||
><el-select v-else v-model="row.billingUnit" clearable filterable :loading="unitLoading"
|
||||
><el-select
|
||||
v-else
|
||||
v-model="row.billingUnit"
|
||||
clearable
|
||||
filterable
|
||||
:disabled="!row.billingElement"
|
||||
:loading="unitLoading"
|
||||
:placeholder="row.billingElement ? '请选择' : '请先选择计费要素'"
|
||||
><el-option
|
||||
v-for="item in unitOptions"
|
||||
:key="item.id || item.dictKey || item.dictValue"
|
||||
:label="item.dictValue"
|
||||
:value="item.dictValue" /></el-select></template
|
||||
v-for="item in unitOptionsFor(row)"
|
||||
:key="item.id || item.value"
|
||||
:label="item.label"
|
||||
:value="item.value" /></el-select></template
|
||||
></el-table-column>
|
||||
<el-table-column label="单价(元)" width="180"
|
||||
><template #default="{ row }"
|
||||
@@ -367,12 +374,19 @@
|
||||
<script>
|
||||
import { getList as getCargoTypeList } from '@/api/base/cargo-type';
|
||||
import { getList as getFeeItemList } from '@/api/base/fee-item';
|
||||
import { getList as getMeasurementUnitList } from '@/api/base/measurement-unit';
|
||||
import { getLazyTree as getRegionLazyTree } from '@/api/base/region';
|
||||
import { InfoFilled } from '@element-plus/icons-vue';
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
import SectionCard from '@/components/section-card/main.vue';
|
||||
|
||||
const clone = value => JSON.parse(JSON.stringify(value));
|
||||
/** 计费要素与计量单位维度的对应关系 */
|
||||
const BILLING_ELEMENT_DIMENSION_MAP = {
|
||||
按重量: '重量',
|
||||
按体积: '体积',
|
||||
按车辆: '数量',
|
||||
};
|
||||
const defaultRule = () => ({
|
||||
feeType: '',
|
||||
feeItem: '',
|
||||
@@ -417,6 +431,7 @@ export default {
|
||||
feeItems: {},
|
||||
feeItemLoadingMap: {},
|
||||
unitOptions: [],
|
||||
measurementUnits: [],
|
||||
unitLoading: false,
|
||||
billingElements: [
|
||||
'按重量',
|
||||
@@ -614,14 +629,53 @@ export default {
|
||||
.finally(() => {
|
||||
this.feeCategoryLoading = false;
|
||||
});
|
||||
this.loadUnitOptions();
|
||||
},
|
||||
loadUnitOptions() {
|
||||
this.unitLoading = true;
|
||||
getDictionary({ code: 'unit_fee' })
|
||||
.then(res => {
|
||||
Promise.all([
|
||||
getDictionary({ code: 'unit_fee' }).then(res => {
|
||||
this.unitOptions = res.data?.data || [];
|
||||
})
|
||||
.finally(() => {
|
||||
this.unitLoading = false;
|
||||
});
|
||||
}),
|
||||
getMeasurementUnitList(1, 9999, { status: 1 }).then(res => {
|
||||
const data = res?.data?.data || res?.data || {};
|
||||
const records = Array.isArray(data) ? data : data.records || [];
|
||||
this.measurementUnits = records.filter(
|
||||
item => item.status === undefined || item.status === null || Number(item.status) === 1
|
||||
);
|
||||
}),
|
||||
]).finally(() => {
|
||||
this.unitLoading = false;
|
||||
});
|
||||
},
|
||||
measurementDimension(billingElement) {
|
||||
return BILLING_ELEMENT_DIMENSION_MAP[billingElement] || '';
|
||||
},
|
||||
unitOptionsFor(row) {
|
||||
const dimension = this.measurementDimension(row?.billingElement);
|
||||
if (dimension) {
|
||||
return this.measurementUnits
|
||||
.filter(item => String(item.dimension || '').trim() === dimension)
|
||||
.map(item => ({
|
||||
id: item.id,
|
||||
label: item.unitName,
|
||||
value: item.unitName,
|
||||
}))
|
||||
.filter(item => item.value);
|
||||
}
|
||||
return (this.unitOptions || [])
|
||||
.map(item => ({
|
||||
id: item.id || item.dictKey || item.dictValue,
|
||||
label: item.dictValue,
|
||||
value: item.dictValue,
|
||||
}))
|
||||
.filter(item => item.value);
|
||||
},
|
||||
syncBillingUnit(row) {
|
||||
const options = this.unitOptionsFor(row);
|
||||
if (!options.some(item => String(item.value) === String(row.billingUnit || ''))) {
|
||||
row.billingUnit = '';
|
||||
}
|
||||
},
|
||||
feeTypeKey(row) {
|
||||
const option = this.feeCategories.find(
|
||||
@@ -699,7 +753,14 @@ export default {
|
||||
return this.typeMap[row.billingElement] || [];
|
||||
},
|
||||
handleElementChange(row) {
|
||||
if (!this.billingTypes(row).includes(row.billingType)) row.billingType = '';
|
||||
const billingTypes = this.billingTypes(row);
|
||||
if (!billingTypes.includes(row.billingType)) {
|
||||
row.billingType =
|
||||
row.billingElement === '固定金额(整单一口价)' && billingTypes.includes('固定一口价')
|
||||
? '固定一口价'
|
||||
: '';
|
||||
}
|
||||
this.syncBillingUnit(row);
|
||||
if (!this.canEditMinimum(row)) {
|
||||
row.minimumBillingWeight = '';
|
||||
row.limitRanges = (row.limitRanges || []).map(item => ({
|
||||
|
||||
@@ -3374,7 +3374,7 @@
|
||||
{{ dispatchDialogStatusText }}
|
||||
</el-tag>
|
||||
<el-tag type="primary" effect="light">
|
||||
{{ formatDispatchTransportType(dispatchRow.transportType) || '公路整车' }}
|
||||
{{ formatDispatchTransportType(dispatchRow.transportType) || '公路运输' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1231,7 +1231,7 @@
|
||||
{{ dispatchDialogStatusText }}
|
||||
</el-tag>
|
||||
<el-tag type="primary" effect="light">
|
||||
{{ formatDispatchTransportType(dispatchRow.transportType) || '公路整车' }}
|
||||
{{ formatDispatchTransportType(dispatchRow.transportType) || '公路运输' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -454,7 +454,7 @@
|
||||
:on-change="fileChange"
|
||||
:on-remove="fileRemove"
|
||||
><el-button type="primary">添加附件</el-button></el-upload
|
||||
><span class="waybill-import-create__file-tip">请上传运单明细表,仅支持 excel 格式</span
|
||||
><span class="waybill-import-create__file-tip">请上传运单明细表,仅支持 excel 格式;日期支持 2026-08-02、2026-8-2、2026/8/2 等写法</span
|
||||
><el-link type="primary" @click="downloadTemplate">下载模板</el-link></el-form-item
|
||||
>
|
||||
</el-form>
|
||||
|
||||
@@ -2212,13 +2212,105 @@
|
||||
<section-card title="物流轨迹">
|
||||
<template #extra>
|
||||
<div class="waybill-manage-page__waybill-track-actions">
|
||||
<el-button plain @click="handleWaybillTrackAction('playback')"
|
||||
<el-button
|
||||
plain
|
||||
:type="waybillTrackMode === 'playback' ? 'primary' : undefined"
|
||||
:loading="waybillTrackLoading"
|
||||
@click="handleWaybillTrackAction('playback')"
|
||||
>轨迹回放</el-button
|
||||
>
|
||||
<el-button plain @click="handleWaybillTrackAction('locate')">实时定位</el-button>
|
||||
<el-button
|
||||
plain
|
||||
:type="waybillTrackMode === 'locate' ? 'primary' : undefined"
|
||||
:loading="waybillLocateLoading"
|
||||
@click="handleWaybillTrackAction('locate')"
|
||||
>实时定位</el-button
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<div class="waybill-manage-page__waybill-map-empty">暂无数据</div>
|
||||
<div
|
||||
v-if="waybillTrackMode === 'playback'"
|
||||
class="waybill-manage-page__waybill-track-toolbar"
|
||||
>
|
||||
<span>时间段</span>
|
||||
<el-date-picker
|
||||
v-model="waybillTrackDateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
:clearable="false"
|
||||
:disabled="waybillTrackLoading"
|
||||
/>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="waybillTrackLoading"
|
||||
@click="loadWaybillTrack"
|
||||
>查询</el-button
|
||||
>
|
||||
<span class="waybill-manage-page__waybill-track-total"
|
||||
>轨迹点 {{ waybillTrackInfo?.total || 0 }} 个</span
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-if="waybillTrackMode === 'locate'"
|
||||
class="waybill-manage-page__waybill-locate"
|
||||
>
|
||||
<div v-if="waybillLocateInfo" class="waybill-manage-page__waybill-locate-meta">
|
||||
<div>
|
||||
<span>车牌号</span><strong>{{ waybillLocateInfo.vehicleNo || '-' }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>定位时间</span><strong>{{ waybillLocateInfo.locateTime || '-' }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>速度</span><strong>{{ waybillLocateInfo.speed || '-' }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>方向</span><strong>{{ waybillLocateInfo.direction || '-' }}</strong>
|
||||
</div>
|
||||
<div class="waybill-manage-page__waybill-locate-address">
|
||||
<span>地址</span><strong>{{ waybillLocateInfo.address || '-' }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>坐标</span
|
||||
><strong
|
||||
>{{
|
||||
waybillLocateInfo.longitude != null && waybillLocateInfo.latitude != null
|
||||
? `${waybillLocateInfo.longitude}, ${waybillLocateInfo.latitude}`
|
||||
: '-'
|
||||
}}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref="waybillLocateMap"
|
||||
class="waybill-manage-page__waybill-locate-map"
|
||||
></div>
|
||||
<div
|
||||
v-if="!waybillLocateInfo"
|
||||
class="waybill-manage-page__waybill-map-tip"
|
||||
>
|
||||
{{ waybillLocateHint || '暂无数据' }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="waybillTrackMode === 'playback'"
|
||||
class="waybill-manage-page__waybill-locate"
|
||||
>
|
||||
<div
|
||||
ref="waybillTrackMap"
|
||||
class="waybill-manage-page__waybill-locate-map waybill-manage-page__waybill-locate-map--track"
|
||||
></div>
|
||||
<div
|
||||
v-if="!(waybillTrackInfo?.points || []).length"
|
||||
class="waybill-manage-page__waybill-map-tip"
|
||||
>
|
||||
{{ waybillTrackHint || '暂无数据' }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="waybill-manage-page__waybill-map-empty">暂无数据</div>
|
||||
</section-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -3029,13 +3121,14 @@ import {
|
||||
getList as getProcessConfigList,
|
||||
getVoucherImages as getProcessConfigVoucherImages,
|
||||
} from '@/api/business/process-config';
|
||||
import { getPunchRecords as getWaybillPunchRecords } from '@/api/business/waybill-manage';
|
||||
import { getPunchRecords as getWaybillPunchRecords, locateVehicle, trackVehicle } from '@/api/business/waybill-manage';
|
||||
import { getList as getDriverList } from '@/api/transportCapacity/driver';
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
import { getDictionary as getSystemDictionary } from '@/api/system/dict';
|
||||
import { addressTypeOptions } from '@/option/base/common-address';
|
||||
import { packageOptions } from '@/option/business/common';
|
||||
import { formatUpdateUserName } from '@/utils/audit';
|
||||
import { submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { openImportDialog } from '@/utils/import-excel';
|
||||
import { applyTableMenuWidth } from '@/utils/table-menu';
|
||||
@@ -3292,6 +3385,21 @@ export default {
|
||||
waybillRouteChangeRecords: [],
|
||||
waybillRouteChangeDragIndex: -1,
|
||||
waybillRouteChangeSaving: false,
|
||||
waybillLocateLoading: false,
|
||||
waybillLocateInfo: null,
|
||||
waybillLocateHint: '',
|
||||
waybillLocateMapInstance: null,
|
||||
waybillLocateMarker: null,
|
||||
waybillLocateInfoWindow: null,
|
||||
waybillTrackMode: '',
|
||||
waybillTrackLoading: false,
|
||||
waybillTrackInfo: null,
|
||||
waybillTrackHint: '',
|
||||
waybillTrackDateRange: [],
|
||||
waybillTrackMapInstance: null,
|
||||
waybillTrackPolyline: null,
|
||||
waybillTrackStartMarker: null,
|
||||
waybillTrackEndMarker: null,
|
||||
mileageDialog: {
|
||||
visible: false,
|
||||
submitting: false,
|
||||
@@ -4523,6 +4631,13 @@ export default {
|
||||
});
|
||||
},
|
||||
closeDetail() {
|
||||
this.destroyWaybillLocateMap();
|
||||
this.destroyWaybillTrackMap();
|
||||
this.waybillTrackMode = '';
|
||||
this.waybillLocateInfo = null;
|
||||
this.waybillLocateHint = '';
|
||||
this.waybillTrackInfo = null;
|
||||
this.waybillTrackHint = '';
|
||||
if (this.isStandaloneWaybillDetailPage) {
|
||||
this.$router.$avueRouter?.closeTag?.();
|
||||
this.$router.push({ path: '/business/waybill-manage', query: {} });
|
||||
@@ -4867,7 +4982,262 @@ export default {
|
||||
}
|
||||
},
|
||||
handleWaybillTrackAction(action) {
|
||||
this.$message.info(action === 'playback' ? '暂无可回放的物流轨迹' : '暂无车辆实时定位数据');
|
||||
if (action === 'playback') {
|
||||
this.destroyWaybillLocateMap();
|
||||
this.waybillTrackMode = 'playback';
|
||||
this.waybillLocateInfo = null;
|
||||
this.waybillLocateHint = '';
|
||||
if (!Array.isArray(this.waybillTrackDateRange) || this.waybillTrackDateRange.length !== 2) {
|
||||
this.waybillTrackDateRange = this.buildDefaultTrackDateRange();
|
||||
}
|
||||
this.loadWaybillTrack();
|
||||
return;
|
||||
}
|
||||
this.destroyWaybillTrackMap();
|
||||
this.waybillTrackMode = 'locate';
|
||||
this.waybillTrackInfo = null;
|
||||
this.waybillTrackHint = '';
|
||||
this.loadWaybillLocate();
|
||||
},
|
||||
buildDefaultTrackDateRange() {
|
||||
const pad = value => String(value).padStart(2, '0');
|
||||
const formatDate = date =>
|
||||
`${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
||||
const today = new Date();
|
||||
const yesterday = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1);
|
||||
return [formatDate(yesterday), formatDate(today)];
|
||||
},
|
||||
destroyWaybillLocateMap() {
|
||||
if (this.waybillLocateMarker) {
|
||||
this.waybillLocateMarker.setMap(null);
|
||||
this.waybillLocateMarker = null;
|
||||
}
|
||||
if (this.waybillLocateInfoWindow) {
|
||||
this.waybillLocateInfoWindow.close();
|
||||
this.waybillLocateInfoWindow = null;
|
||||
}
|
||||
if (this.waybillLocateMapInstance) {
|
||||
this.waybillLocateMapInstance.destroy();
|
||||
this.waybillLocateMapInstance = null;
|
||||
}
|
||||
},
|
||||
destroyWaybillTrackMap() {
|
||||
if (this.waybillTrackPolyline) {
|
||||
this.waybillTrackPolyline.setMap(null);
|
||||
this.waybillTrackPolyline = null;
|
||||
}
|
||||
if (this.waybillTrackStartMarker) {
|
||||
this.waybillTrackStartMarker.setMap(null);
|
||||
this.waybillTrackStartMarker = null;
|
||||
}
|
||||
if (this.waybillTrackEndMarker) {
|
||||
this.waybillTrackEndMarker.setMap(null);
|
||||
this.waybillTrackEndMarker = null;
|
||||
}
|
||||
if (this.waybillTrackMapInstance) {
|
||||
this.waybillTrackMapInstance.destroy();
|
||||
this.waybillTrackMapInstance = null;
|
||||
}
|
||||
},
|
||||
async loadWaybillLocate() {
|
||||
const waybillId = this.detailRow?.id;
|
||||
if (!waybillId) {
|
||||
this.$message.warning('运单信息不完整');
|
||||
return;
|
||||
}
|
||||
if (!this.detailRow.vehicleNo) {
|
||||
this.$message.warning('运单未绑定车牌号,无法实时定位');
|
||||
return;
|
||||
}
|
||||
this.waybillLocateLoading = true;
|
||||
this.waybillLocateHint = '正在获取车辆实时定位...';
|
||||
this.waybillLocateInfo = null;
|
||||
try {
|
||||
const locateApi =
|
||||
typeof this.api.locateVehicle === 'function' ? this.api.locateVehicle : locateVehicle;
|
||||
const res = await locateApi(waybillId);
|
||||
const data = res?.data?.data || null;
|
||||
if (!data || data.longitude == null || data.latitude == null) {
|
||||
this.waybillLocateInfo = null;
|
||||
this.waybillLocateHint = '暂无车辆实时定位数据';
|
||||
this.$message.warning('暂无车辆实时定位数据');
|
||||
return;
|
||||
}
|
||||
this.waybillLocateInfo = data;
|
||||
this.waybillLocateHint = '';
|
||||
await this.$nextTick();
|
||||
await this.renderWaybillLocateMap(data);
|
||||
} catch (error) {
|
||||
this.waybillLocateInfo = null;
|
||||
this.waybillLocateHint = '实时定位获取失败';
|
||||
this.$message.error(error?.message || '实时定位获取失败');
|
||||
} finally {
|
||||
this.waybillLocateLoading = false;
|
||||
}
|
||||
},
|
||||
async loadWaybillTrack() {
|
||||
const waybillId = this.detailRow?.id;
|
||||
if (!waybillId) {
|
||||
this.$message.warning('运单信息不完整');
|
||||
return;
|
||||
}
|
||||
if (!this.detailRow.vehicleNo) {
|
||||
this.$message.warning('运单未绑定车牌号,无法查询历史轨迹');
|
||||
return;
|
||||
}
|
||||
if (!Array.isArray(this.waybillTrackDateRange) || this.waybillTrackDateRange.length !== 2) {
|
||||
this.waybillTrackDateRange = this.buildDefaultTrackDateRange();
|
||||
}
|
||||
const [startDate, endDate] = this.waybillTrackDateRange;
|
||||
this.waybillTrackLoading = true;
|
||||
this.waybillTrackHint = '正在获取历史轨迹...';
|
||||
this.destroyWaybillTrackMap();
|
||||
try {
|
||||
const trackApi =
|
||||
typeof this.api.trackVehicle === 'function' ? this.api.trackVehicle : trackVehicle;
|
||||
const res = await trackApi(waybillId, startDate, endDate);
|
||||
const data = res?.data?.data || null;
|
||||
const points = Array.isArray(data?.points) ? data.points : [];
|
||||
if (!data || points.length === 0) {
|
||||
this.waybillTrackInfo = data || { total: 0, points: [] };
|
||||
this.waybillTrackHint = '暂无可回放的物流轨迹';
|
||||
this.$message.warning('暂无可回放的物流轨迹');
|
||||
return;
|
||||
}
|
||||
this.waybillTrackInfo = data;
|
||||
this.waybillTrackHint = '';
|
||||
await this.$nextTick();
|
||||
await this.renderWaybillTrackMap(points);
|
||||
} catch (error) {
|
||||
this.waybillTrackInfo = null;
|
||||
this.waybillTrackHint = '历史轨迹获取失败';
|
||||
this.$message.error(error?.message || '历史轨迹获取失败');
|
||||
} finally {
|
||||
this.waybillTrackLoading = false;
|
||||
}
|
||||
},
|
||||
async renderWaybillLocateMap(locateInfo) {
|
||||
const longitude = Number(locateInfo?.longitude);
|
||||
const latitude = Number(locateInfo?.latitude);
|
||||
if (!Number.isFinite(longitude) || !Number.isFinite(latitude)) {
|
||||
this.$message.warning('定位坐标无效,无法在地图上展示');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.loadAmap();
|
||||
await this.$nextTick();
|
||||
const container = this.$refs.waybillLocateMap;
|
||||
if (!container || !window.AMap) {
|
||||
this.$message.warning('高德地图容器未就绪');
|
||||
return;
|
||||
}
|
||||
this.destroyWaybillLocateMap();
|
||||
this.waybillLocateMapInstance = new window.AMap.Map(container, {
|
||||
zoom: 15,
|
||||
center: [longitude, latitude],
|
||||
viewMode: '2D',
|
||||
resizeEnable: true,
|
||||
});
|
||||
const content = [
|
||||
`<div style="padding:4px 2px;line-height:1.6;font-size:12px;">`,
|
||||
`<div><b>${locateInfo.vehicleNo || '车辆位置'}</b></div>`,
|
||||
locateInfo.locateTime ? `<div>时间:${locateInfo.locateTime}</div>` : '',
|
||||
locateInfo.address ? `<div>地址:${locateInfo.address}</div>` : '',
|
||||
`<div>坐标:${longitude}, ${latitude}</div>`,
|
||||
`</div>`,
|
||||
].join('');
|
||||
this.waybillLocateInfoWindow = new window.AMap.InfoWindow({
|
||||
content,
|
||||
offset: new window.AMap.Pixel(0, -30),
|
||||
});
|
||||
this.waybillLocateMarker = new window.AMap.Marker({
|
||||
position: [longitude, latitude],
|
||||
title: locateInfo.vehicleNo || '车辆位置',
|
||||
anchor: 'bottom-center',
|
||||
});
|
||||
this.waybillLocateMarker.on('click', () => {
|
||||
this.waybillLocateInfoWindow.open(this.waybillLocateMapInstance, [longitude, latitude]);
|
||||
});
|
||||
this.waybillLocateMapInstance.add(this.waybillLocateMarker);
|
||||
this.waybillLocateInfoWindow.open(this.waybillLocateMapInstance, [longitude, latitude]);
|
||||
this.waybillLocateMapInstance.setFitView([this.waybillLocateMarker], false, [40, 40, 40, 40]);
|
||||
setTimeout(() => {
|
||||
this.waybillLocateMapInstance?.resize?.();
|
||||
}, 80);
|
||||
} catch (error) {
|
||||
window.console.warn('实时定位地图渲染失败', error);
|
||||
this.$message.error('高德地图渲染失败,请稍后重试');
|
||||
}
|
||||
},
|
||||
async renderWaybillTrackMap(points) {
|
||||
const path = (points || [])
|
||||
.map(item => [Number(item.longitude), Number(item.latitude)])
|
||||
.filter(item => Number.isFinite(item[0]) && Number.isFinite(item[1]));
|
||||
if (!path.length) {
|
||||
this.$message.warning('轨迹坐标无效,无法在地图上展示');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.loadAmap();
|
||||
await this.$nextTick();
|
||||
const container = this.$refs.waybillTrackMap;
|
||||
if (!container || !window.AMap) {
|
||||
this.$message.warning('高德地图容器未就绪');
|
||||
return;
|
||||
}
|
||||
this.destroyWaybillTrackMap();
|
||||
this.waybillTrackMapInstance = new window.AMap.Map(container, {
|
||||
zoom: 12,
|
||||
center: path[0],
|
||||
viewMode: '2D',
|
||||
resizeEnable: true,
|
||||
});
|
||||
this.waybillTrackPolyline = new window.AMap.Polyline({
|
||||
path,
|
||||
strokeColor: '#409eff',
|
||||
strokeWeight: 6,
|
||||
strokeOpacity: 0.9,
|
||||
lineJoin: 'round',
|
||||
lineCap: 'round',
|
||||
showDir: path.length > 1,
|
||||
});
|
||||
this.waybillTrackStartMarker = new window.AMap.Marker({
|
||||
position: path[0],
|
||||
title: '起点',
|
||||
anchor: 'bottom-center',
|
||||
label: {
|
||||
content: '起',
|
||||
direction: 'top',
|
||||
offset: new window.AMap.Pixel(0, -6),
|
||||
},
|
||||
});
|
||||
this.waybillTrackEndMarker = new window.AMap.Marker({
|
||||
position: path[path.length - 1],
|
||||
title: '终点',
|
||||
anchor: 'bottom-center',
|
||||
label: {
|
||||
content: '终',
|
||||
direction: 'top',
|
||||
offset: new window.AMap.Pixel(0, -6),
|
||||
},
|
||||
});
|
||||
this.waybillTrackMapInstance.add([
|
||||
this.waybillTrackPolyline,
|
||||
this.waybillTrackStartMarker,
|
||||
this.waybillTrackEndMarker,
|
||||
]);
|
||||
this.waybillTrackMapInstance.setFitView(
|
||||
[this.waybillTrackPolyline, this.waybillTrackStartMarker, this.waybillTrackEndMarker],
|
||||
false,
|
||||
[48, 48, 48, 48]
|
||||
);
|
||||
setTimeout(() => {
|
||||
this.waybillTrackMapInstance?.resize?.();
|
||||
}, 80);
|
||||
} catch (error) {
|
||||
window.console.warn('历史轨迹地图渲染失败', error);
|
||||
this.$message.error('高德地图渲染失败,请稍后重试');
|
||||
}
|
||||
},
|
||||
displayStatus(row, prop) {
|
||||
const formatStatus = this.config.formatStatus;
|
||||
@@ -5152,11 +5522,23 @@ export default {
|
||||
return;
|
||||
}
|
||||
this.getSaveRequest()(submitRow).then(
|
||||
() => {
|
||||
this.onLoad(this.page);
|
||||
this.$message({ type: 'success', message: '操作成功!' });
|
||||
done();
|
||||
if (this.isStandaloneWaybillPage) this.closeCrudDialog();
|
||||
res => {
|
||||
const data = res.data?.data || {};
|
||||
const id = data.id || submitRow.id;
|
||||
const after = id
|
||||
? submitMkApprovalFlow({
|
||||
bizType: 'waybill-manage',
|
||||
formInstanceId: id,
|
||||
subjectName: data.waybillNo || submitRow.waybillNo || '',
|
||||
approvalStatus: data.approvalStatus || submitRow.approvalStatus || '',
|
||||
})
|
||||
: Promise.resolve();
|
||||
return after.then(() => {
|
||||
this.onLoad(this.page);
|
||||
this.$message({ type: 'success', message: '提交成功!' });
|
||||
done();
|
||||
if (this.isStandaloneWaybillPage) this.closeCrudDialog();
|
||||
});
|
||||
},
|
||||
error => {
|
||||
window.console.log(error);
|
||||
@@ -5175,12 +5557,26 @@ export default {
|
||||
? this.api.updateAttachments
|
||||
: this.getSaveRequest();
|
||||
request(submitRow).then(
|
||||
() => {
|
||||
this.attachmentUploadMode = false;
|
||||
this.onLoad(this.page);
|
||||
this.$message({ type: 'success', message: '操作成功!' });
|
||||
done();
|
||||
if (this.isStandaloneWaybillPage) this.closeCrudDialog();
|
||||
res => {
|
||||
const skipMk = this.attachmentUploadMode;
|
||||
const data = res.data?.data || {};
|
||||
const id = data.id || submitRow.id;
|
||||
const after =
|
||||
skipMk || !id
|
||||
? Promise.resolve()
|
||||
: submitMkApprovalFlow({
|
||||
bizType: 'waybill-manage',
|
||||
formInstanceId: id,
|
||||
subjectName: data.waybillNo || submitRow.waybillNo || '',
|
||||
approvalStatus: data.approvalStatus || submitRow.approvalStatus || '',
|
||||
});
|
||||
return after.then(() => {
|
||||
this.attachmentUploadMode = false;
|
||||
this.onLoad(this.page);
|
||||
this.$message({ type: 'success', message: skipMk ? '操作成功!' : '提交成功!' });
|
||||
done();
|
||||
if (this.isStandaloneWaybillPage) this.closeCrudDialog();
|
||||
});
|
||||
},
|
||||
error => {
|
||||
this.attachmentUploadMode = false;
|
||||
@@ -9553,6 +9949,81 @@ export default {
|
||||
border: 1px solid #eff1f7;
|
||||
}
|
||||
|
||||
&__waybill-locate {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
&__waybill-locate-meta {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px 16px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #eff1f7;
|
||||
background: #fafafa;
|
||||
color: #606266;
|
||||
font-size: 13px;
|
||||
|
||||
span {
|
||||
margin-right: 8px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: #303133;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
&__waybill-locate-address {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
&__waybill-locate-map {
|
||||
width: 100%;
|
||||
min-height: 320px;
|
||||
height: 320px;
|
||||
border: 1px solid #eff1f7;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
&__waybill-locate-map--track {
|
||||
min-height: 420px;
|
||||
height: 420px;
|
||||
}
|
||||
|
||||
&__waybill-map-tip {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
z-index: 2;
|
||||
transform: translate(-50%, -50%);
|
||||
padding: 8px 14px;
|
||||
border-radius: 4px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&__waybill-track-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
color: #606266;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
&__waybill-track-total {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
&__waybill-track-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
@@ -302,6 +302,7 @@
|
||||
|
||||
<script>
|
||||
import * as api from '@/api/business/contract-manage';
|
||||
import { submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||
import BillingPlanEditor from './components/billing-plan-editor.vue';
|
||||
import ContractAttachmentSection, {
|
||||
normalizeContractFileRows,
|
||||
@@ -629,7 +630,7 @@ export default {
|
||||
removeCustomPeriodRow(index) { if (index <= 0) return; const periods = [...(this.settlementRule.customPeriods || [])]; periods.splice(index, 1); this.settlementRule.customPeriods = normalizeCustomPeriods(periods); },
|
||||
validateCustomPeriods(periods = [], label) { const rows = normalizeCustomPeriods(periods); if (!rows.length) { this.$message.warning(`${label}:请至少配置一段自定义周期`); return false; } for (let index = 0; index < rows.length; index += 1) { const row = rows[index]; const startDay = Number(row.startDay); const endDay = Number(row.endDay); if (!Number.isFinite(startDay) || startDay < 1 || startDay > 31) { this.$message.warning(`${label}:请选择第${index + 1}行运单区间开始日`); return false; } if (!Number.isFinite(endDay) || endDay < 1 || endDay > 31) { this.$message.warning(`${label}:请选择第${index + 1}行运单区间结束日`); return false; } if (endDay < startDay) { this.$message.warning(`${label}:第${index + 1}行结束日不能早于开始日`); return false; } if (index > 0 && startDay !== Number(rows[index - 1].endDay) + 1) { this.$message.warning(`${label}:自定义多周期区间必须连续,不允许重叠或存在日期缺口`); return false; } } return true; },
|
||||
validateSettlementRule(rule, label) { if (Number(rule.autoGenerate) !== 1) return true; if (!rule.billStartDate || !rule.settlementType) { this.$message.warning(`${label}:请完整填写账单起始日期和结算类型`); return false; } if (rule.settlementType === '月结' && !rule.billCycleType) { this.$message.warning(`${label}:请选择结算周期`); return false; } if (rule.settlementType === '月结' && rule.billCycleType === '固定截单日' && !rule.billCutoffDay) { this.$message.warning(`${label}:请选择账单截单日`); return false; } if (rule.settlementType === '月结' && rule.billCycleType === '自定义多周期') return this.validateCustomPeriods(rule.customPeriods, label); if (rule.settlementType === '固定天数周期结算' && !rule.cycleDays) { this.$message.warning(`${label}:请选择周期天数`); return false; } return true; },
|
||||
async submit() { await this.$refs.formRef.validate(); const total = this.paymentRatioRows.reduce((sum, row) => sum + Number(row.ratioLimit || 0), 0); if (this.paymentRatioRows.length && Math.abs(total - 100) > 0.0001) { this.$message.warning('付款比例上限合计必须等于100%'); return; } if (this.settlementConfigTab === 'pre') this.preSettlementConfig = { ...this.settlementRule }; else this.formalSettlementConfig = { ...this.settlementRule }; if (!this.validateSettlementRule(this.preSettlementConfig, '预结算配置') || !this.validateSettlementRule(this.formalSettlementConfig, '正式结算配置')) return; const settlementRule = { preSettlementConfig: this.preSettlementConfig, formalSettlementConfig: this.formalSettlementConfig }; await api.submitChange({ ...this.form, settlementCurrency: String(this.form.settlementCurrency || '').trim() || 'RMB', copyCount: normalizeOptionalPositiveInteger(this.form.copyCount), invoiceCycle: normalizeOptionalPositiveInteger(this.form.invoiceCycle), paymentDays: normalizeOptionalPositiveInteger(this.form.paymentDays), contractAmount: normalizeOptionalAmount(this.form.contractAmount), startDate: this.period[0], endDate: this.period[1], feeGenerationMode: this.feeGenerationMode, billingEnabled: this.feeGenerationMode === 'system' ? 1 : 0, billingPlanJson: JSON.stringify(this.plans), settlementRuleJson: JSON.stringify(settlementRule), preSettlementConfigJson: JSON.stringify(this.preSettlementConfig), formalSettlementConfigJson: JSON.stringify(this.formalSettlementConfig), paymentRatioJson: JSON.stringify(this.paymentRatioRows), contractFileJson: JSON.stringify(this.contractFileRows), attachmentsJson: JSON.stringify(this.attachments), changeContent: this.form.changeContent, changeReason: this.form.changeReason, changeAttachmentsJson: JSON.stringify(this.changeMaterials) }); this.$message.success('变更已提交'); this.$router.back(); },
|
||||
async submit() { await this.$refs.formRef.validate(); const total = this.paymentRatioRows.reduce((sum, row) => sum + Number(row.ratioLimit || 0), 0); if (this.paymentRatioRows.length && Math.abs(total - 100) > 0.0001) { this.$message.warning('付款比例上限合计必须等于100%'); return; } if (this.settlementConfigTab === 'pre') this.preSettlementConfig = { ...this.settlementRule }; else this.formalSettlementConfig = { ...this.settlementRule }; if (!this.validateSettlementRule(this.preSettlementConfig, '预结算配置') || !this.validateSettlementRule(this.formalSettlementConfig, '正式结算配置')) return; const settlementRule = { preSettlementConfig: this.preSettlementConfig, formalSettlementConfig: this.formalSettlementConfig }; await api.submitChange({ ...this.form, settlementCurrency: String(this.form.settlementCurrency || '').trim() || 'RMB', copyCount: normalizeOptionalPositiveInteger(this.form.copyCount), invoiceCycle: normalizeOptionalPositiveInteger(this.form.invoiceCycle), paymentDays: normalizeOptionalPositiveInteger(this.form.paymentDays), contractAmount: normalizeOptionalAmount(this.form.contractAmount), startDate: this.period[0], endDate: this.period[1], feeGenerationMode: this.feeGenerationMode, billingEnabled: this.feeGenerationMode === 'system' ? 1 : 0, billingPlanJson: JSON.stringify(this.plans), settlementRuleJson: JSON.stringify(settlementRule), preSettlementConfigJson: JSON.stringify(this.preSettlementConfig), formalSettlementConfigJson: JSON.stringify(this.formalSettlementConfig), paymentRatioJson: JSON.stringify(this.paymentRatioRows), contractFileJson: JSON.stringify(this.contractFileRows), attachmentsJson: JSON.stringify(this.attachments), changeContent: this.form.changeContent, changeReason: this.form.changeReason, changeAttachmentsJson: JSON.stringify(this.changeMaterials) }); await submitMkApprovalFlow({ bizType: 'contract-manage', formInstanceId: this.form.id, subjectName: this.form.contractName || '', approvalStatus: this.form.approvalStatus || 'change_rejected' }); this.$message.success('变更已提交'); this.$router.back(); },
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1035,6 +1035,7 @@ import { getDictionary as getBizDictionary } from '@/api/system/dictbiz';
|
||||
import { config, option } from '@/option/business/contract-manage';
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { downloadFileByUrl, downloadXls } from '@/utils/util';
|
||||
import { submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||
import BillingPlanEditor from './components/billing-plan-editor.vue';
|
||||
import ContractAttachmentSection, {
|
||||
attachmentName as sharedAttachmentName,
|
||||
@@ -1824,9 +1825,17 @@ export default {
|
||||
const id = saved.id || this.form.id;
|
||||
if (action === 'temporary' || action === 'formal') {
|
||||
if (!id) throw new Error('合同保存成功但未返回主键,无法提交合同');
|
||||
return action === 'temporary'
|
||||
return (action === 'temporary'
|
||||
? this.api.toTemporary(id)
|
||||
: this.api.submitFormal(id);
|
||||
: this.api.submitFormal(id)
|
||||
).then(() =>
|
||||
submitMkApprovalFlow({
|
||||
bizType: 'contract-manage',
|
||||
formInstanceId: id,
|
||||
subjectName: saved.contractName || submit.contractName || '',
|
||||
approvalStatus: saved.approvalStatus || submit.approvalStatus || '',
|
||||
})
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})
|
||||
@@ -1853,8 +1862,20 @@ export default {
|
||||
: this.api.submit(payload);
|
||||
request
|
||||
.then(() => {
|
||||
this.$message.success(isChangeRejected ? '已重新提交变更审批' : '修改成功');
|
||||
this.closeForm();
|
||||
if (!isChangeRejected) {
|
||||
this.$message.success('修改成功');
|
||||
this.closeForm();
|
||||
return;
|
||||
}
|
||||
return submitMkApprovalFlow({
|
||||
bizType: 'contract-manage',
|
||||
formInstanceId: payload.id || this.form.id,
|
||||
subjectName: payload.contractName || this.form.contractName || '',
|
||||
approvalStatus: this.form.approvalStatus || 'change_rejected',
|
||||
}).then(() => {
|
||||
this.$message.success('已重新提交变更审批');
|
||||
this.closeForm();
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
this.submitAction = '';
|
||||
@@ -2698,7 +2719,19 @@ export default {
|
||||
}
|
||||
const run = value => {
|
||||
const args = operation.prompt ? [row.id, value] : [row.id];
|
||||
this.api[operation.action](...args).then(() => {
|
||||
const request = () => this.api[operation.action](...args);
|
||||
const afterMk =
|
||||
operation.action === 'submitFormal'
|
||||
? request().then(() =>
|
||||
submitMkApprovalFlow({
|
||||
bizType: 'contract-manage',
|
||||
formInstanceId: row.id,
|
||||
subjectName: row.contractName || '',
|
||||
approvalStatus: row.approvalStatus || '',
|
||||
})
|
||||
)
|
||||
: request();
|
||||
afterMk.then(() => {
|
||||
this.$message.success(operation.successMessage || `${operation.label}成功`);
|
||||
this.onLoad(this.page, this.query);
|
||||
});
|
||||
|
||||
@@ -140,15 +140,14 @@
|
||||
@load="onLoad(page, query)"
|
||||
/>
|
||||
|
||||
<!--
|
||||
项目新增/编辑/查看/变更/补录全部是独立整页(openProjectDialog 一律 push /business/project-apply/form),
|
||||
这里固定渲染普通容器,不再按 $route 在 div 与 el-dialog 之间切换。
|
||||
原因:标签页 keep-alive 会缓存本页实例,若容器在离开本页后又变回 el-dialog,
|
||||
缓存实例会把 append-to-body 的弹窗渲染进 body 并永久停留(Vue 的 KeepAlive.deactivate →
|
||||
Teleport.move 不会搬走 append-to-body 的弹窗 DOM),表现为“点左侧菜单又弹出新增项目管理弹窗”。
|
||||
-->
|
||||
<div v-if="isProjectFormPage" class="project-apply-page-form">
|
||||
<div class="archive-page-form__title">
|
||||
<component
|
||||
:is="projectFormContainer"
|
||||
v-if="projectBox"
|
||||
v-bind="projectFormContainerProps"
|
||||
@update:model-value="projectBox = $event"
|
||||
@closed="resetProjectDialog"
|
||||
>
|
||||
<div v-if="isProjectFormPage" class="archive-page-form__title">
|
||||
{{ projectDialogTitle }}
|
||||
</div>
|
||||
<el-form
|
||||
@@ -319,7 +318,7 @@
|
||||
<el-form-item prop="fundLimit" class="project-apply-form__tip-label">
|
||||
<template #label>
|
||||
<span>项目资金使用额度</span>
|
||||
<el-tooltip content="实际业务回款周期内付款额度" placement="top">
|
||||
<el-tooltip content="实际业务回款周期内付款" placement="top">
|
||||
<el-icon class="project-apply-form__label-tip"><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
@@ -789,7 +788,8 @@
|
||||
:class="{ 'project-apply-dialog__footer--change': isChangeDialog }"
|
||||
>
|
||||
<template v-if="isChangeDialog">
|
||||
<el-button @click="closeProjectForm">取消</el-button>
|
||||
<el-button v-if="!isProjectFormPage" @click="handleCancelProject">取消</el-button>
|
||||
<el-button v-if="isProjectFormPage" @click="closeProjectForm">取消</el-button>
|
||||
<el-button type="primary" plain :loading="submitLoading" @click="saveChangeProject">
|
||||
保存
|
||||
</el-button>
|
||||
@@ -798,7 +798,8 @@
|
||||
</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button @click="closeProjectForm">取消</el-button>
|
||||
<el-button v-if="!isProjectFormPage" @click="handleCancelProject">取消</el-button>
|
||||
<el-button v-if="isProjectFormPage" @click="closeProjectForm">取消</el-button>
|
||||
<el-button
|
||||
v-if="!dialogReadonly"
|
||||
type="primary"
|
||||
@@ -818,7 +819,7 @@
|
||||
</el-button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</component>
|
||||
|
||||
<el-dialog
|
||||
v-model="attachmentDocumentPreviewVisible"
|
||||
@@ -924,6 +925,7 @@ import { getDictionary as getBizDictionary } from '@/api/system/dictbiz';
|
||||
import { getList as getUserList } from '@/api/system/user';
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { downloadFileByUrl, downloadXls } from '@/utils/util';
|
||||
import { submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||
import { ElImageViewer } from 'element-plus';
|
||||
import { OpenFileViewer } from '@open-file-viewer/vue';
|
||||
import PdfPreview from '@/components/pdf-preview/main.vue';
|
||||
@@ -1115,6 +1117,11 @@ export default {
|
||||
total: 0,
|
||||
},
|
||||
selectionList: [],
|
||||
// 表单页实例标记:创建时按路由落定,不随 keep-alive 切走后的 $route 变化翻转。
|
||||
// 否则 isProjectFormPage 变 false 后容器会从 div 切成 el-dialog(append-to-body),
|
||||
// 盖住后续打开的页面(如客商档案)。
|
||||
isFormPageInstance: false,
|
||||
projectBox: false,
|
||||
dialogType: 'add',
|
||||
dialogReadonly: false,
|
||||
submitLoading: false,
|
||||
@@ -1306,7 +1313,23 @@ export default {
|
||||
return ['add', 'majorSupplement'].includes(this.dialogType);
|
||||
},
|
||||
isProjectFormPage() {
|
||||
return this.$route.path === '/business/project-apply/form';
|
||||
return this.isFormPageInstance;
|
||||
},
|
||||
projectFormContainer() {
|
||||
// 表单页实例始终用页面容器,避免 keep-alive 失活时误切弹窗
|
||||
return this.isFormPageInstance ? 'div' : 'el-dialog';
|
||||
},
|
||||
projectFormContainerProps() {
|
||||
if (this.isFormPageInstance) return { class: 'project-apply-page-form' };
|
||||
return {
|
||||
modelValue: this.projectBox,
|
||||
title: this.projectDialogTitle,
|
||||
appendToBody: true,
|
||||
destroyOnClose: true,
|
||||
width: '1440px',
|
||||
top: '4vh',
|
||||
class: 'project-apply-dialog',
|
||||
};
|
||||
},
|
||||
isBasicInfoReadonly() {
|
||||
return this.dialogReadonly || this.isChangeDialog;
|
||||
@@ -1342,10 +1365,13 @@ export default {
|
||||
this.loadCargoTypeOptions();
|
||||
this.loadTransportTypeOptions();
|
||||
this.loadSettlementModeOptions();
|
||||
if (this.isProjectFormPage) {
|
||||
if (this.$route.path === '/business/project-apply/form') {
|
||||
this.isFormPageInstance = true;
|
||||
this.openProjectFormPage();
|
||||
}
|
||||
},
|
||||
// 标签切走(keep-alive 缓存)或销毁时,收起 append-to-body 的内层弹窗,
|
||||
// 避免 Teleport 出去的 DOM 继续盖住后续页面。
|
||||
deactivated() {
|
||||
this.closeInnerDialogs();
|
||||
},
|
||||
@@ -1353,6 +1379,12 @@ export default {
|
||||
this.closeInnerDialogs();
|
||||
},
|
||||
methods: {
|
||||
closeInnerDialogs() {
|
||||
this.changeRecordDetailVisible = false;
|
||||
this.attachmentDocumentPreviewVisible = false;
|
||||
this.attachmentImagePreviewVisible = false;
|
||||
this.userBox = false;
|
||||
},
|
||||
buildTableOption() {
|
||||
return {
|
||||
...option,
|
||||
@@ -1379,6 +1411,7 @@ export default {
|
||||
const id = this.$route.query.id;
|
||||
this.dialogType = type;
|
||||
this.dialogReadonly = type === 'view';
|
||||
this.projectBox = true;
|
||||
if (['add', 'majorSupplement'].includes(type)) {
|
||||
this.applyProjectDetail({
|
||||
...emptyForm(),
|
||||
@@ -1399,15 +1432,11 @@ export default {
|
||||
});
|
||||
},
|
||||
closeProjectForm() {
|
||||
this.$router.push('/business/project-apply');
|
||||
},
|
||||
// 页面被 keep-alive 缓存/卸载时,关闭本页所有 append-to-body 的二级弹窗。
|
||||
// 若不关闭,Teleport 出去的弹窗 DOM 在实例失活时不会被搬离 body,会残留盖在后续页面上。
|
||||
closeInnerDialogs() {
|
||||
this.changeRecordDetailVisible = false;
|
||||
this.attachmentDocumentPreviewVisible = false;
|
||||
this.attachmentImagePreviewVisible = false;
|
||||
this.userBox = false;
|
||||
if (this.isProjectFormPage) {
|
||||
this.$router.push('/business/project-apply');
|
||||
} else {
|
||||
this.projectBox = false;
|
||||
}
|
||||
},
|
||||
statusValue(row) {
|
||||
return row[this.config.statusProp || 'status'];
|
||||
@@ -1561,7 +1590,17 @@ export default {
|
||||
}
|
||||
const run = value => {
|
||||
const args = operation.prompt ? [row.id, value] : [row.id];
|
||||
this.api[operation.action](...args).then(res => {
|
||||
const request = () => this.api[operation.action](...args);
|
||||
const afterMk =
|
||||
operation.action === 'submitApproval'
|
||||
? submitMkApprovalFlow({
|
||||
bizType: 'project-apply',
|
||||
formInstanceId: row.id,
|
||||
subjectName: row.projectName || '',
|
||||
approvalStatus: row.approvalStatus || '',
|
||||
}).then(() => request())
|
||||
: request();
|
||||
afterMk.then(res => {
|
||||
if (res.data?.success === false || res.data?.data === false) {
|
||||
this.$message.error(res.data?.msg || `${operation.label}失败,请联系管理员`);
|
||||
return;
|
||||
@@ -1626,6 +1665,17 @@ export default {
|
||||
else if (type === 'change') query.name = '项目变更';
|
||||
this.$router.push({ path: '/business/project-apply/form', query });
|
||||
},
|
||||
resetProjectDialog() {
|
||||
this.$refs.projectForm?.clearValidate();
|
||||
this.form = emptyForm();
|
||||
this.customerRows = [];
|
||||
this.carrierRows = [];
|
||||
this.attachmentRows = [];
|
||||
this.selectedAttachmentRows = [];
|
||||
this.changeRows = [];
|
||||
this.selectedCustomerId = [];
|
||||
this.selectedCarrierIds = [];
|
||||
},
|
||||
applyProjectDetail(row) {
|
||||
const displayRow = {
|
||||
...row,
|
||||
@@ -1718,9 +1768,26 @@ export default {
|
||||
this.$message.error(res.data?.msg || '保存失败,请联系管理员');
|
||||
return;
|
||||
}
|
||||
this.$message.success('操作成功!');
|
||||
this.closeProjectForm();
|
||||
this.onLoad(this.page, this.query);
|
||||
const data = res.data?.data || {};
|
||||
const id = data.id || this.form.id;
|
||||
const name = data.projectName || this.form.projectName || '';
|
||||
const status = data.approvalStatus || this.form.approvalStatus || '';
|
||||
if (!id) {
|
||||
this.$message.success('操作成功!');
|
||||
this.closeProjectForm();
|
||||
this.onLoad(this.page, this.query);
|
||||
return;
|
||||
}
|
||||
return submitMkApprovalFlow({
|
||||
bizType: 'project-apply',
|
||||
formInstanceId: id,
|
||||
subjectName: name,
|
||||
approvalStatus: status,
|
||||
}).then(() => {
|
||||
this.$message.success('提交成功!');
|
||||
this.closeProjectForm();
|
||||
this.onLoad(this.page, this.query);
|
||||
});
|
||||
},
|
||||
error => {
|
||||
window.console.log(error);
|
||||
@@ -1748,6 +1815,23 @@ export default {
|
||||
this.submitLoading = false;
|
||||
});
|
||||
},
|
||||
handleCancelProject() {
|
||||
if (this.isProjectFormPage) {
|
||||
this.closeProjectForm();
|
||||
return;
|
||||
}
|
||||
if (!['add', 'majorSupplement', 'change'].includes(this.dialogType)) {
|
||||
this.closeProjectForm();
|
||||
return;
|
||||
}
|
||||
this.$confirm('确认后直接关闭弹窗,列表无数据变更,是否继续?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
this.closeProjectForm();
|
||||
});
|
||||
},
|
||||
saveChangeProject() {
|
||||
this.submitChangeForm(false);
|
||||
},
|
||||
@@ -1771,9 +1855,23 @@ export default {
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.$message.success(`${needSubmit ? '提交' : '保存'}成功!`);
|
||||
this.closeProjectForm();
|
||||
this.onLoad(this.page, this.query);
|
||||
if (!needSubmit) {
|
||||
this.$message.success('保存成功!');
|
||||
this.closeProjectForm();
|
||||
this.onLoad(this.page, this.query);
|
||||
return;
|
||||
}
|
||||
const id = this.form.id;
|
||||
return submitMkApprovalFlow({
|
||||
bizType: 'project-apply',
|
||||
formInstanceId: id,
|
||||
subjectName: this.form.projectName || '',
|
||||
approvalStatus: this.form.approvalStatus || 'change_rejected',
|
||||
}).then(() => {
|
||||
this.$message.success('提交成功!');
|
||||
this.closeProjectForm();
|
||||
this.onLoad(this.page, this.query);
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
this.submitLoading = false;
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
{{ planData.businessStatusName || '-' }}
|
||||
</span>
|
||||
<span class="detail-status-text detail-transport-type">
|
||||
{{ planData.transportTypeName || '公路整车' }}
|
||||
{{ planData.transportTypeName || '公路运输' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="transport-plan-dispatch-page__summary-grid">
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
<el-button type="primary">添加附件</el-button>
|
||||
</el-upload>
|
||||
<span class="transport-plan-import-page__file-tip">
|
||||
请上传计划明细表,仅支持 Excel 格式
|
||||
请上传计划明细表,仅支持 Excel 格式;日期支持 2026-08-02、2026-8-2、2026/8/2 等写法
|
||||
</span>
|
||||
<el-link type="primary" @click="handleTemplateDownload">下载模板</el-link>
|
||||
</el-form-item>
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
<template>
|
||||
<div ref="page" class="mk-public-biz-view">
|
||||
<basic-container>
|
||||
<div class="mk-public-biz-view__title">{{ pageTitle }}</div>
|
||||
<el-descriptions v-if="detail" :column="2" border>
|
||||
<el-descriptions-item
|
||||
v-for="item in visibleFields"
|
||||
:key="item.prop"
|
||||
:label="item.label"
|
||||
:span="item.span || 1"
|
||||
>
|
||||
{{ displayValue(detail[item.prop]) }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-empty v-else description="暂无数据" />
|
||||
</basic-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getMkPublicDetail, postMkPublicProcessMessage } from '@/api/mk-process';
|
||||
|
||||
const FIELD_MAP = {
|
||||
'project-apply': [
|
||||
{ label: '申请单号', prop: 'applyNo' },
|
||||
{ label: '项目编号', prop: 'projectCode' },
|
||||
{ label: '项目名称', prop: 'projectName', span: 2 },
|
||||
{ label: '审核状态', prop: 'approvalStatusName' },
|
||||
{ label: '当前节点', prop: 'currentNode' },
|
||||
{ label: '当前处理人', prop: 'currentProcessor' },
|
||||
{ label: '所属组织', prop: 'deptName' },
|
||||
{ label: '备注', prop: 'remark', span: 2 },
|
||||
],
|
||||
'contract-manage': [
|
||||
{ label: '合同编号', prop: 'contractNo' },
|
||||
{ label: '合同名称', prop: 'contractName', span: 2 },
|
||||
{ label: '合同类别', prop: 'contractCategory' },
|
||||
{ label: '审核状态', prop: 'approvalStatusName' },
|
||||
{ label: '当前节点', prop: 'currentNode' },
|
||||
{ label: '当前处理人', prop: 'currentProcessor' },
|
||||
{ label: '甲方', prop: 'partyA' },
|
||||
{ label: '乙方', prop: 'partyB' },
|
||||
{ label: '项目名称', prop: 'projectName', span: 2 },
|
||||
],
|
||||
'waybill-manage': [
|
||||
{ label: '运单号', prop: 'waybillNo' },
|
||||
{ label: '项目', prop: 'projectName' },
|
||||
{ label: '客户合同', prop: 'contractName' },
|
||||
{ label: '客户名称', prop: 'customerName' },
|
||||
{ label: '货物名称', prop: 'cargoName' },
|
||||
{ label: '承运商', prop: 'carrierName' },
|
||||
{ label: '当前过程节点', prop: 'currentProcessNode' },
|
||||
{ label: '发货地', prop: 'departureName' },
|
||||
{ label: '收货地', prop: 'arrivalName' },
|
||||
],
|
||||
'pre-settlement': [
|
||||
{ label: '预结算单号', prop: 'preSettlementNo' },
|
||||
{ label: '合同名称', prop: 'contractName' },
|
||||
{ label: '项目名称', prop: 'projectName' },
|
||||
{ label: '结算金额', prop: 'settlementAmount' },
|
||||
{ label: '审核状态', prop: 'approvalStatusName' },
|
||||
{ label: '当前节点', prop: 'currentNode' },
|
||||
{ label: '当前处理人', prop: 'currentProcessor' },
|
||||
{ label: '备注', prop: 'remark', span: 2 },
|
||||
],
|
||||
'formal-settlement': [
|
||||
{ label: '正式结算单号', prop: 'formalSettlementNo' },
|
||||
{ label: '预结算单号', prop: 'preSettlementNos' },
|
||||
{ label: '项目名称', prop: 'projectName' },
|
||||
{ label: '合同名称', prop: 'contractName' },
|
||||
{ label: '结算金额', prop: 'settlementAmount' },
|
||||
{ label: '审核状态', prop: 'approvalStatusName' },
|
||||
{ label: '当前节点', prop: 'currentNode' },
|
||||
{ label: '当前处理人', prop: 'currentProcessor' },
|
||||
],
|
||||
'payment-application': [
|
||||
{ label: '付款申请号', prop: 'paymentNo' },
|
||||
{ label: '付款类型', prop: 'paymentTypeName' },
|
||||
{ label: '收款方', prop: 'payeeName' },
|
||||
{ label: '项目名称', prop: 'projectName' },
|
||||
{ label: '申请金额', prop: 'applyAmount' },
|
||||
{ label: '审核状态', prop: 'approvalStatusName' },
|
||||
{ label: '当前节点', prop: 'currentNode' },
|
||||
{ label: '当前处理人', prop: 'currentProcessor' },
|
||||
],
|
||||
};
|
||||
|
||||
const TITLE_MAP = {
|
||||
'project-apply': '查看项目信息',
|
||||
'contract-manage': '查看合同信息',
|
||||
'waybill-manage': '查看运单信息',
|
||||
'pre-settlement': '查看预结算信息',
|
||||
'formal-settlement': '查看正式结算信息',
|
||||
'payment-application': '查看付款申请信息',
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'MkPublicBizView',
|
||||
data() {
|
||||
return {
|
||||
detail: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
bizType() {
|
||||
return this.$route.meta.bizType || this.$route.query.bizType || '';
|
||||
},
|
||||
pageTitle() {
|
||||
return TITLE_MAP[this.bizType] || '查看详情';
|
||||
},
|
||||
visibleFields() {
|
||||
return FIELD_MAP[this.bizType] || [];
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.handleIframeHeight = () => this.sendIframeHeight();
|
||||
this.handleProcessMessage = event => this.onProcessMessage(event);
|
||||
document.addEventListener('DOMContentLoaded', this.handleIframeHeight, false);
|
||||
window.addEventListener('message', this.handleProcessMessage);
|
||||
},
|
||||
mounted() {
|
||||
document.documentElement.classList.add('mk-iframe-page');
|
||||
document.body.classList.add('mk-iframe-page');
|
||||
const app = document.getElementById('app');
|
||||
if (app) app.classList.add('mk-iframe-page');
|
||||
this.loadDetail();
|
||||
this.handleIframeHeight();
|
||||
window.addEventListener('load', this.handleIframeHeight);
|
||||
this.mkHeightTimers = [300, 800, 1600].map(delay => setTimeout(this.handleIframeHeight, delay));
|
||||
if (typeof ResizeObserver === 'undefined') return;
|
||||
this.mkHeightObserver = new ResizeObserver(() => this.handleIframeHeight());
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.page) this.mkHeightObserver.observe(this.$refs.page);
|
||||
this.mkHeightObserver.observe(document.body);
|
||||
});
|
||||
},
|
||||
beforeUnmount() {
|
||||
document.removeEventListener('DOMContentLoaded', this.handleIframeHeight, false);
|
||||
window.removeEventListener('load', this.handleIframeHeight);
|
||||
window.removeEventListener('message', this.handleProcessMessage);
|
||||
(this.mkHeightTimers || []).forEach(timer => clearTimeout(timer));
|
||||
if (this.mkHeightObserver) {
|
||||
this.mkHeightObserver.disconnect();
|
||||
this.mkHeightObserver = null;
|
||||
}
|
||||
document.documentElement.classList.remove('mk-iframe-page');
|
||||
document.body.classList.remove('mk-iframe-page');
|
||||
const app = document.getElementById('app');
|
||||
if (app) app.classList.remove('mk-iframe-page');
|
||||
},
|
||||
methods: {
|
||||
loadDetail() {
|
||||
const id = this.getFormId();
|
||||
if (!id || !this.bizType) return;
|
||||
getMkPublicDetail(this.bizType, id).then(res => {
|
||||
this.detail = res.data?.data || null;
|
||||
this.handleIframeHeight();
|
||||
});
|
||||
},
|
||||
sendIframeHeight() {
|
||||
this.$nextTick(() => {
|
||||
window.parent.postMessage({ height: document.body.clientHeight }, '*');
|
||||
});
|
||||
},
|
||||
onProcessMessage(event) {
|
||||
const data = event && event.data;
|
||||
if (!data || typeof data !== 'object') return;
|
||||
if (data.height && !data.status && !data.type) return;
|
||||
if (data.type === 'formValues' || data.type === 'afterSubmit') return;
|
||||
const formValues = data.formValues;
|
||||
if (data.status === 'submit') {
|
||||
this.submitData(formValues);
|
||||
} else if (data.status === 'save') {
|
||||
this.saveData(formValues);
|
||||
}
|
||||
if (data.type === 'getFormValues') {
|
||||
window.parent.postMessage({ type: 'formValues', formData: this.buildFormData() }, '*');
|
||||
}
|
||||
},
|
||||
submitData(lbpmFormValues) {
|
||||
const formData = this.buildFormData();
|
||||
this.postFormData('submit', lbpmFormValues, formData);
|
||||
if (!lbpmFormValues) return;
|
||||
const parameters = Object.assign({}, lbpmFormValues, {
|
||||
loginName: this.getLoginName(lbpmFormValues),
|
||||
formInstanceId: this.getFormId(),
|
||||
subject: formData.subject,
|
||||
});
|
||||
window.parent.postMessage({ type: 'afterSubmit', success: true, parameters }, '*');
|
||||
},
|
||||
saveData(lbpmFormValues) {
|
||||
this.postFormData('save', lbpmFormValues, this.buildFormData());
|
||||
},
|
||||
postFormData(status, formValues, formData) {
|
||||
const payload = { status, formValues: formValues || {}, formData };
|
||||
postMkPublicProcessMessage(this.bizType, payload).catch(error => {
|
||||
console.error('公开页提交流程数据失败:', error);
|
||||
});
|
||||
},
|
||||
buildFormData() {
|
||||
const detail = this.detail || {};
|
||||
return {
|
||||
...detail,
|
||||
id: this.getFormId() || detail.id,
|
||||
subject: this.pageTitle,
|
||||
formInstanceId: this.getFormId(),
|
||||
};
|
||||
},
|
||||
getFormId() {
|
||||
return this.$route.query.id || this.detail?.id || '';
|
||||
},
|
||||
getLoginName(formValues = {}) {
|
||||
return (
|
||||
formValues.loginName ||
|
||||
this.$route.query.loginName ||
|
||||
this.$route.query.submitIdentity ||
|
||||
''
|
||||
);
|
||||
},
|
||||
displayValue(value) {
|
||||
if (value === null || value === undefined || value === '') return '-';
|
||||
if (Array.isArray(value)) return value.join('、') || '-';
|
||||
if (typeof value === 'object') return JSON.stringify(value);
|
||||
return value;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
html.mk-iframe-page,
|
||||
html.mk-iframe-page body,
|
||||
html.mk-iframe-page #app,
|
||||
html.mk-iframe-page #app.mk-iframe-page {
|
||||
height: auto !important;
|
||||
min-height: 100%;
|
||||
overflow: visible;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mk-public-biz-view {
|
||||
min-height: 100%;
|
||||
padding: 12px 0 24px;
|
||||
box-sizing: border-box;
|
||||
background: #f0f2f5;
|
||||
|
||||
:deep(.basic-container) {
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
position: relative;
|
||||
padding-left: 12px;
|
||||
margin: 8px 0 16px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 22px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 2px;
|
||||
width: 4px;
|
||||
height: 18px;
|
||||
background: #409eff;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -608,6 +608,7 @@ import {
|
||||
} from '@/api/vehicle/customer-archive';
|
||||
import { readSettlementTransfer, removeSettlementTransfer } from '@/utils/settlement-transfer';
|
||||
import { downloadFileByUrl } from '@/utils/util';
|
||||
import { submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||
import PdfPreview from '@/components/pdf-preview/main.vue';
|
||||
|
||||
const attachmentViewerPlugins = [
|
||||
@@ -2354,6 +2355,12 @@ export default {
|
||||
const saved = await this.saveDraft(true);
|
||||
if (!saved) return;
|
||||
await api.submit({ id: this.form.id });
|
||||
await submitMkApprovalFlow({
|
||||
bizType: 'payment-application',
|
||||
formInstanceId: this.form.id,
|
||||
subjectName: this.form.paymentNo || '',
|
||||
approvalStatus: this.form.approvalStatus || '',
|
||||
});
|
||||
this.$message.success('提交成功');
|
||||
this.goBack();
|
||||
},
|
||||
|
||||
@@ -233,6 +233,7 @@ import { mapGetters } from 'vuex';
|
||||
import * as api from '@/api/payment/paymentApplication';
|
||||
import { paymentApplicationTableColumns } from '@/option/payment/paymentApplication';
|
||||
import organizationSearch from '@/mixins/organization-search';
|
||||
import { submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||
|
||||
const emptyQuery = () => ({
|
||||
paymentNo: '',
|
||||
@@ -361,6 +362,12 @@ export default {
|
||||
},
|
||||
async handleSubmit(row) {
|
||||
await api.submit({ id: row.id });
|
||||
await submitMkApprovalFlow({
|
||||
bizType: 'payment-application',
|
||||
formInstanceId: row.id,
|
||||
subjectName: row.paymentNo || '',
|
||||
approvalStatus: row.approvalStatus || '',
|
||||
});
|
||||
this.$message.success('提交成功');
|
||||
this.loadTable();
|
||||
},
|
||||
|
||||
@@ -955,6 +955,7 @@ import {
|
||||
settlementSummaryColumns,
|
||||
} from '@/option/settlement/preSettlementTable';
|
||||
import { downloadFileByUrl } from '@/utils/util';
|
||||
import { submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||
import * as XLSX from 'xlsx';
|
||||
|
||||
export default {
|
||||
@@ -1553,6 +1554,12 @@ export default {
|
||||
await this.persistPendingAdjustments();
|
||||
if (shouldSubmit) {
|
||||
await submit({ id: this.form.id });
|
||||
await submitMkApprovalFlow({
|
||||
bizType: 'pre-settlement',
|
||||
formInstanceId: this.form.id,
|
||||
subjectName: this.form.preSettlementNo || this.form.contractName || '',
|
||||
approvalStatus: this.form.approvalStatus || '',
|
||||
});
|
||||
this.$message.success('审批流程已发起');
|
||||
this.visible = false;
|
||||
this.$emit('success', this.form.id);
|
||||
|
||||
@@ -182,6 +182,7 @@ import organizationSearch from '@/mixins/organization-search';
|
||||
import { formalSettlementTableColumns } from '@/option/settlement/formalSettlementTable';
|
||||
import FormalSettlementEditor from './components/formal-settlement-editor.vue';
|
||||
import FormalSettlementTablePanel from './components/formal-settlement-table-panel.vue';
|
||||
import { submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||
|
||||
const emptyQuery = () => ({
|
||||
formalSettlementNo: '',
|
||||
@@ -337,6 +338,12 @@ export default {
|
||||
async handleSubmit(row) {
|
||||
await this.$confirm('提交后来源预结算将保持锁定,确认提交?', '提示', { type: 'warning' });
|
||||
await api.submit({ id: row.id });
|
||||
await submitMkApprovalFlow({
|
||||
bizType: 'formal-settlement',
|
||||
formInstanceId: row.id,
|
||||
subjectName: row.formalSettlementNo || '',
|
||||
approvalStatus: row.approvalStatus || '',
|
||||
});
|
||||
this.$message.success('提交成功');
|
||||
this.loadTable();
|
||||
},
|
||||
|
||||
+272
-30
@@ -22,14 +22,6 @@
|
||||
@tree-load="treeLoad"
|
||||
>
|
||||
<template #menu-left>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
:loading="iamSyncLoading"
|
||||
v-if="userInfo.authority.includes('admin')"
|
||||
@click="handleIamOrganizationSync"
|
||||
>同步组织
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
icon="el-icon-delete"
|
||||
@@ -38,6 +30,14 @@
|
||||
@click="handleDelete"
|
||||
>删除
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
:loading="oaSyncLoading"
|
||||
v-if="userInfo.authority.includes('admin')"
|
||||
@click="handleOaOrgSync"
|
||||
>自动同步组织
|
||||
</el-button>
|
||||
</template>
|
||||
<template #menu="scope">
|
||||
<el-link
|
||||
@@ -124,6 +124,46 @@
|
||||
/>
|
||||
</div>
|
||||
</el-dialog>
|
||||
<el-dialog
|
||||
v-model="oaSyncVisible"
|
||||
width="480px"
|
||||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="!oaSyncRunning"
|
||||
:show-close="!oaSyncRunning"
|
||||
class="oa-sync-dialog"
|
||||
@close="closeOaSyncDialog"
|
||||
>
|
||||
<template #header>
|
||||
<span class="dialog-title">{{ oaSyncDialogTitle }}</span>
|
||||
</template>
|
||||
<div class="oa-sync-body">
|
||||
<el-progress
|
||||
:percentage="oaSyncPercent"
|
||||
:status="oaSyncProgressStatus"
|
||||
:stroke-width="12"
|
||||
/>
|
||||
<div class="oa-sync-meta">
|
||||
当前阶段:{{ oaSyncStageLabel }},第 {{ oaSyncProgress.current || 0 }} / {{ oaSyncTotalPage }} 页
|
||||
</div>
|
||||
<div class="oa-sync-stats">
|
||||
<div class="oa-sync-stat">
|
||||
<span class="oa-sync-stat__label">同步成功</span>
|
||||
<span class="oa-sync-stat__value oa-sync-stat__value--success">{{ oaSyncProgress.synced }}</span>
|
||||
</div>
|
||||
<div class="oa-sync-stat">
|
||||
<span class="oa-sync-stat__label">跳过</span>
|
||||
<span class="oa-sync-stat__value oa-sync-stat__value--skip">{{ oaSyncProgress.skipped }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button v-if="oaSyncRunning" @click="cancelOaSync">取消</el-button>
|
||||
<el-button v-else type="primary" @click="closeOaSyncDialog">关闭</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</basic-container>
|
||||
</template>
|
||||
|
||||
@@ -135,7 +175,8 @@ import {
|
||||
add,
|
||||
getDept,
|
||||
getDeptTree,
|
||||
syncIamOrganizations,
|
||||
syncOaCompany,
|
||||
syncOaDepartment,
|
||||
} from '@/api/system/dept';
|
||||
import { getLeaderList } from '@/api/system/user';
|
||||
import { getList as getCustomerArchiveList } from '@/api/vehicle/customer-archive';
|
||||
@@ -168,7 +209,20 @@ export default {
|
||||
selectionList: [],
|
||||
query: {},
|
||||
loading: true,
|
||||
iamSyncLoading: false,
|
||||
oaSyncLoading: false,
|
||||
oaSyncVisible: false,
|
||||
oaSyncRunning: false,
|
||||
oaSyncCancelled: false,
|
||||
oaSyncStatus: 'running',
|
||||
oaSyncStage: 'company',
|
||||
oaSyncController: null,
|
||||
oaSyncProgress: {
|
||||
current: 0,
|
||||
size: 20,
|
||||
total: 0,
|
||||
synced: 0,
|
||||
skipped: 0,
|
||||
},
|
||||
parentId: 0,
|
||||
page: {
|
||||
pageSize: 10,
|
||||
@@ -453,8 +507,153 @@ export default {
|
||||
});
|
||||
return ids.join(',');
|
||||
},
|
||||
oaSyncDialogTitle() {
|
||||
if (this.oaSyncStatus === 'done') {
|
||||
return '同步完成';
|
||||
}
|
||||
if (this.oaSyncStatus === 'cancelled') {
|
||||
return '已取消同步';
|
||||
}
|
||||
if (this.oaSyncStatus === 'error') {
|
||||
return '同步失败';
|
||||
}
|
||||
return '自动同步组织';
|
||||
},
|
||||
oaSyncStageLabel() {
|
||||
return this.oaSyncStage === 'department' ? '同步部门' : '同步公司';
|
||||
},
|
||||
oaSyncTotalPage() {
|
||||
const total = Number(this.oaSyncProgress.total) || 0;
|
||||
const size = Number(this.oaSyncProgress.size) || 20;
|
||||
if (total <= 0) {
|
||||
return this.oaSyncProgress.current || 0;
|
||||
}
|
||||
return Math.max(1, Math.ceil(total / size));
|
||||
},
|
||||
oaSyncPercent() {
|
||||
if (this.oaSyncStatus === 'done') {
|
||||
return 100;
|
||||
}
|
||||
const totalPage = this.oaSyncTotalPage;
|
||||
const current = Number(this.oaSyncProgress.current) || 0;
|
||||
const stageBase = this.oaSyncStage === 'department' ? 50 : 0;
|
||||
if (totalPage <= 0) {
|
||||
return stageBase;
|
||||
}
|
||||
const stagePercent = Math.min(50, Math.round((current / totalPage) * 50));
|
||||
return Math.min(99, stageBase + stagePercent);
|
||||
},
|
||||
oaSyncProgressStatus() {
|
||||
if (this.oaSyncStatus === 'done') {
|
||||
return 'success';
|
||||
}
|
||||
if (this.oaSyncStatus === 'error') {
|
||||
return 'exception';
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleOaOrgSync() {
|
||||
this.$confirm('确定从OA先同步公司、再同步部门?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
this.startOaOrgSync();
|
||||
});
|
||||
},
|
||||
startOaOrgSync() {
|
||||
this.oaSyncLoading = true;
|
||||
this.oaSyncVisible = true;
|
||||
this.oaSyncRunning = true;
|
||||
this.oaSyncCancelled = false;
|
||||
this.oaSyncStatus = 'running';
|
||||
this.oaSyncStage = 'company';
|
||||
this.oaSyncController = new AbortController();
|
||||
this.oaSyncProgress = {
|
||||
current: 0,
|
||||
size: 20,
|
||||
total: 0,
|
||||
synced: 0,
|
||||
skipped: 0,
|
||||
};
|
||||
this.runOaOrgSyncPages();
|
||||
},
|
||||
isOaSyncCanceledError(error) {
|
||||
return (
|
||||
this.oaSyncCancelled ||
|
||||
error?.code === 'ERR_CANCELED' ||
|
||||
error?.name === 'CanceledError' ||
|
||||
error?.name === 'AbortError'
|
||||
);
|
||||
},
|
||||
async runOaOrgSyncStage(syncApi) {
|
||||
const size = 20;
|
||||
let current = 1;
|
||||
while (!this.oaSyncCancelled) {
|
||||
const res = await syncApi(current, size, this.oaSyncController?.signal);
|
||||
const page = res?.data?.data || {};
|
||||
this.oaSyncProgress.current = page.current || current;
|
||||
this.oaSyncProgress.size = page.size || size;
|
||||
this.oaSyncProgress.total = page.total || 0;
|
||||
this.oaSyncProgress.synced += page.syncedCount || 0;
|
||||
this.oaSyncProgress.skipped += page.skippedCount || 0;
|
||||
if (page.finished) {
|
||||
break;
|
||||
}
|
||||
current += 1;
|
||||
}
|
||||
},
|
||||
async runOaOrgSyncPages() {
|
||||
try {
|
||||
this.oaSyncStage = 'company';
|
||||
await this.runOaOrgSyncStage(syncOaCompany);
|
||||
if (this.oaSyncCancelled) {
|
||||
this.oaSyncStatus = 'cancelled';
|
||||
return;
|
||||
}
|
||||
this.oaSyncStage = 'department';
|
||||
this.oaSyncProgress.current = 0;
|
||||
this.oaSyncProgress.total = 0;
|
||||
await this.runOaOrgSyncStage(syncOaDepartment);
|
||||
if (this.oaSyncCancelled) {
|
||||
this.oaSyncStatus = 'cancelled';
|
||||
return;
|
||||
}
|
||||
this.oaSyncStatus = 'done';
|
||||
} catch (error) {
|
||||
if (this.isOaSyncCanceledError(error)) {
|
||||
this.oaSyncStatus = 'cancelled';
|
||||
} else {
|
||||
this.oaSyncStatus = 'error';
|
||||
this.$message.error(error?.message || 'OA组织同步失败');
|
||||
}
|
||||
} finally {
|
||||
this.oaSyncRunning = false;
|
||||
this.oaSyncLoading = false;
|
||||
if (this.oaSyncStatus === 'done' || this.oaSyncStatus === 'cancelled') {
|
||||
this.parentId = 0;
|
||||
this.data = [];
|
||||
this.$refs.crud?.refreshTable?.();
|
||||
this.onLoad(this.page, this.query);
|
||||
}
|
||||
}
|
||||
},
|
||||
cancelOaSync() {
|
||||
if (!this.oaSyncRunning) {
|
||||
return;
|
||||
}
|
||||
this.oaSyncCancelled = true;
|
||||
this.oaSyncController?.abort();
|
||||
},
|
||||
closeOaSyncDialog() {
|
||||
if (this.oaSyncRunning) {
|
||||
this.cancelOaSync();
|
||||
return;
|
||||
}
|
||||
this.oaSyncVisible = false;
|
||||
},
|
||||
initData(tenantId) {
|
||||
getDeptTree(tenantId).then(res => {
|
||||
const column = this.findColumn(this.option.column, 'parentId');
|
||||
@@ -708,26 +907,6 @@ export default {
|
||||
done(row);
|
||||
});
|
||||
},
|
||||
handleIamOrganizationSync() {
|
||||
this.$confirm('确定从IAM同步组织信息?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
.then(() => {
|
||||
this.iamSyncLoading = true;
|
||||
return syncIamOrganizations();
|
||||
})
|
||||
.then(res => {
|
||||
const count = res?.data?.data ?? 0;
|
||||
this.$message.success(`IAM组织同步完成,共处理${count}条`);
|
||||
this.parentId = 0;
|
||||
this.onLoad(this.page, this.query);
|
||||
})
|
||||
.finally(() => {
|
||||
this.iamSyncLoading = false;
|
||||
});
|
||||
},
|
||||
handleDelete() {
|
||||
if (this.selectionList.length === 0) {
|
||||
this.$message.warning('请选择至少一条数据');
|
||||
@@ -895,3 +1074,66 @@ export default {
|
||||
color: var(--el-color-danger-light-3);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.oa-sync-dialog .dialog-title {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.oa-sync-dialog .dialog-title::before {
|
||||
width: 4px;
|
||||
height: 18px;
|
||||
margin-right: 8px;
|
||||
background: #409eff;
|
||||
content: '';
|
||||
}
|
||||
|
||||
.oa-sync-body {
|
||||
padding: 8px 4px 0;
|
||||
}
|
||||
|
||||
.oa-sync-meta {
|
||||
margin-top: 12px;
|
||||
color: #606266;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.oa-sync-stats {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.oa-sync-stat {
|
||||
flex: 1;
|
||||
padding: 12px 16px;
|
||||
background: #fafafa;
|
||||
border: 1px solid #eff1f7;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.oa-sync-stat__label {
|
||||
display: block;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.oa-sync-stat__value {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.oa-sync-stat__value--success {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.oa-sync-stat__value--skip {
|
||||
color: #909399;
|
||||
}
|
||||
</style>
|
||||
|
||||
+236
-21
@@ -198,6 +198,17 @@
|
||||
<el-form ref="userFormRef" :model="form" :rules="userRules" label-position="right" label-width="calc(6em + 24px)">
|
||||
<el-row :gutter="18">
|
||||
<el-col :span="6"><el-form-item label="账号名" prop="account"><el-input v-model="form.account" /></el-form-item></el-col>
|
||||
<el-col v-if="userDialogMode === 'add'" :span="6">
|
||||
<el-form-item label="登录密码" prop="password">
|
||||
<el-input
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
show-password
|
||||
autocomplete="new-password"
|
||||
placeholder="请输入登录密码"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6"><el-form-item label="姓名" prop="realName"><el-input v-model="form.realName" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="手机号" prop="phone"><el-input v-model="form.phone" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="邮箱"><el-input v-model="form.email" /></el-form-item></el-col>
|
||||
@@ -340,6 +351,45 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="oaSyncVisible"
|
||||
width="480px"
|
||||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="!oaSyncRunning"
|
||||
:show-close="!oaSyncRunning"
|
||||
class="oa-sync-dialog"
|
||||
@close="closeOaSyncDialog"
|
||||
>
|
||||
<template #header>
|
||||
<span class="dialog-title">{{ oaSyncDialogTitle }}</span>
|
||||
</template>
|
||||
<div class="oa-sync-body">
|
||||
<el-progress
|
||||
:percentage="oaSyncPercent"
|
||||
:status="oaSyncProgressStatus"
|
||||
:stroke-width="12"
|
||||
/>
|
||||
<div class="oa-sync-meta">当前进度:第 {{ oaSyncProgress.current || 0 }} / {{ oaSyncTotalPage }} 页</div>
|
||||
<div class="oa-sync-stats">
|
||||
<div class="oa-sync-stat">
|
||||
<span class="oa-sync-stat__label">同步成功</span>
|
||||
<span class="oa-sync-stat__value oa-sync-stat__value--success">{{ oaSyncProgress.synced }}</span>
|
||||
</div>
|
||||
<div class="oa-sync-stat">
|
||||
<span class="oa-sync-stat__label">跳过</span>
|
||||
<span class="oa-sync-stat__value oa-sync-stat__value--skip">{{ oaSyncProgress.skipped }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button v-if="oaSyncRunning" @click="cancelOaSync">取消</el-button>
|
||||
<el-button v-else type="primary" @click="closeOaSyncDialog">关闭</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 认证日志组件 -->
|
||||
<auth-log ref="authLog" />
|
||||
<!-- 认证锁定配置组件 -->
|
||||
@@ -403,6 +453,18 @@ export default {
|
||||
query: {},
|
||||
loading: true,
|
||||
iamSyncLoading: false,
|
||||
oaSyncVisible: false,
|
||||
oaSyncRunning: false,
|
||||
oaSyncCancelled: false,
|
||||
oaSyncStatus: 'running',
|
||||
oaSyncController: null,
|
||||
oaSyncProgress: {
|
||||
current: 0,
|
||||
size: 50,
|
||||
total: 0,
|
||||
synced: 0,
|
||||
skipped: 0,
|
||||
},
|
||||
page: {
|
||||
pageSize: 10,
|
||||
currentPage: 1,
|
||||
@@ -453,19 +515,56 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['userInfo', 'permission']),
|
||||
// 所属组织级联配置:checkStrictly 为 false 时只能选择最后一级(叶子节点)
|
||||
oaSyncDialogTitle() {
|
||||
if (this.oaSyncStatus === 'done') {
|
||||
return '同步完成';
|
||||
}
|
||||
if (this.oaSyncStatus === 'cancelled') {
|
||||
return '已取消同步';
|
||||
}
|
||||
if (this.oaSyncStatus === 'error') {
|
||||
return '同步失败';
|
||||
}
|
||||
return '同步人员';
|
||||
},
|
||||
oaSyncTotalPage() {
|
||||
const total = Number(this.oaSyncProgress.total) || 0;
|
||||
const size = Number(this.oaSyncProgress.size) || 50;
|
||||
if (total <= 0) {
|
||||
return this.oaSyncProgress.current || 0;
|
||||
}
|
||||
return Math.max(1, Math.ceil(total / size));
|
||||
},
|
||||
oaSyncPercent() {
|
||||
if (this.oaSyncStatus === 'done') {
|
||||
return 100;
|
||||
}
|
||||
const totalPage = this.oaSyncTotalPage;
|
||||
const current = Number(this.oaSyncProgress.current) || 0;
|
||||
if (totalPage <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return Math.min(100, Math.round((current / totalPage) * 100));
|
||||
},
|
||||
oaSyncProgressStatus() {
|
||||
if (this.oaSyncStatus === 'done') {
|
||||
return 'success';
|
||||
}
|
||||
if (this.oaSyncStatus === 'error') {
|
||||
return 'exception';
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
// 所属组织级联配置:checkStrictly 为 true 时任意一级都可选择
|
||||
deptCascaderProps() {
|
||||
return {
|
||||
label: 'title',
|
||||
value: 'id',
|
||||
children: 'children',
|
||||
emitPath: false,
|
||||
checkStrictly: false,
|
||||
checkStrictly: true,
|
||||
expandTrigger: 'click',
|
||||
leaf: (data, node) => {
|
||||
// 判断是否为叶子节点:没有 children 或 children 为空数组
|
||||
return !data.children || data.children.length === 0;
|
||||
},
|
||||
leaf: data => !data.children || data.children.length === 0,
|
||||
};
|
||||
},
|
||||
// 所属组织为单选级联:form.deptId 为数组(提交时 join),级联返回单个 id 值
|
||||
@@ -564,23 +663,87 @@ export default {
|
||||
}
|
||||
},
|
||||
handleIamSync() {
|
||||
this.$confirm('确定从IAM同步人员信息?', '提示', {
|
||||
this.$confirm('确定从OA同步人员及组织信息?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
.then(() => {
|
||||
this.iamSyncLoading = true;
|
||||
return syncIamAccounts();
|
||||
})
|
||||
.then(res => {
|
||||
const count = res?.data?.data ?? 0;
|
||||
this.$message.success(`IAM人员同步完成,共处理${count}条`);
|
||||
}).then(() => {
|
||||
this.startOaSync();
|
||||
});
|
||||
},
|
||||
startOaSync() {
|
||||
this.iamSyncLoading = true;
|
||||
this.oaSyncVisible = true;
|
||||
this.oaSyncRunning = true;
|
||||
this.oaSyncCancelled = false;
|
||||
this.oaSyncStatus = 'running';
|
||||
this.oaSyncController = new AbortController();
|
||||
this.oaSyncProgress = {
|
||||
current: 0,
|
||||
size: 50,
|
||||
total: 0,
|
||||
synced: 0,
|
||||
skipped: 0,
|
||||
};
|
||||
this.runOaSyncPages();
|
||||
},
|
||||
isOaSyncCanceledError(error) {
|
||||
return (
|
||||
this.oaSyncCancelled ||
|
||||
error?.code === 'ERR_CANCELED' ||
|
||||
error?.name === 'CanceledError' ||
|
||||
error?.name === 'AbortError'
|
||||
);
|
||||
},
|
||||
async runOaSyncPages() {
|
||||
const size = 50;
|
||||
let current = 1;
|
||||
try {
|
||||
while (!this.oaSyncCancelled) {
|
||||
const res = await syncIamAccounts(current, size, this.oaSyncController?.signal);
|
||||
const page = res?.data?.data || {};
|
||||
this.oaSyncProgress.current = page.current || current;
|
||||
this.oaSyncProgress.size = page.size || size;
|
||||
this.oaSyncProgress.total = page.total || 0;
|
||||
this.oaSyncProgress.synced += page.syncedCount || 0;
|
||||
this.oaSyncProgress.skipped += page.skippedCount || 0;
|
||||
if (page.finished) {
|
||||
this.oaSyncStatus = 'done';
|
||||
break;
|
||||
}
|
||||
current += 1;
|
||||
}
|
||||
if (this.oaSyncCancelled && this.oaSyncStatus === 'running') {
|
||||
this.oaSyncStatus = 'cancelled';
|
||||
}
|
||||
} catch (error) {
|
||||
if (this.isOaSyncCanceledError(error)) {
|
||||
this.oaSyncStatus = 'cancelled';
|
||||
} else {
|
||||
this.oaSyncStatus = 'error';
|
||||
this.$message.error(error?.message || 'OA人员同步失败');
|
||||
}
|
||||
} finally {
|
||||
this.oaSyncRunning = false;
|
||||
this.iamSyncLoading = false;
|
||||
if (this.oaSyncStatus === 'done' || this.oaSyncStatus === 'cancelled') {
|
||||
this.onLoad(this.page, this.query);
|
||||
})
|
||||
.finally(() => {
|
||||
this.iamSyncLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
cancelOaSync() {
|
||||
if (!this.oaSyncRunning) {
|
||||
return;
|
||||
}
|
||||
this.oaSyncCancelled = true;
|
||||
this.oaSyncController?.abort();
|
||||
},
|
||||
closeOaSyncDialog() {
|
||||
if (this.oaSyncRunning) {
|
||||
this.cancelOaSync();
|
||||
return;
|
||||
}
|
||||
this.oaSyncVisible = false;
|
||||
},
|
||||
handleSetLeader(row) {
|
||||
const tip = row.isLeader === 1 ? '确定取消用户的主管职务?' : '确定设置用户为主管职务?';
|
||||
@@ -680,6 +843,7 @@ export default {
|
||||
if (mode === 'add') {
|
||||
this.form = {
|
||||
account: '',
|
||||
password: '',
|
||||
realName: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
@@ -735,6 +899,9 @@ export default {
|
||||
roleId: func.join(this.form.roleId),
|
||||
leaderId: func.join(this.form.leaderId),
|
||||
};
|
||||
if (this.userDialogMode !== 'add') {
|
||||
delete row.password;
|
||||
}
|
||||
const request = this.userDialogMode === 'add' ? add(row) : update(this.sensitiveManager.getSubmitData(row));
|
||||
request.then(() => {
|
||||
this.userDialog = false;
|
||||
@@ -1150,14 +1317,16 @@ export default {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.user-edit-dialog .dialog-title {
|
||||
.user-edit-dialog .dialog-title,
|
||||
.oa-sync-dialog .dialog-title {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.user-edit-dialog .dialog-title::before {
|
||||
.user-edit-dialog .dialog-title::before,
|
||||
.oa-sync-dialog .dialog-title::before {
|
||||
width: 4px;
|
||||
height: 18px;
|
||||
margin-right: 8px;
|
||||
@@ -1165,6 +1334,52 @@ export default {
|
||||
content: '';
|
||||
}
|
||||
|
||||
.oa-sync-body {
|
||||
padding: 8px 4px 0;
|
||||
}
|
||||
|
||||
.oa-sync-meta {
|
||||
margin-top: 12px;
|
||||
color: #606266;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.oa-sync-stats {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.oa-sync-stat {
|
||||
flex: 1;
|
||||
padding: 12px 16px;
|
||||
background: #fafafa;
|
||||
border: 1px solid #eff1f7;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.oa-sync-stat__label {
|
||||
display: block;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.oa-sync-stat__value {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.oa-sync-stat__value--success {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.oa-sync-stat__value--skip {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.user-edit-dialog .el-form-item {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<div ref="page" class="customer-archive-public-view">
|
||||
<customer-archive ref="archive" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CustomerArchive from './customer-archive.vue';
|
||||
import { postPublicProcessMessage } from '@/api/vehicle/customer-archive';
|
||||
|
||||
export default {
|
||||
name: 'CustomerArchivePublicView',
|
||||
components: {
|
||||
CustomerArchive,
|
||||
},
|
||||
created() {
|
||||
this.handleIframeHeight = () => this.sendIframeHeight();
|
||||
this.handleProcessMessage = event => this.onProcessMessage(event);
|
||||
document.addEventListener('DOMContentLoaded', this.handleIframeHeight, false);
|
||||
window.addEventListener('message', this.handleProcessMessage);
|
||||
},
|
||||
mounted() {
|
||||
document.documentElement.classList.add('mk-iframe-page');
|
||||
document.body.classList.add('mk-iframe-page');
|
||||
const app = document.getElementById('app');
|
||||
if (app) app.classList.add('mk-iframe-page');
|
||||
this.handleIframeHeight();
|
||||
window.addEventListener('load', this.handleIframeHeight);
|
||||
this.mkHeightTimers = [300, 800, 1600].map(delay =>
|
||||
setTimeout(this.handleIframeHeight, delay)
|
||||
);
|
||||
if (typeof ResizeObserver === 'undefined') return;
|
||||
this.mkHeightObserver = new ResizeObserver(() => this.handleIframeHeight());
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.page) this.mkHeightObserver.observe(this.$refs.page);
|
||||
this.mkHeightObserver.observe(document.body);
|
||||
});
|
||||
},
|
||||
beforeUnmount() {
|
||||
document.removeEventListener('DOMContentLoaded', this.handleIframeHeight, false);
|
||||
window.removeEventListener('load', this.handleIframeHeight);
|
||||
window.removeEventListener('message', this.handleProcessMessage);
|
||||
(this.mkHeightTimers || []).forEach(timer => clearTimeout(timer));
|
||||
if (this.mkHeightObserver) {
|
||||
this.mkHeightObserver.disconnect();
|
||||
this.mkHeightObserver = null;
|
||||
}
|
||||
document.documentElement.classList.remove('mk-iframe-page');
|
||||
document.body.classList.remove('mk-iframe-page');
|
||||
const app = document.getElementById('app');
|
||||
if (app) app.classList.remove('mk-iframe-page');
|
||||
},
|
||||
methods: {
|
||||
sendIframeHeight() {
|
||||
this.$nextTick(() => {
|
||||
var tbody = document.body;
|
||||
var height = tbody.clientHeight;
|
||||
// 如需动态改变表单高度,可以直接发送此postMessage
|
||||
window.parent.postMessage({ height: height }, '*');
|
||||
});
|
||||
},
|
||||
onProcessMessage(event) {
|
||||
const data = event && event.data;
|
||||
if (!data || typeof data !== 'object') return;
|
||||
if (data.height && !data.status && !data.type) return;
|
||||
if (data.type === 'formValues' || data.type === 'afterSubmit') return;
|
||||
console.log('客商公开页收到流程消息:', data);
|
||||
const formValues = data.formValues;
|
||||
if (data.status === 'submit') {
|
||||
this.submitData(formValues);
|
||||
} else if (data.status === 'save') {
|
||||
this.saveData(formValues);
|
||||
}
|
||||
if (data.type === 'getFormValues') {
|
||||
window.parent.postMessage({ type: 'formValues', formData: this.buildFormData() }, '*');
|
||||
}
|
||||
},
|
||||
submitData(lbpmFormValues) {
|
||||
const formData = this.buildFormData();
|
||||
this.postFormData('submit', lbpmFormValues, formData);
|
||||
if (!lbpmFormValues) return;
|
||||
const parameters = Object.assign({}, lbpmFormValues, {
|
||||
loginName: this.getLoginName(lbpmFormValues),
|
||||
formInstanceId: this.getFormId(),
|
||||
subject: formData.subject,
|
||||
});
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: 'afterSubmit',
|
||||
success: true,
|
||||
parameters,
|
||||
},
|
||||
'*'
|
||||
);
|
||||
},
|
||||
saveData(lbpmFormValues) {
|
||||
this.postFormData('save', lbpmFormValues, this.buildFormData());
|
||||
},
|
||||
postFormData(status, formValues, formData) {
|
||||
const payload = {
|
||||
status,
|
||||
formValues: formValues || {},
|
||||
formData,
|
||||
};
|
||||
console.log('客商公开页提交流程数据:', payload);
|
||||
postPublicProcessMessage(payload).catch(error => {
|
||||
console.error('客商公开页提交流程数据失败:', error);
|
||||
});
|
||||
},
|
||||
buildFormData() {
|
||||
const archive = this.getArchiveForm();
|
||||
return {
|
||||
...archive,
|
||||
subject: archive.fullName || archive.shortName || '',
|
||||
formInstanceId: this.getFormId(),
|
||||
};
|
||||
},
|
||||
getArchiveForm() {
|
||||
const archive = this.$refs.archive && this.$refs.archive.archiveForm;
|
||||
if (!archive) return {};
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(archive));
|
||||
} catch (error) {
|
||||
return { ...archive };
|
||||
}
|
||||
},
|
||||
getFormId() {
|
||||
return this.$route.query.id || this.getArchiveForm().id || '';
|
||||
},
|
||||
getLoginName(formValues = {}) {
|
||||
return (
|
||||
formValues.loginName ||
|
||||
this.$route.query.loginName ||
|
||||
this.$route.query.submitIdentity ||
|
||||
''
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
html.mk-iframe-page,
|
||||
html.mk-iframe-page body,
|
||||
html.mk-iframe-page #app,
|
||||
html.mk-iframe-page #app.mk-iframe-page {
|
||||
height: auto !important;
|
||||
min-height: 100%;
|
||||
overflow: visible;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.customer-archive-public-view {
|
||||
min-height: 100%;
|
||||
padding: 12px 0 24px;
|
||||
box-sizing: border-box;
|
||||
background: #f0f2f5;
|
||||
|
||||
:deep(.basic-container) {
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
:deep(.archive-page-form .archive-form__footer) {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<basic-container class="customer-archive-page">
|
||||
<basic-container class="customer-archive-page" :class="{ 'is-public-view': isPublicViewPage }">
|
||||
<avue-crud
|
||||
v-if="!isArchivePage"
|
||||
:option="option"
|
||||
@@ -913,7 +913,7 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<div class="archive-form__footer">
|
||||
<div class="archive-form__footer" v-if="!isPublicViewPage">
|
||||
<!-- 次要:独立页返回 / 只读关闭 / 弹窗取消 -->
|
||||
<el-button @click="closeArchive">{{ readonly ? '关闭' : '取消' }}</el-button>
|
||||
<el-button type="primary" plain v-if="!readonly" @click="saveArchive">保存</el-button>
|
||||
@@ -1482,7 +1482,9 @@ import { ElCascader } from 'element-plus';
|
||||
import {
|
||||
getList,
|
||||
getDetail,
|
||||
getPublicDetail,
|
||||
getChangeRecordList,
|
||||
getPublicChangeRecordList,
|
||||
submit,
|
||||
submitApproval,
|
||||
withdrawApproval,
|
||||
@@ -1498,6 +1500,7 @@ import {
|
||||
getDetail as getCreditScoreQuantificationDetail,
|
||||
} from '@/api/vehicle/credit-score-quantification';
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
import { processDelete, processSubmit } from '@/api/system/business-process';
|
||||
import { getDeptTree } from '@/api/system/dept';
|
||||
import { getLazyTree } from '@/api/base/region';
|
||||
import { exportBlob } from '@/api/common';
|
||||
@@ -1989,6 +1992,17 @@ export default {
|
||||
};
|
||||
},
|
||||
created() {
|
||||
if (this.isPublicViewPage) {
|
||||
this.readonly = true;
|
||||
const id = this.$route.query.id;
|
||||
if (!id) {
|
||||
this.archiveBox = true;
|
||||
this.$message.error('缺少客商ID');
|
||||
return;
|
||||
}
|
||||
this.openArchive({ id }, true);
|
||||
return;
|
||||
}
|
||||
this.initDeptTree();
|
||||
this.initRegionOptions();
|
||||
this.initBusinessDictionaries();
|
||||
@@ -2010,7 +2024,12 @@ export default {
|
||||
};
|
||||
},
|
||||
isArchivePage() {
|
||||
return this.$route.path === '/vehicle/customer-archive/form';
|
||||
return (
|
||||
this.$route.path === '/vehicle/customer-archive/form' || this.isPublicViewPage
|
||||
);
|
||||
},
|
||||
isPublicViewPage() {
|
||||
return this.$route.path === '/vehicle/customer-archive/public-view';
|
||||
},
|
||||
archiveContainer() {
|
||||
return 'div';
|
||||
@@ -2190,6 +2209,45 @@ export default {
|
||||
hasPermission(code) {
|
||||
return this.isAdmin || this.validData(this.permission[code], false);
|
||||
},
|
||||
mergeSelectOptions(target, values) {
|
||||
const list = Array.isArray(values) ? values : values ? [values] : [];
|
||||
list.forEach(value => {
|
||||
if (value === undefined || value === null || value === '') return;
|
||||
if (!this[target].some(item => String(item.value) === String(value))) {
|
||||
this[target].push({ label: String(value), value });
|
||||
}
|
||||
});
|
||||
},
|
||||
ensurePublicViewOptions(archive = {}) {
|
||||
this.mergeSelectOptions('customerNatureOptions', archive.customerNature);
|
||||
this.mergeSelectOptions('customerTypeOptions', archive.customerType);
|
||||
this.mergeSelectOptions('businessScopeOptions', archive.businessScope);
|
||||
this.mergeSelectOptions(
|
||||
'qualificationTypeOptions',
|
||||
(this.qualificationFiles || []).map(item => item.type)
|
||||
);
|
||||
const deptId = Array.isArray(archive.deptId) ? archive.deptId[0] : archive.deptId;
|
||||
if (deptId && archive.deptName) {
|
||||
this.deptTree = [
|
||||
{
|
||||
label: archive.deptName,
|
||||
value: String(deptId),
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
this.deptOptions = this.flattenDept(this.deptTree);
|
||||
}
|
||||
(archive.scores || []).forEach(score => {
|
||||
if (!score.quantificationId) return;
|
||||
const id = String(score.quantificationId);
|
||||
if (!this.scoreQuantificationOptions.some(item => String(item.id) === id)) {
|
||||
this.scoreQuantificationOptions.push({
|
||||
id,
|
||||
name: score.quantificationName || score.name || id,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
// 检测输入框文本是否溢出,仅溢出时才允许 tooltip 显示完整地址
|
||||
checkOverflow(refName, flag) {
|
||||
const inst = this.$refs[refName];
|
||||
@@ -2278,7 +2336,8 @@ export default {
|
||||
loadChangeRecords() {
|
||||
if (!this.archiveForm.id) return;
|
||||
const page = this.changeRecordPage;
|
||||
getChangeRecordList(this.archiveForm.id, page.currentPage, page.pageSize).then(res => {
|
||||
const requestList = this.isPublicViewPage ? getPublicChangeRecordList : getChangeRecordList;
|
||||
requestList(this.archiveForm.id, page.currentPage, page.pageSize).then(res => {
|
||||
const data = res.data.data || {};
|
||||
page.records = this.replaceNegativeOneWithBlank(data.records || []);
|
||||
page.total = Number(data.total || 0);
|
||||
@@ -3890,6 +3949,9 @@ export default {
|
||||
});
|
||||
},
|
||||
closeArchive() {
|
||||
if (this.isPublicViewPage) return;
|
||||
this.archiveBox = false;
|
||||
this.$router.$avueRouter?.closeTag?.();
|
||||
this.$router.push('/vehicle/customer-archive');
|
||||
},
|
||||
openArchive(row, readonly = false) {
|
||||
@@ -3898,7 +3960,9 @@ export default {
|
||||
this.resetDetailPagination();
|
||||
this.resetChangeRecordPage();
|
||||
if (row && row.id) {
|
||||
getDetail(row.id).then(res => {
|
||||
const requestDetail = this.isPublicViewPage ? getPublicDetail : getDetail;
|
||||
requestDetail(row.id)
|
||||
.then(res => {
|
||||
const archive = this.normalizeDetail(res.data.data);
|
||||
this.archiveForm = readonly ? this.replaceNegativeOneWithBlank(archive) : archive;
|
||||
this.originalAccessType = this.archiveForm.accessType || '';
|
||||
@@ -3912,9 +3976,14 @@ export default {
|
||||
this.qualificationUploadFiles = [];
|
||||
this.ocrQualificationUploadFiles = [];
|
||||
this.selectedQualificationFiles = [];
|
||||
if (this.isPublicViewPage) this.ensurePublicViewOptions(this.archiveForm);
|
||||
this.archiveBox = true;
|
||||
this.loadChangeRecords();
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.archiveBox = true;
|
||||
if (this.isPublicViewPage) this.$message.error('客商信息加载失败');
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.archiveForm = this.emptyArchive();
|
||||
@@ -4078,7 +4147,16 @@ export default {
|
||||
this.$message({ type: 'success', message: '保存成功,请在列表提交审批' });
|
||||
return;
|
||||
}
|
||||
submitApproval(id).then(() => {
|
||||
const fullName =
|
||||
(data && typeof data === 'object' ? data.fullName : '') ||
|
||||
archive.fullName ||
|
||||
this.archiveForm.fullName ||
|
||||
'';
|
||||
this.submitCustomerApproval(
|
||||
id,
|
||||
fullName,
|
||||
archive.approvalStatus || this.archiveForm.approvalStatus
|
||||
).then(() => {
|
||||
this.closeArchive();
|
||||
this.onLoad(this.page);
|
||||
this.$message({ type: 'success', message: '提交成功!' });
|
||||
@@ -4086,6 +4164,42 @@ export default {
|
||||
});
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 列表 / 新增 / 编辑提交共用:提交 MK 审核流并更新客商审批状态
|
||||
*/
|
||||
submitCustomerApproval(id, fullName = '', approvalStatus = '') {
|
||||
return this.submitMkApprovalFlow(id, fullName, approvalStatus).then(() => submitApproval(id));
|
||||
},
|
||||
/**
|
||||
* 提交 MK 审核流:templateCode 取业务字典 mk_template 中「提交客商审核流」的键值
|
||||
* 提交人手机号由后端从用户表读取真实值(前端接口会脱敏)
|
||||
* MK 驳回后再提交时,先删除旧流程再创建新流程
|
||||
*/
|
||||
async submitMkApprovalFlow(formInstanceId, subjectName = '', approvalStatus = '') {
|
||||
if (approvalStatus === 'rejected') {
|
||||
await processDelete({ formInstanceId: String(formInstanceId) });
|
||||
}
|
||||
const templateCode = await this.resolveMkTemplateCode('提交客商审核流');
|
||||
const subject = subjectName
|
||||
? `客商准入审批:${subjectName}`
|
||||
: `客商准入审批:${formInstanceId}`;
|
||||
return processSubmit({
|
||||
templateCode,
|
||||
formInstanceId: String(formInstanceId),
|
||||
subject,
|
||||
});
|
||||
},
|
||||
async resolveMkTemplateCode(dictName = '提交客商审核流') {
|
||||
const res = await getDictionary({ code: 'mk_template' });
|
||||
const list = res?.data?.data || [];
|
||||
const matched = list.find(item => String(item.dictValue || '').trim() === dictName);
|
||||
const templateCode = matched?.dictKey;
|
||||
if (!templateCode) {
|
||||
this.$message.warning(`未配置业务字典 mk_template「${dictName}」,无法提交审核流`);
|
||||
return Promise.reject(new Error(`未配置业务字典 mk_template「${dictName}」`));
|
||||
}
|
||||
return String(templateCode);
|
||||
},
|
||||
addScore() {
|
||||
this.scoreRecordIndex = -1;
|
||||
this.currentScore = this.normalizeScore({});
|
||||
@@ -4676,7 +4790,13 @@ export default {
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
.then(() => submitApproval(row.id))
|
||||
.then(() =>
|
||||
this.submitCustomerApproval(
|
||||
row.id,
|
||||
archive.fullName || row.fullName,
|
||||
archive.approvalStatus || row.approvalStatus
|
||||
)
|
||||
)
|
||||
.then(() => {
|
||||
this.onLoad(this.page);
|
||||
this.$message({ type: 'success', message: '提交成功!' });
|
||||
@@ -5348,6 +5468,12 @@ export default {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.customer-archive-page.is-public-view {
|
||||
:deep(.archive-page-form .archive-form__footer) {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.archive-page-form {
|
||||
min-height: 100%;
|
||||
margin-bottom: 60px;
|
||||
|
||||
@@ -516,7 +516,7 @@ export default {
|
||||
propsHttp: {
|
||||
res: 'data',
|
||||
},
|
||||
tip: '请上传 .xls,.xlsx 标准格式文件',
|
||||
tip: '请上传 .xls,.xlsx 标准格式文件;日期支持 2026-08-02、2026-8-2、2026/8/2 等写法',
|
||||
action: '/blade-transport/maintenance-plan/import-maintenance-plan',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -489,7 +489,7 @@ export default {
|
||||
propsHttp: {
|
||||
res: 'data',
|
||||
},
|
||||
tip: '请上传 .xls,.xlsx 标准格式文件',
|
||||
tip: '请上传 .xls,.xlsx 标准格式文件;日期支持 2026-08-02、2026-8-2、2026/8/2 等写法',
|
||||
action: '/blade-transport/maintenance-record/import-maintenance-record',
|
||||
},
|
||||
{
|
||||
|
||||
+13
-13
@@ -47,26 +47,26 @@ export default ({ mode, command }) => {
|
||||
__VUE_I18N_LEGACY_API__: true,
|
||||
__INTLIFY_PROD_DEVTOOLS__: false,
|
||||
},
|
||||
// server: {
|
||||
// port: 2888,
|
||||
// proxy: {
|
||||
// '/api': {
|
||||
// target: 'http://localhost',
|
||||
// //target: 'https://saber3.bladex.cn/api',
|
||||
// changeOrigin: true,
|
||||
// rewrite: path => path.replace(/^\/api/, ''),
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
server: {
|
||||
port: 2889,
|
||||
port: 2888,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://172.16.203.228:8000',
|
||||
target: 'http://localhost',
|
||||
//target: 'https://saber3.bladex.cn/api',
|
||||
changeOrigin: true,
|
||||
rewrite: path => path.replace(/^\/api/, ''),
|
||||
},
|
||||
},
|
||||
},
|
||||
// server: {
|
||||
// port: 2889,
|
||||
// proxy: {
|
||||
// '/api': {
|
||||
// target: 'http://172.16.203.228:8000',
|
||||
// changeOrigin: true,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
resolve: {
|
||||
alias: {
|
||||
'~': resolve(__dirname, './'),
|
||||
|
||||
Reference in New Issue
Block a user