Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 74988c0b5e | |||
| 4c117d87a5 | |||
| d6bcb12c20 | |||
| e1d87f5788 | |||
| 56b8f68449 | |||
| 0ac17dea0b | |||
| 39e3115fbe | |||
| 9c0f7cb3f4 | |||
| 7b478bdbf2 | |||
| 87d6ff97a3 | |||
| e3bd44e8fc | |||
| 324daf9d4d | |||
| 37ac683fb4 | |||
| c402d68aea | |||
| e8f6311477 | |||
| fa78a68df6 | |||
| 320422856c | |||
| 0add4c5cdc | |||
| f6621392b1 | |||
| 2a5eca8393 | |||
| e4ae1d3c77 | |||
| 96783fcd7b | |||
| 372e8826a2 | |||
| 8d9fe998c0 | |||
| 92a8d4aab1 | |||
| 59a1ca8a0d | |||
| 5544cfb3ca | |||
| fcd5ef7df9 | |||
| 489b406214 | |||
| 353148eb7f | |||
| 293b4fc6ce | |||
| 53b588063e | |||
| 0889f29c59 | |||
| f1c004813e | |||
| d67cf55491 | |||
| d7969c191a | |||
| a657b9d0ef | |||
| 6384e80758 | |||
| d3af455374 | |||
| 1622015724 | |||
| f7d8fbf16b | |||
| 847f09b14d | |||
| 2ae817ec38 | |||
| 6d80ffde6c | |||
| 44809ad303 | |||
| 91cb3ee8c2 | |||
| b2a6d73bac |
@@ -24,11 +24,16 @@ export const getDetail = id =>
|
||||
},
|
||||
});
|
||||
|
||||
export const getCarrierContracts = projectIds =>
|
||||
export const getCarrierContracts = (projectIds, waybillIds = []) =>
|
||||
request({
|
||||
url: `${baseUrl}/carrier-contracts`,
|
||||
method: 'get',
|
||||
params: { projectIds: (projectIds || []).join(',') },
|
||||
params: {
|
||||
projectIds: (projectIds || []).join(','),
|
||||
...(waybillIds || []).filter(Boolean).length
|
||||
? { waybillIds: (waybillIds || []).filter(Boolean).join(',') }
|
||||
: {},
|
||||
},
|
||||
});
|
||||
|
||||
export const saveDraft = row =>
|
||||
|
||||
@@ -14,6 +14,13 @@ export const cancel = api.cancel;
|
||||
export const reassign = api.reassign;
|
||||
export const complete = api.complete;
|
||||
export const batchComplete = api.batchComplete;
|
||||
/** 确认回单:回单节点配成「需业务员确认」时用;后端校验司机打卡节点齐全且无未了结异常后才置完成 */
|
||||
export const confirmReturn = id =>
|
||||
request({
|
||||
url: `${baseUrl}/confirm-return`,
|
||||
method: 'post',
|
||||
params: { id },
|
||||
});
|
||||
export const changeRoute = data =>
|
||||
request({
|
||||
url: `${baseUrl}/change-route`,
|
||||
|
||||
@@ -139,3 +139,10 @@ export const getPlatformCompanySelect = () => {
|
||||
method: 'get',
|
||||
});
|
||||
};
|
||||
|
||||
export const getBusinessDeptSelect = () => {
|
||||
return request({
|
||||
url: '/blade-system/dept/business-dept-select',
|
||||
method: 'get',
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import request from '@/axios';
|
||||
|
||||
export const getRoleMenuDataScopeTree = roleId => {
|
||||
return request({
|
||||
url: '/blade-system/role-menu-data-scope/tree',
|
||||
method: 'get',
|
||||
params: {
|
||||
roleId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const submitRoleMenuDataScope = data => {
|
||||
return request({
|
||||
url: '/blade-system/role-menu-data-scope/submit',
|
||||
method: 'post',
|
||||
data,
|
||||
});
|
||||
};
|
||||
+31
-10
@@ -105,12 +105,27 @@ const isJsonContentType = res => {
|
||||
return contentType.includes('application/json') || contentType.includes('text/json');
|
||||
};
|
||||
|
||||
/**
|
||||
* 是否为可按文本读取的 Blob / Blob-like(含跨 realm)。
|
||||
* 禁止对普通业务 JSON 对象调用 .text()。
|
||||
*/
|
||||
const isBlobLikeData = value =>
|
||||
value != null &&
|
||||
typeof value === 'object' &&
|
||||
typeof value.size === 'number' &&
|
||||
typeof value.type === 'string' &&
|
||||
typeof value.text === 'function';
|
||||
|
||||
/**
|
||||
* 判断响应体是否已是解析好的对象(无需再解析)。
|
||||
* 注意 Blob 也是 object,必须排除,否则会把待解析的文件流误判为已完成。
|
||||
*/
|
||||
const isPlainObject = value =>
|
||||
value !== null && typeof value === 'object' && !(value instanceof Blob);
|
||||
value !== null &&
|
||||
typeof value === 'object' &&
|
||||
!(value instanceof Blob) &&
|
||||
!isBlobLikeData(value) &&
|
||||
!(value instanceof ArrayBuffer);
|
||||
//跨域请求,允许保存cookie
|
||||
axios.defaults.withCredentials = true;
|
||||
// NProgress Configuration
|
||||
@@ -190,15 +205,20 @@ axios.interceptors.response.use(
|
||||
// 用户就会以为系统崩了——而后端其实返回了明确的业务提示(如「未读取到数据行…」)。
|
||||
// 这里按 Content-Type 判断:JSON 才解析回对象;真·文件流(Excel 等)保持原样不动。
|
||||
// 兼容 Blob(浏览器 responseType='blob')与 string(非浏览器环境)两种形态。
|
||||
// 仅对真正的 Blob/Blob-like 调 .text(),避免业务 JSON 已解析后出现
|
||||
// 「e.data.text is not a function」。
|
||||
if (isJsonContentType(res) && !isPlainObject(res.data)) {
|
||||
try {
|
||||
let text = '';
|
||||
if (typeof res.data?.text === 'function') {
|
||||
text = await res.data.text();
|
||||
} else if (typeof res.data === 'string') {
|
||||
text = res.data;
|
||||
} else if (typeof res.data?.arrayBuffer === 'function') {
|
||||
text = new TextDecoder().decode(await res.data.arrayBuffer());
|
||||
const raw = res.data;
|
||||
if (isBlobLikeData(raw) || (typeof Blob !== 'undefined' && raw instanceof Blob)) {
|
||||
text = await raw.text();
|
||||
} else if (typeof raw === 'string') {
|
||||
text = raw;
|
||||
} else if (raw && typeof raw.arrayBuffer === 'function') {
|
||||
text = new TextDecoder().decode(await raw.arrayBuffer());
|
||||
} else if (raw instanceof ArrayBuffer) {
|
||||
text = new TextDecoder().decode(raw);
|
||||
}
|
||||
const parsed = typeof text === 'string' && text ? JSON.parse(text) : null;
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
@@ -208,10 +228,11 @@ axios.interceptors.response.use(
|
||||
// 声明是 JSON 却解析失败:保持原样,交给下游按文件处理
|
||||
}
|
||||
}
|
||||
//获取状态信息
|
||||
const status = res.data.error_code || res.data.code || res.status;
|
||||
// 获取状态信息:文件流没有 code/msg,必须退回 HTTP status,禁止读 Blob 上的幽灵字段
|
||||
const payload = isPlainObject(res.data) ? res.data : {};
|
||||
const status = Number(payload.error_code || payload.code || res.status);
|
||||
const statusWhiteList = website.statusWhiteList || [];
|
||||
const message = res.data.msg || res.data.error_description || '系统错误';
|
||||
const message = payload.msg || payload.error_description || '系统错误';
|
||||
//如果在白名单里则自行catch逻辑处理
|
||||
if (statusWhiteList.includes(status)) return Promise.reject(res);
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ import { h } from 'vue';
|
||||
* 1. 混入本 mixin:mixins: [organizationSearch]
|
||||
* 2. option 中「所属组织」列加上 searchslot: true(prop 为 organizationName 或 deptName 均可)
|
||||
* 3. 在 created / 初始化时调用 this.loadOrganizationOptions()
|
||||
* 4. 需要「未分配组织」筛选项的页面在 data 里置 enableUnassignedOrgFilter: true;
|
||||
* 该项只追加进搜索树 organizationSearchTreeOptions,表单级联 organizationTreeOptions 不含哨兵
|
||||
*
|
||||
* 说明:
|
||||
* - 自动识别 organizationName / deptName 两种 prop,并挂载 column.renderSearch
|
||||
@@ -26,6 +28,12 @@ import { h } from 'vue';
|
||||
*/
|
||||
const ORG_PROPS = ['organizationName', 'deptName'];
|
||||
|
||||
// 「未分配组织」筛选项的哨兵值:不是真实组织名,选中后提交给后端的就是这串字面量。
|
||||
// 后端 TransportBusiConstant.UNASSIGNED_ORG_FILTER、DriverMapper.xml / TransportVehicleMapper.xml
|
||||
// 的 organizationName 分支以及司机、车辆导出查询都按它改判为 organization_name = '';
|
||||
// 后端换成空串会让整段组织条件不生效,筛选静默失效。前端与后端这几处必须同步修改。
|
||||
const UNASSIGNED_ORG_FILTER = '__UNASSIGNED_ORG__';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
@@ -34,9 +42,18 @@ export default {
|
||||
organizationTreeOptions: [],
|
||||
organizationTreeFlatOptions: [],
|
||||
organizationLoading: false,
|
||||
// 组织筛选是否追加「未分配组织」一项;页面在 data 里覆盖为 true 才生效
|
||||
enableUnassignedOrgFilter: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// 搜索栏级联的选项:开关打开时在真实组织最前面追加「未分配组织」。
|
||||
// 不能直接改 organizationTreeOptions——司机/车辆页的编辑表单也用这棵树选组织。
|
||||
organizationSearchTreeOptions() {
|
||||
return this.enableUnassignedOrgFilter
|
||||
? [this.unassignedOrgOption(), ...this.organizationTreeOptions]
|
||||
: this.organizationTreeOptions;
|
||||
},
|
||||
// 所属组织级联配置:checkStrictly 允许选中任意层级,不必选到末级
|
||||
organizationCascaderProps() {
|
||||
return {
|
||||
@@ -61,7 +78,7 @@ export default {
|
||||
? this.excludeExternalOrganization(source)
|
||||
: source;
|
||||
this.organizationTreeOptions = this.normalizeOrgTree(tree);
|
||||
this.organizationTreeFlatOptions = this.flattenOrgTree(this.organizationTreeOptions);
|
||||
this.organizationTreeFlatOptions = this.flattenOrgTree(this.organizationSearchTreeOptions);
|
||||
const option = this.tableOption || this.option;
|
||||
if (!option) return;
|
||||
ORG_PROPS.forEach(prop => {
|
||||
@@ -100,12 +117,20 @@ export default {
|
||||
};
|
||||
});
|
||||
},
|
||||
// 哨兵节点的 id 与 rawLabel 都取哨兵值,resolveOrgName 取到的仍是哨兵而不是某个真实组织名
|
||||
unassignedOrgOption() {
|
||||
return { id: UNASSIGNED_ORG_FILTER, label: '未分配组织', rawLabel: UNASSIGNED_ORG_FILTER };
|
||||
},
|
||||
flattenOrgTree(tree = []) {
|
||||
return (tree || []).flatMap(item => [
|
||||
{ id: String(item.id), label: item.label, rawLabel: item.rawLabel },
|
||||
...this.flattenOrgTree(item.children || []),
|
||||
]);
|
||||
},
|
||||
// 组织列渲染用:空串或纯空白即「未分配组织」(组织列 NOT NULL,无组织存空串)
|
||||
isBlankOrganizationName(value) {
|
||||
return String(value ?? '').trim() === '';
|
||||
},
|
||||
// 兼容两种入参:级联路径数组(取末级)或单个部门 id
|
||||
findOrgById(id) {
|
||||
const value = Array.isArray(id) ? id.at(-1) : id;
|
||||
@@ -155,7 +180,7 @@ export default {
|
||||
// 级联单选:modelValue 必须是 id 路径数组,否则控件内部状态会错乱导致无法再次选中
|
||||
if (scope.row) scope.row[prop] = Array.isArray(value) ? value : [];
|
||||
},
|
||||
options: this.organizationTreeOptions,
|
||||
options: this.organizationSearchTreeOptions,
|
||||
props: {
|
||||
label: 'label',
|
||||
value: 'id',
|
||||
|
||||
@@ -42,6 +42,7 @@ export const waybillStatusOptions = [
|
||||
{ label: '草稿', value: 'draft' },
|
||||
{ label: '待执行', value: 'pending' },
|
||||
{ label: '进行中', value: 'running' },
|
||||
{ label: '待回单确认', value: 'pending_return_confirm' },
|
||||
{ label: '已完成', value: 'completed' },
|
||||
{ label: '已取消', value: 'cancelled' },
|
||||
];
|
||||
|
||||
@@ -471,7 +471,7 @@ export const config = {
|
||||
endDateRange: ['endDateStart', 'endDateEnd'],
|
||||
createTimeRange: ['createTimeStart', 'createTimeEnd'],
|
||||
},
|
||||
actions: ['copy', 'cancel', 'reassign', 'complete', 'batchComplete'],
|
||||
actions: ['copy', 'cancel', 'reassign', 'complete', 'batchComplete', 'confirmReturn'],
|
||||
detailButton: true,
|
||||
statusProp: 'businessStatus',
|
||||
statusTextProp: 'businessStatusName',
|
||||
@@ -483,6 +483,8 @@ export const config = {
|
||||
row.requireAccept === true ||
|
||||
row.requireAccept === 1 ||
|
||||
requiresDriverAcceptConfirmation(row);
|
||||
// 待回单确认由末位打卡产生,司机必然已接单
|
||||
if (status === 'pending_return_confirm') return '待回单确认';
|
||||
// 需司机接单且尚未接单 → 待执行(含库中仍为 running 的历史数据展示校正)
|
||||
if (needAccept && !hasDriverAcceptRecord(row) && !terminalStatuses.includes(status)) {
|
||||
return '待执行';
|
||||
@@ -504,7 +506,7 @@ export const config = {
|
||||
return !isDriverRejectedWaybill(row);
|
||||
},
|
||||
canCancel(row) {
|
||||
// 进行中的运单不允许取消
|
||||
// 进行中的运单不允许取消;待回单确认仍可取消(与页面 canCancel 一致)
|
||||
return !isInProgressStatus(row, row.businessStatusName);
|
||||
},
|
||||
deleteStatus: ['draft'],
|
||||
|
||||
@@ -14,6 +14,11 @@ const validatePhone = (rule, value, callback) => {
|
||||
callback(new Error('请输入手机号'));
|
||||
return;
|
||||
}
|
||||
// 详情回显为脱敏手机号(含 *)时放行,未修改则提交时剔除该字段
|
||||
if (phone.includes('*')) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
if (!/^1\d{10}$/.test(phone)) {
|
||||
callback(new Error('请输入正确的手机号'));
|
||||
return;
|
||||
@@ -121,6 +126,62 @@ export default {
|
||||
order: 60,
|
||||
rules: [{ required: true, message: '请选择小程序身份', trigger: 'change' }],
|
||||
},
|
||||
{
|
||||
label: '所属组织',
|
||||
prop: 'deptId',
|
||||
formslot: true,
|
||||
hide: true,
|
||||
span: 24,
|
||||
order: 55,
|
||||
// 所属组织可为空:为空表示未分配组织,账号的数据权限为空集
|
||||
},
|
||||
{
|
||||
label: '所属组织',
|
||||
prop: 'deptName',
|
||||
minWidth: 180,
|
||||
overHidden: true,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
viewDisplay: false,
|
||||
order: 54,
|
||||
},
|
||||
{
|
||||
label: '数据权限范围',
|
||||
prop: 'dataScopeRange',
|
||||
type: 'radio',
|
||||
span: 24,
|
||||
hide: true,
|
||||
order: 53,
|
||||
value: 1,
|
||||
dicData: [
|
||||
{ label: '仅所属组织', value: 1 },
|
||||
{ label: '全部组织', value: 2 },
|
||||
{ label: '自定义', value: 3 },
|
||||
],
|
||||
rules: [{ required: true, message: '请选择数据权限范围', trigger: 'change' }],
|
||||
},
|
||||
{
|
||||
label: '数据层级范围',
|
||||
prop: 'dataLevelRange',
|
||||
type: 'radio',
|
||||
span: 24,
|
||||
hide: true,
|
||||
order: 52,
|
||||
value: 1,
|
||||
dicData: [
|
||||
{ label: '包含下级', value: 1 },
|
||||
{ label: '仅本级', value: 2 },
|
||||
],
|
||||
rules: [{ required: true, message: '请选择数据层级范围', trigger: 'change' }],
|
||||
},
|
||||
{
|
||||
label: '数据权限',
|
||||
prop: 'dataScopeDeptIds',
|
||||
formslot: true,
|
||||
span: 24,
|
||||
hide: true,
|
||||
order: 51,
|
||||
},
|
||||
{
|
||||
label: '性别',
|
||||
prop: 'sex',
|
||||
|
||||
@@ -315,29 +315,6 @@ export const userOption = safe => {
|
||||
{ label: '承运商', value: 2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '数据权限范围',
|
||||
prop: 'dataScopeRange',
|
||||
type: 'radio',
|
||||
span: 24,
|
||||
value: 1,
|
||||
dicData: [
|
||||
{ label: '仅所属组织', value: 1 },
|
||||
{ label: '全部组织', value: 2 },
|
||||
{ label: '自定义', value: 3 },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '数据层级范围',
|
||||
prop: 'dataLevelRange',
|
||||
type: 'radio',
|
||||
span: 24,
|
||||
value: 1,
|
||||
dicData: [
|
||||
{ label: '包含下级', value: 1 },
|
||||
{ label: '仅本级', value: 2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '备注',
|
||||
prop: 'remark',
|
||||
|
||||
@@ -48,6 +48,7 @@ export const option = {
|
||||
searchOrder: 1,
|
||||
searchslot: true,
|
||||
slot: true,
|
||||
overHidden: true,
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
@@ -55,6 +56,8 @@ export const option = {
|
||||
prop: 'useDepartment',
|
||||
search: true,
|
||||
searchOrder: 3,
|
||||
slot: true,
|
||||
overHidden: true,
|
||||
minWidth: 140,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -622,7 +622,7 @@
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
// 合同管理新增/编辑弹窗(business-crud-page 封装 → avue-crud,弹窗带 avue-crud__dialog class)
|
||||
// 合同管理新增/编辑弹窗(avue-crud,弹窗带 avue-crud__dialog class)
|
||||
// 复刻 waybill-manage 方案:接近全屏、压过库内置 .avue-dialog 的 calc(100% - 200px),
|
||||
// 并解开 body 的 200px 上限、抵消全局 footer 的 -30px 上移
|
||||
.contract-manage-dialog.el-dialog {
|
||||
|
||||
@@ -26,6 +26,35 @@ const uniqueOrgNames = (values = []) => {
|
||||
return result;
|
||||
};
|
||||
|
||||
// 候选司机取数上限:超出后只能靠关键字缩小范围
|
||||
const DRIVER_CANDIDATE_MAX_SIZE = 200;
|
||||
|
||||
/**
|
||||
* 派单候选司机:租户内的司机档案,按岗位等条件过滤。
|
||||
*
|
||||
* 与组织、承运商都无关——司机档案与组织树、承运商之间没有外键,`organization_name`
|
||||
* 只是自由文本,用它做匹配会让小程序自动建档的司机永远进不了候选。
|
||||
*/
|
||||
export const fetchDriverCandidates = async (query = {}) => {
|
||||
const params = { ...query };
|
||||
const size = Math.min(Math.max(Number(params.size) || 50, 50), DRIVER_CANDIDATE_MAX_SIZE);
|
||||
delete params.size;
|
||||
const res = await getDriverList(1, size, params);
|
||||
return extractRecords(res).slice(0, size);
|
||||
};
|
||||
|
||||
/**
|
||||
* 司机候选项的展示文本:姓名(手机尾号后 4 位)。
|
||||
*
|
||||
* 只用于下拉渲染。写回表单的仍是姓名本身,别把带尾号的文本当值用。
|
||||
*/
|
||||
export const driverOptionText = (item = {}) => {
|
||||
const name = normalizeOrgName(item.driverName || item.name);
|
||||
const mobile = String(item.mobile || item.phone || item.driverPhone || '').trim();
|
||||
if (!name) return '';
|
||||
return mobile.length >= 4 ? `${name}(${mobile.slice(-4)})` : name;
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析承运商客商档案上联系人的所属组织(branchName),无联系人时回退客商所属组织。
|
||||
*/
|
||||
@@ -72,13 +101,6 @@ export const resolveCarrierOrganizationNames = async ({ carrierId, carrierName }
|
||||
return orgNames;
|
||||
};
|
||||
|
||||
export const clearCarrierOrganizationCache = (carrier = {}) => {
|
||||
const id = String(carrier.carrierId || '').trim();
|
||||
const name = normalizeOrgName(carrier.carrierName);
|
||||
if (id) orgNameCache.delete(id);
|
||||
if (name) orgNameCache.delete(name);
|
||||
};
|
||||
|
||||
export const matchOrganizationName = (value, orgNames = []) => {
|
||||
const text = normalizeOrgName(value);
|
||||
if (!text || !orgNames.length) return false;
|
||||
@@ -90,34 +112,9 @@ const filterByOrganizations = (records = [], orgNames = []) => {
|
||||
return records.filter(item => matchOrganizationName(item.organizationName, orgNames));
|
||||
};
|
||||
|
||||
/**
|
||||
* 按承运商联系人所属组织筛选司机。
|
||||
* 未选择承运商时返回空列表(需先选承运商)。
|
||||
*/
|
||||
export const fetchDriversByCarrierOrganizations = async (
|
||||
query = {},
|
||||
{ carrierId, carrierName } = {}
|
||||
) => {
|
||||
if (!String(carrierId || '').trim() && !normalizeOrgName(carrierName)) {
|
||||
return [];
|
||||
}
|
||||
const orgNames = await resolveCarrierOrganizationNames({ carrierId, carrierName });
|
||||
if (!orgNames.length) return [];
|
||||
|
||||
const size = Math.max(Number(query.size) || 50, 50);
|
||||
const params = { ...query };
|
||||
delete params.size;
|
||||
// 单组织时用后端模糊条件缩小范围;多组织再前端精确过滤
|
||||
if (orgNames.length === 1) {
|
||||
params.organizationName = orgNames[0];
|
||||
}
|
||||
const res = await getDriverList(1, Math.min(size * 5, 200), params);
|
||||
return filterByOrganizations(extractRecords(res), orgNames).slice(0, size);
|
||||
};
|
||||
|
||||
/**
|
||||
* 按承运商联系人所属组织筛选车辆。
|
||||
* 未选择承运商时返回空列表。
|
||||
* 未选择承运商时返回空列表(需先选承运商)。
|
||||
*/
|
||||
export const fetchVehiclesByCarrierOrganizations = async (
|
||||
query = {},
|
||||
|
||||
@@ -106,6 +106,9 @@ const CERT_SNAPSHOT_GROUPS = {
|
||||
fields: [
|
||||
{ key: 'realName', label: '真实姓名' },
|
||||
{ key: 'idCardNo', label: '身份证号' },
|
||||
{ key: 'birthday', label: '出生年月', type: TYPE_DATE },
|
||||
{ key: 'addressRegion', label: '住址' },
|
||||
{ key: 'address', label: '详细地址' },
|
||||
{ key: 'idCardFrontUrl', label: '身份证正面照', type: TYPE_IMAGE },
|
||||
{ key: 'idCardBackUrl', label: '身份证反面照', type: TYPE_IMAGE },
|
||||
{ key: 'idCardValidStart', label: '身份证有效期起', type: TYPE_DATE },
|
||||
|
||||
@@ -88,7 +88,7 @@ export const parseBlobJson = async data => {
|
||||
*
|
||||
* 这里补上这一步:从错误响应的 Blob 里取出后端 `msg`,取不到再退回通用文案。
|
||||
*/
|
||||
const resolveErrorMessage = async error => {
|
||||
export const resolveImportErrorMessage = async error => {
|
||||
const payload = error?.response?.data ?? error?.data;
|
||||
if (isBusinessJson(payload) && payload.msg) {
|
||||
return payload.msg;
|
||||
@@ -109,6 +109,26 @@ const resolveErrorMessage = async error => {
|
||||
return fallback;
|
||||
};
|
||||
|
||||
/**
|
||||
* 统一消费导入接口响应:成功 JSON / 失败 Excel 流。
|
||||
* 兼容 axios 已把 JSON Blob 解析成对象的情况,禁止再对普通对象调 .text()。
|
||||
*/
|
||||
export const consumeImportResponse = async (res, options = {}) => {
|
||||
const { businessName = '导入', dayjsFormat } = options;
|
||||
if (isExcelResponse(res)) {
|
||||
return {
|
||||
kind: 'excel',
|
||||
blob: res?.data,
|
||||
fileName: `${businessName}导入失败明细${dayjsFormat || ''}.xlsx`,
|
||||
};
|
||||
}
|
||||
const result = isBusinessJson(res?.data) ? res.data : await parseBlobJson(res?.data);
|
||||
if (Number(result?.code) !== 200) {
|
||||
throw new Error(result?.msg || '导入失败');
|
||||
}
|
||||
return { kind: 'success', result };
|
||||
};
|
||||
|
||||
/**
|
||||
* 加工后端返回的导入失败明细 Excel。
|
||||
* 若传入 failDetailDecorator(函数),则动态加载 exceljs 读取 blob 流,
|
||||
@@ -235,7 +255,7 @@ export const handleImportExcel = async (vm, option, column, businessName, onSucc
|
||||
// 拦截器已提示过的错误(如后端 400 返回的业务提示)不再重复弹一次,
|
||||
// 只保留一次清晰的中文提示。
|
||||
if (!error?._notified) {
|
||||
vm.$message.error(await resolveErrorMessage(error));
|
||||
vm.$message.error(await resolveImportErrorMessage(error));
|
||||
}
|
||||
notifyUploadError(option, error);
|
||||
} finally {
|
||||
|
||||
+190
-100
@@ -50,7 +50,7 @@
|
||||
title="角色权限配置"
|
||||
append-to-body
|
||||
v-model="box"
|
||||
width="345px"
|
||||
width="960px"
|
||||
@close="handleDialogClose"
|
||||
>
|
||||
<el-tabs type="border-card">
|
||||
@@ -100,41 +100,26 @@
|
||||
align="middle"
|
||||
style="margin-bottom: 12px; background: #f5f7fa; padding: 6px 10px; border-radius: 4px"
|
||||
>
|
||||
<span style="display: inline-flex; align-items: center">
|
||||
<el-switch
|
||||
v-model="dataScopeLinked"
|
||||
active-text="节点联动"
|
||||
size="small"
|
||||
@change="handleLinkedChange('treeDataScope')"
|
||||
/>
|
||||
<el-tooltip content="开启后勾选父节点会自动勾选所有子节点,关闭则可独立勾选任意节点" placement="top">
|
||||
<el-icon style="margin-left: 4px; color: #909399; cursor: pointer"><el-icon-question-filled /></el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
<el-button-group>
|
||||
<el-button
|
||||
size="small"
|
||||
plain
|
||||
@click="handleSelectAll('treeDataScope', dataScopeGrantList)"
|
||||
>全选</el-button
|
||||
>
|
||||
<el-button
|
||||
size="small"
|
||||
plain
|
||||
@click="handleInvertSelect('treeDataScope', dataScopeGrantList, dataScopeLinked)"
|
||||
>反选</el-button
|
||||
>
|
||||
</el-button-group>
|
||||
<span style="color: #606266; font-size: 13px"
|
||||
>勾选多个菜单后点击「设置」,可批量配置数据权限;设置后菜单旁展示当前范围</span
|
||||
>
|
||||
<el-button type="primary" size="small" @click="openDataScopeSetting">设置</el-button>
|
||||
</el-row>
|
||||
<el-tree
|
||||
:data="dataScopeGrantList"
|
||||
:data="roleDataScopeMenuTree"
|
||||
show-checkbox
|
||||
:check-strictly="!dataScopeLinked"
|
||||
node-key="id"
|
||||
ref="treeDataScope"
|
||||
:default-checked-keys="dataScopeTreeObj"
|
||||
ref="treeRoleDataScope"
|
||||
:props="props"
|
||||
>
|
||||
<template #default="{ data }">
|
||||
<span class="role-data-scope-node">
|
||||
<span>{{ data.title }}</span>
|
||||
<span v-if="getMenuScopeLabel(data.id)" class="role-data-scope-tag"
|
||||
>【{{ getMenuScopeLabel(data.id) }}】</span
|
||||
>
|
||||
</span>
|
||||
</template>
|
||||
</el-tree>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="接口权限">
|
||||
@@ -188,6 +173,45 @@
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
title="数据权限设置"
|
||||
append-to-body
|
||||
v-model="dataScopeSettingBox"
|
||||
width="560px"
|
||||
class="role-data-scope-setting-dialog"
|
||||
>
|
||||
<el-form label-position="right" label-width="auto">
|
||||
<el-form-item label="角色名称">
|
||||
<el-input v-model="dataScopeSetting.roleName" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="数据权限">
|
||||
<el-radio-group v-model="dataScopeSetting.scopeType">
|
||||
<el-radio :value="1">所在部门及下级</el-radio>
|
||||
<el-radio :value="2">全部组织</el-radio>
|
||||
<el-radio :value="3">本人创建</el-radio>
|
||||
<el-radio :value="4">自定义组织</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="Number(dataScopeSetting.scopeType) === 4" label="组织">
|
||||
<el-tree
|
||||
ref="treeCustomDept"
|
||||
:data="deptTree"
|
||||
show-checkbox
|
||||
node-key="id"
|
||||
default-expand-all
|
||||
:props="{ label: 'title', children: 'children' }"
|
||||
style="width: 100%; max-height: 320px; overflow: auto; border: 1px solid #ebeef5; padding: 8px"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="dataScopeSettingBox = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitDataScopeSetting">保存</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</basic-container>
|
||||
</template>
|
||||
|
||||
@@ -196,37 +220,44 @@ import {
|
||||
add,
|
||||
getList,
|
||||
getRole,
|
||||
getRoleAlias,
|
||||
getRoleTreeById,
|
||||
grant,
|
||||
grantTree,
|
||||
remove,
|
||||
update,
|
||||
} from '@/api/system/role';
|
||||
import { getRoleMenuDataScopeTree, submitRoleMenuDataScope } from '@/api/system/roleDataScope';
|
||||
import { getDeptTree } from '@/api/system/dept';
|
||||
import { mapGetters } from 'vuex';
|
||||
import website from '@/config/website';
|
||||
import { validatenull } from '@/utils/validate';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
form: {},
|
||||
box: false,
|
||||
dataScopeSettingBox: false,
|
||||
props: {
|
||||
label: 'title',
|
||||
value: 'key',
|
||||
},
|
||||
menuGrantList: [],
|
||||
dataScopeGrantList: [],
|
||||
apiScopeGrantList: [],
|
||||
apiGrantList: [],
|
||||
menuTreeObj: [],
|
||||
dataScopeTreeObj: [],
|
||||
apiScopeTreeObj: [],
|
||||
menuLinked: false,
|
||||
dataScopeLinked: false,
|
||||
apiScopeLinked: false,
|
||||
currentRoleIds: [],
|
||||
currentRoleName: '',
|
||||
roleDataScopeMenuTree: [],
|
||||
roleDataScopeMap: {},
|
||||
deptTree: [],
|
||||
dataScopeSetting: {
|
||||
roleName: '',
|
||||
scopeType: 2,
|
||||
menuIds: [],
|
||||
},
|
||||
selectionList: [],
|
||||
query: {},
|
||||
loading: true,
|
||||
@@ -309,32 +340,19 @@ export default {
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '角色别名',
|
||||
labelTip: '用于权限校验与标注的属性,可手动录入也可点击现有别名快速录入',
|
||||
label: '角色标识',
|
||||
labelTip: '用于权限校验与标注的属性',
|
||||
prop: 'roleAlias',
|
||||
search: true,
|
||||
span: 24,
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入角色别名',
|
||||
message: '请输入角色标识',
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '现有别名',
|
||||
labelTip: '当前系统已存在的角色别名,点击可快速录入',
|
||||
prop: 'currentAlias',
|
||||
type: 'radio',
|
||||
dicData: [],
|
||||
props: {
|
||||
label: 'roleName',
|
||||
value: 'roleAlias',
|
||||
},
|
||||
hide: true,
|
||||
span: 24,
|
||||
},
|
||||
{
|
||||
label: '角色排序',
|
||||
prop: 'sort',
|
||||
@@ -378,14 +396,6 @@ export default {
|
||||
return ids;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'form.currentAlias'() {
|
||||
const alias = this.form.currentAlias;
|
||||
if (!validatenull(alias)) {
|
||||
this.form.roleAlias = alias;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
initData(roleId) {
|
||||
getRoleTreeById(roleId).then(res => {
|
||||
@@ -452,9 +462,9 @@ export default {
|
||||
},
|
||||
submit() {
|
||||
const menuList = this.$refs.treeMenu.getCheckedKeys();
|
||||
const dataScopeList = this.$refs.treeDataScope.getCheckedKeys();
|
||||
const apiScopeList = this.$refs.treeApiScope.getCheckedKeys();
|
||||
grant(this.currentRoleIds, menuList, dataScopeList, apiScopeList).then(() => {
|
||||
// 数据权限已改为角色菜单配置;传 null 跳过原生 dataScope 更新,避免误清历史配置
|
||||
grant(this.currentRoleIds, menuList, null, apiScopeList).then(() => {
|
||||
this.box = false;
|
||||
this.$message({
|
||||
type: 'success',
|
||||
@@ -465,6 +475,84 @@ export default {
|
||||
this.onLoad(this.page);
|
||||
});
|
||||
},
|
||||
getMenuScopeLabel(menuId) {
|
||||
const scope = this.roleDataScopeMap[String(menuId)];
|
||||
return scope ? scope.scopeTypeName : '';
|
||||
},
|
||||
loadRoleDataScopeTree(roleId) {
|
||||
return getRoleMenuDataScopeTree(roleId).then(res => {
|
||||
const data = res.data.data || {};
|
||||
this.currentRoleName = data.roleName || '';
|
||||
this.roleDataScopeMenuTree = data.menuTree || [];
|
||||
const scopeMap = {};
|
||||
(data.scopes || []).forEach(item => {
|
||||
scopeMap[String(item.menuId)] = item;
|
||||
});
|
||||
this.roleDataScopeMap = scopeMap;
|
||||
});
|
||||
},
|
||||
openDataScopeSetting() {
|
||||
const tree = this.$refs.treeRoleDataScope;
|
||||
if (!tree) {
|
||||
return;
|
||||
}
|
||||
const menuIds = tree.getCheckedKeys();
|
||||
if (!menuIds || menuIds.length === 0) {
|
||||
this.$message.warning('请先勾选要设置的菜单');
|
||||
return;
|
||||
}
|
||||
this.dataScopeSetting = {
|
||||
roleName: this.currentRoleName,
|
||||
scopeType: 2,
|
||||
menuIds,
|
||||
};
|
||||
const loadDept =
|
||||
this.deptTree && this.deptTree.length
|
||||
? Promise.resolve()
|
||||
: getDeptTree(this.userInfo.tenantId).then(res => {
|
||||
this.deptTree = res.data.data || [];
|
||||
});
|
||||
loadDept.then(() => {
|
||||
this.dataScopeSettingBox = true;
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.treeCustomDept) {
|
||||
this.$refs.treeCustomDept.setCheckedKeys([]);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
submitDataScopeSetting() {
|
||||
const roleId = this.currentRoleIds && this.currentRoleIds[0];
|
||||
if (!roleId) {
|
||||
this.$message.warning('角色信息异常,请重新打开权限配置');
|
||||
return;
|
||||
}
|
||||
const scopeType = Number(this.dataScopeSetting.scopeType || 2);
|
||||
let deptIds = [];
|
||||
if (scopeType === 4) {
|
||||
deptIds = this.$refs.treeCustomDept ? this.$refs.treeCustomDept.getCheckedKeys() : [];
|
||||
if (!deptIds.length) {
|
||||
this.$message.warning('自定义组织时请勾选组织');
|
||||
return;
|
||||
}
|
||||
}
|
||||
submitRoleMenuDataScope({
|
||||
roleId,
|
||||
menuIds: this.dataScopeSetting.menuIds,
|
||||
scopeType,
|
||||
deptIds,
|
||||
}).then(() => {
|
||||
this.$message.success('数据权限设置成功');
|
||||
this.dataScopeSettingBox = false;
|
||||
this.loadRoleDataScopeTree(roleId).then(() => {
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.treeRoleDataScope) {
|
||||
this.$refs.treeRoleDataScope.setCheckedKeys([]);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
rowSave(row, done, loading) {
|
||||
add(this.withTenantId(row)).then(
|
||||
() => {
|
||||
@@ -547,10 +635,6 @@ export default {
|
||||
this.form.parentId = '';
|
||||
}
|
||||
}
|
||||
getRoleAlias().then(res => {
|
||||
const column = this.findColumn(this.option.column, 'currentAlias');
|
||||
column.dicData = res.data.data;
|
||||
});
|
||||
done();
|
||||
},
|
||||
handleRole() {
|
||||
@@ -563,58 +647,52 @@ export default {
|
||||
return;
|
||||
}
|
||||
this.menuTreeObj = [];
|
||||
this.dataScopeTreeObj = [];
|
||||
this.apiScopeTreeObj = [];
|
||||
// 保存当前要配置权限的角色ID
|
||||
this.currentRoleIds = this.idsArray;
|
||||
grantTree().then(res => {
|
||||
this.menuGrantList = res.data.data.menu;
|
||||
this.dataScopeGrantList = res.data.data.dataScope;
|
||||
this.apiScopeGrantList = res.data.data.apiScope;
|
||||
getRole(this.ids).then(res => {
|
||||
this.menuTreeObj = res.data.data.menu;
|
||||
this.dataScopeTreeObj = res.data.data.dataScope;
|
||||
this.apiScopeTreeObj = res.data.data.apiScope;
|
||||
this.box = true;
|
||||
this.$nextTick(() => {
|
||||
this.applyCheckedKeys('treeMenu', this.menuTreeObj);
|
||||
this.applyCheckedKeys('treeDataScope', this.dataScopeTreeObj);
|
||||
this.applyCheckedKeys('treeApiScope', this.apiScopeTreeObj);
|
||||
});
|
||||
this.currentRoleName = (this.selectionList[0] && this.selectionList[0].roleName) || '';
|
||||
const roleId = this.currentRoleIds[0];
|
||||
Promise.all([grantTree(), getRole(this.ids), this.loadRoleDataScopeTree(roleId)]).then(results => {
|
||||
const grantRes = results[0];
|
||||
const roleRes = results[1];
|
||||
this.menuGrantList = grantRes.data.data.menu;
|
||||
this.apiScopeGrantList = grantRes.data.data.apiScope;
|
||||
this.menuTreeObj = roleRes.data.data.menu;
|
||||
this.apiScopeTreeObj = roleRes.data.data.apiScope;
|
||||
this.box = true;
|
||||
this.$nextTick(() => {
|
||||
this.applyCheckedKeys('treeMenu', this.menuTreeObj);
|
||||
this.applyCheckedKeys('treeApiScope', this.apiScopeTreeObj);
|
||||
});
|
||||
});
|
||||
},
|
||||
handleRowRole(row) {
|
||||
this.menuTreeObj = [];
|
||||
this.dataScopeTreeObj = [];
|
||||
this.apiScopeTreeObj = [];
|
||||
// 保存当前要配置权限的角色ID(单个行操作)
|
||||
this.currentRoleIds = [row.id];
|
||||
grantTree().then(res => {
|
||||
this.menuGrantList = res.data.data.menu;
|
||||
this.dataScopeGrantList = res.data.data.dataScope;
|
||||
this.apiScopeGrantList = res.data.data.apiScope;
|
||||
getRole(row.id).then(res => {
|
||||
this.menuTreeObj = res.data.data.menu;
|
||||
this.dataScopeTreeObj = res.data.data.dataScope;
|
||||
this.apiScopeTreeObj = res.data.data.apiScope;
|
||||
this.box = true;
|
||||
this.$nextTick(() => {
|
||||
this.applyCheckedKeys('treeMenu', this.menuTreeObj);
|
||||
this.applyCheckedKeys('treeDataScope', this.dataScopeTreeObj);
|
||||
this.applyCheckedKeys('treeApiScope', this.apiScopeTreeObj);
|
||||
});
|
||||
this.currentRoleName = row.roleName || '';
|
||||
Promise.all([grantTree(), getRole(row.id), this.loadRoleDataScopeTree(row.id)]).then(results => {
|
||||
const grantRes = results[0];
|
||||
const roleRes = results[1];
|
||||
this.menuGrantList = grantRes.data.data.menu;
|
||||
this.apiScopeGrantList = grantRes.data.data.apiScope;
|
||||
this.menuTreeObj = roleRes.data.data.menu;
|
||||
this.apiScopeTreeObj = roleRes.data.data.apiScope;
|
||||
this.box = true;
|
||||
this.$nextTick(() => {
|
||||
this.applyCheckedKeys('treeMenu', this.menuTreeObj);
|
||||
this.applyCheckedKeys('treeApiScope', this.apiScopeTreeObj);
|
||||
});
|
||||
});
|
||||
},
|
||||
handleDialogClose() {
|
||||
// 对话框关闭时清空选择状态和权限树数据
|
||||
this.menuTreeObj = [];
|
||||
this.dataScopeTreeObj = [];
|
||||
this.apiScopeTreeObj = [];
|
||||
this.menuLinked = false;
|
||||
this.dataScopeLinked = false;
|
||||
this.apiScopeLinked = false;
|
||||
this.roleDataScopeMenuTree = [];
|
||||
this.roleDataScopeMap = {};
|
||||
this.currentRoleName = '';
|
||||
this.dataScopeSettingBox = false;
|
||||
this.selectionClear();
|
||||
},
|
||||
handleDelete() {
|
||||
@@ -659,3 +737,15 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.role-data-scope-node {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.role-data-scope-tag {
|
||||
color: #409eff;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -294,8 +294,8 @@ export default {
|
||||
query: { id: String(customerId), name: '查看客商档案', view: '1' },
|
||||
});
|
||||
},
|
||||
waybillStatusName(value) { return ({ pending: '待执行', waiting: '待执行', running: '进行中', processing: '进行中', completed: '已完成', cancelled: '已取消' })[value] || value || '-'; },
|
||||
waybillStatusType(value) { return ({ pending: 'info', waiting: 'info', running: 'warning', processing: 'warning', completed: 'success', cancelled: 'danger' })[value] || 'info'; },
|
||||
waybillStatusName(value) { return ({ pending: '待执行', waiting: '待执行', running: '进行中', processing: '进行中', pending_return_confirm: '待回单确认', completed: '已完成', cancelled: '已取消' })[value] || value || '-'; },
|
||||
waybillStatusType(value) { return ({ pending: 'info', waiting: 'info', running: 'warning', processing: 'warning', pending_return_confirm: 'warning', completed: 'success', cancelled: 'danger' })[value] || 'info'; },
|
||||
statusName(value) { return ({ waiting_dispatch: '待调度', dispatching: '调度中', completed: '调度完成', closed: '调度关闭' })[value] || value || '-'; },
|
||||
statusType(value) { return ({ waiting_dispatch: 'warning', dispatching: 'success', completed: 'primary', closed: 'danger' })[value] || 'info'; },
|
||||
},
|
||||
|
||||
@@ -210,7 +210,11 @@
|
||||
@mousedown="() => handleDriverFocus(route)"
|
||||
@focus="() => handleDriverFocus(route)"
|
||||
@select="handleDriverSuggestionSelect"
|
||||
/></el-form-item
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<span>{{ driverOptionText(item) }}</span>
|
||||
</template>
|
||||
</el-autocomplete></el-form-item
|
||||
></el-col>
|
||||
<el-col :span="6"
|
||||
><el-form-item
|
||||
@@ -239,7 +243,11 @@
|
||||
@mousedown="() => handleDriverFocus(route)"
|
||||
@focus="() => handleDriverFocus(route)"
|
||||
@select="handleEscortSuggestionSelect"
|
||||
/></el-form-item
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<span>{{ driverOptionText(item) }}</span>
|
||||
</template>
|
||||
</el-autocomplete></el-form-item
|
||||
></el-col>
|
||||
<el-col v-if="route.carrierType !== '承运商'" :span="6"
|
||||
><el-form-item label="押运人手机号"
|
||||
@@ -316,7 +324,7 @@
|
||||
|
||||
<script>
|
||||
import * as api from '@/api/business/master-order';
|
||||
import { fetchDriversByCarrierOrganizations } from '@/utils/carrier-org-resource';
|
||||
import { fetchDriverCandidates, driverOptionText } from '@/utils/carrier-org-resource';
|
||||
import { formatContractOptionLabel } from '@/utils/contract';
|
||||
import {
|
||||
currencyUnit,
|
||||
@@ -403,6 +411,7 @@ export default {
|
||||
this.applySelfOperatedCarrierDefaults();
|
||||
},
|
||||
methods: {
|
||||
driverOptionText,
|
||||
loadQuantityUnitOptions() {
|
||||
if (this.quantityUnitLoading) return this.quantityUnitLoadPromise;
|
||||
this.quantityUnitLoading = true;
|
||||
@@ -944,12 +953,6 @@ export default {
|
||||
route.escortPhone = '';
|
||||
this.driverOptions = [];
|
||||
},
|
||||
getRouteCarrierFilter(route = {}) {
|
||||
return {
|
||||
carrierId: route.carrierId || '',
|
||||
carrierName: route.carrierName || '',
|
||||
};
|
||||
},
|
||||
carrierOptionValue(item = {}) {
|
||||
return item.contractId || item.id || item.value || item.carrierName || '';
|
||||
},
|
||||
@@ -1026,36 +1029,22 @@ export default {
|
||||
this.activeSuggestRoute = route;
|
||||
},
|
||||
async loadDriverOptions(route = {}, keyword = '') {
|
||||
const carrier = this.getRouteCarrierFilter(route);
|
||||
if (!carrier.carrierId && !carrier.carrierName) {
|
||||
this.driverOptions = [];
|
||||
return [];
|
||||
}
|
||||
this.driverLoading = true;
|
||||
try {
|
||||
this.driverOptions = await fetchDriversByCarrierOrganizations(
|
||||
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' },
|
||||
carrier
|
||||
);
|
||||
this.driverOptions = await fetchDriverCandidates({
|
||||
size: 20,
|
||||
...(keyword ? { driverName: keyword } : {}),
|
||||
posts: '司机',
|
||||
});
|
||||
return this.driverOptions;
|
||||
} finally {
|
||||
this.driverLoading = false;
|
||||
}
|
||||
},
|
||||
fetchDriverSuggestions(queryString, callback) {
|
||||
const route = this.activeSuggestRoute || {};
|
||||
const carrier = this.getRouteCarrierFilter(route);
|
||||
const keyword = String(queryString || '').trim();
|
||||
if (!carrier.carrierId && !carrier.carrierName) {
|
||||
callback([]);
|
||||
if (!keyword) this.$message.warning('请先选择承运商');
|
||||
return;
|
||||
}
|
||||
this.driverLoading = true;
|
||||
fetchDriversByCarrierOrganizations(
|
||||
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' },
|
||||
carrier
|
||||
)
|
||||
fetchDriverCandidates({ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' })
|
||||
.then(records => {
|
||||
this.driverOptions = records;
|
||||
callback(
|
||||
@@ -1080,19 +1069,13 @@ export default {
|
||||
if (drivingVehicle) route.vehicleNo = drivingVehicle;
|
||||
},
|
||||
fetchEscortSuggestions(queryString, callback) {
|
||||
const route = this.activeSuggestRoute || {};
|
||||
const keyword = String(queryString || '').trim();
|
||||
const carrier = this.getRouteCarrierFilter(route);
|
||||
if (!carrier.carrierId && !carrier.carrierName) {
|
||||
callback([]);
|
||||
if (!keyword) this.$message.warning('请先选择承运商');
|
||||
return;
|
||||
}
|
||||
this.escortLoading = true;
|
||||
fetchDriversByCarrierOrganizations(
|
||||
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '押运员' },
|
||||
carrier
|
||||
)
|
||||
fetchDriverCandidates({
|
||||
size: 20,
|
||||
...(keyword ? { driverName: keyword } : {}),
|
||||
posts: '押运员',
|
||||
})
|
||||
.then(records => {
|
||||
callback(
|
||||
records.map(item => ({
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
<div class="route-node-form">
|
||||
<el-form-item label="途经地" required class="route-address-form-item"
|
||||
><div class="route-address-inputs"
|
||||
><el-input v-if="isNonRoadAddress(`route-${index}`)" v-model="route.departureName" class="route-address-name" readonly :disabled="isBasicRouteReadonly || !route.transportType" placeholder="请先选择运输方式" /><el-cascader v-else v-model="addressRegionPaths[`route-${index}`]" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="isBasicRouteReadonly || !route.transportType" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange(`route-${index}`, value)" /><el-input v-model="route.departureAddress" class="route-address-detail" readonly :disabled="isBasicRouteReadonly || !route.transportType" placeholder="请先选择运输方式" @click="handleAddressDetailClick(`route-${index}`)" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" :disabled="isBasicRouteReadonly || !route.transportType" @click="openCommonAddress(`route-${index}`)" /></el-tooltip></div
|
||||
><el-input v-if="isNonRoadAddress(`route-${index}`)" v-model="route.departureName" class="route-address-name" readonly :disabled="isBasicRouteReadonly" placeholder="请选择站点" /><el-cascader v-else v-model="addressRegionPaths[`route-${index}`]" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="isBasicRouteReadonly" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange(`route-${index}`, value)" /><el-input v-model="route.departureAddress" class="route-address-detail" readonly :disabled="isBasicRouteReadonly" placeholder="请输入详细地址" @click="handleAddressDetailClick(`route-${index}`)" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" :disabled="isBasicRouteReadonly" @click="openCommonAddress(`route-${index}`)" /></el-tooltip></div
|
||||
></el-form-item>
|
||||
<el-form-item label="联系人" class="route-contact-form-item"><el-input v-model="route.departureContact" :disabled="isBasicRouteReadonly" placeholder="请输入" /></el-form-item>
|
||||
<el-form-item label="联系方式" class="route-phone-form-item"><div class="route-contact-actions"><el-input v-model="route.departurePhone" :disabled="isBasicRouteReadonly" placeholder="请输入" /><template v-if="!isBasicRouteReadonly"><el-tooltip v-if="index === form.routes.length - 1" content="新增途经点" placement="top"><el-button class="route-action-button" type="primary" link @click="addRoute"><el-icon><CirclePlus /></el-icon></el-button></el-tooltip><el-tooltip content="删除途经点" placement="top"><el-button class="route-action-button" type="danger" link @click="removeRoute(index)"><el-icon><Remove /></el-icon></el-button></el-tooltip></template></div></el-form-item>
|
||||
@@ -111,7 +111,7 @@
|
||||
<div class="route-node-form">
|
||||
<el-form-item label="收货地" required class="route-address-form-item"
|
||||
><div class="route-address-inputs"
|
||||
><el-input v-if="isNonRoadAddress('arrival')" v-model="form.arrivalName" class="route-address-name" readonly :disabled="isBasicRouteReadonly || !form.finalTransportType" placeholder="请先选择运输方式" /><el-cascader v-else v-model="addressRegionPaths.arrival" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="isBasicRouteReadonly || !form.finalTransportType" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange('arrival', value)" /><el-input v-model="form.arrivalAddress" class="route-address-detail" readonly :disabled="isBasicRouteReadonly || !form.finalTransportType" placeholder="请先选择运输方式" @click="handleAddressDetailClick('arrival')" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" :disabled="isBasicRouteReadonly || !form.finalTransportType" @click="openCommonAddress('arrival')" /></el-tooltip></div
|
||||
><el-input v-if="isNonRoadAddress('arrival')" v-model="form.arrivalName" class="route-address-name" readonly :disabled="isBasicRouteReadonly" placeholder="请选择站点" /><el-cascader v-else v-model="addressRegionPaths.arrival" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="isBasicRouteReadonly" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange('arrival', value)" /><el-input v-model="form.arrivalAddress" class="route-address-detail" readonly :disabled="isBasicRouteReadonly" placeholder="请输入详细地址" @click="handleAddressDetailClick('arrival')" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" :disabled="isBasicRouteReadonly" @click="openCommonAddress('arrival')" /></el-tooltip></div
|
||||
></el-form-item>
|
||||
<el-form-item label="联系人" class="route-contact-form-item"
|
||||
><el-input v-model="form.arrivalContact" :disabled="isBasicRouteReadonly" placeholder="请输入" /></el-form-item>
|
||||
@@ -714,7 +714,7 @@ export default {
|
||||
'msg',
|
||||
'zip',
|
||||
],
|
||||
transports: ['公路运输', '铁路运输', '水路运输', '航空运输'],
|
||||
transports: ['铁路运输', '水路运输', '航空运输', '公路运输'],
|
||||
quantityUnitOptions: [],
|
||||
quantityUnitLoading: false,
|
||||
form: {
|
||||
@@ -798,6 +798,7 @@ export default {
|
||||
methods: {
|
||||
formatContractOptionLabel,
|
||||
defaultQuantityUnit() {
|
||||
if (this.quantityUnitOptions.includes('吨')) return '吨';
|
||||
return this.quantityUnitOptions[0] || '吨';
|
||||
},
|
||||
loadQuantityUnitOptions() {
|
||||
@@ -1164,22 +1165,30 @@ export default {
|
||||
const field = this.addressModel(target);
|
||||
if (!field) return false;
|
||||
const siteCode = String(field.model[field.siteCode] || '').trim();
|
||||
// 地址展示模式由实际选择的数据决定:有站点编码表示非公路站点名称,
|
||||
// 没有站点编码则保留省市区级联,避免切换运输方式时改写已有地址。
|
||||
// 展示模式由已选数据决定:有站点编码则显示站点名,否则省市区;暂不按运输方式强制站点限制。
|
||||
return Boolean(siteCode && siteCode !== '/');
|
||||
},
|
||||
hasAddressValue(target) {
|
||||
const field = this.addressModel(target);
|
||||
if (!field) return false;
|
||||
return Boolean(
|
||||
String(field.model[field.address] || '').trim() || String(field.model[field.name] || '').trim()
|
||||
);
|
||||
},
|
||||
tipEmptyAddressDetail(target) {
|
||||
// 替代原「请先选择运输方式」placeholder:空地址点击时 toast,已有值不再提示。
|
||||
if (!this.hasAddressValue(target)) {
|
||||
this.$message.warning('请输入详细地址');
|
||||
}
|
||||
},
|
||||
resolveAddressRegionCode(row = {}) {
|
||||
return row.regionCode || row.districtCode || row.areaCode || row.adcode || '';
|
||||
},
|
||||
openCommonAddress(target) {
|
||||
if (this.isBasicRouteReadonly) return;
|
||||
this.tipEmptyAddressDetail(target);
|
||||
this.addressTarget = target;
|
||||
if (this.addressTransportMode(target) !== 'road') {
|
||||
this.stationQuery = {};
|
||||
this.stationPage.current = 1;
|
||||
this.stationDialogVisible = true;
|
||||
return;
|
||||
}
|
||||
// 暂不按运输方式限制站点:统一走常用地址选择。
|
||||
this.addressQuery = {};
|
||||
this.addressPage.current = 1;
|
||||
this.addressDialogVisible = true;
|
||||
@@ -1279,18 +1288,11 @@ export default {
|
||||
},
|
||||
handleAddressDetailClick(target) {
|
||||
if (this.isBasicRouteReadonly) return;
|
||||
if (this.addressTransportMode(target) !== 'road') {
|
||||
this.openCommonAddress(target);
|
||||
return;
|
||||
}
|
||||
this.tipEmptyAddressDetail(target);
|
||||
this.openAddressMap(target);
|
||||
},
|
||||
openAddressMap(target) {
|
||||
if (this.isBasicRouteReadonly) return;
|
||||
if (this.addressTransportMode(target) !== 'road') {
|
||||
this.$message.info('非公路运输地址不能使用地图选择');
|
||||
return;
|
||||
}
|
||||
const field = this.addressModel(target);
|
||||
if (!field) return;
|
||||
this.mapTarget = target;
|
||||
@@ -1690,7 +1692,11 @@ export default {
|
||||
'',
|
||||
cargoTypePath,
|
||||
quantity: row.quantity || '',
|
||||
quantityUnit: row.quantityUnit || this.defaultQuantityUnit(),
|
||||
// 常用货物单位若为空或「个」,新建总单统一默认「吨」
|
||||
quantityUnit:
|
||||
!row.quantityUnit || String(row.quantityUnit).trim() === '个'
|
||||
? this.defaultQuantityUnit()
|
||||
: row.quantityUnit,
|
||||
packageType: row.packageType || row.package || '',
|
||||
brand: row.brand || '',
|
||||
specification: row.specification || row.spec || '',
|
||||
@@ -1775,17 +1781,21 @@ export default {
|
||||
return formatUpdateUserName(row);
|
||||
},
|
||||
selectRoute(route) {
|
||||
// 先写入运输方式(会触发地点清空),再回填线路地址,避免被清空逻辑覆盖。
|
||||
this.form.finalTransportType = '公路运输';
|
||||
if (!this.form.routes.length) this.addRoute();
|
||||
if (this.form.routes[0]) this.form.routes[0].transportType = '公路运输';
|
||||
this.form.departureName = route.departureName || '';
|
||||
this.form.departureAddress = route.departureAddress || '';
|
||||
this.form.departureContact = route.departureContact || '';
|
||||
this.form.departurePhone = route.departurePhone || '';
|
||||
this.form.departureSiteCode = '';
|
||||
this.form.arrivalName = route.arrivalName || '';
|
||||
this.form.arrivalAddress = route.arrivalAddress || '';
|
||||
this.form.arrivalContact = route.arrivalContact || '';
|
||||
this.form.arrivalPhone = route.arrivalPhone || '';
|
||||
this.form.arrivalSiteCode = '';
|
||||
this.syncAddressRegionPaths();
|
||||
this.form.finalTransportType = '公路运输';
|
||||
if (!this.form.routes.length) this.addRoute();
|
||||
this.routeDialogVisible = false;
|
||||
},
|
||||
createRoute(index = this.form.routes.length) {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
:class="{ 'punch-record-photos__grid--edit': editable }"
|
||||
>
|
||||
<div
|
||||
v-for="(item, groupIndex) in group.items"
|
||||
v-for="item in group.items"
|
||||
:key="`${item.photo.url}-${item.index}`"
|
||||
class="punch-record-photos__item"
|
||||
@click="handlePreview(item.index)"
|
||||
@@ -23,8 +23,6 @@
|
||||
:lazy="lazy"
|
||||
fit="cover"
|
||||
/>
|
||||
<!-- 主图 = 该类型的第一张,按上传顺序取;没有独立字段,也不可切换 -->
|
||||
<div v-if="groupIndex === 0" class="punch-record-photos__badge">主图</div>
|
||||
<el-link
|
||||
v-if="editable"
|
||||
class="punch-record-photos__remove"
|
||||
@@ -59,7 +57,7 @@
|
||||
import { computed } from 'vue';
|
||||
|
||||
// 打卡照片是每个打卡节点必有的一个照片类型,不由过程配置的凭证类型决定;
|
||||
// 它与各凭证类型各自最多 9 张,同一类型下第一张即界面上的「主图」。
|
||||
// 它与各凭证类型各自最多 9 张。
|
||||
const PUNCH_PHOTO_TYPE = '打卡照片';
|
||||
// 照片没带类型时的兜底名称,与后端落库时的兜底口径一致
|
||||
const PHOTO_TYPE_FALLBACK = '凭证';
|
||||
@@ -252,18 +250,6 @@ const countText = group =>
|
||||
}
|
||||
}
|
||||
|
||||
&__badge {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 0 6px;
|
||||
border-radius: 4px 0 4px 0;
|
||||
background: rgba(64, 158, 255, 0.9);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&__remove {
|
||||
align-self: center;
|
||||
font-size: 12px;
|
||||
|
||||
@@ -1870,7 +1870,11 @@
|
||||
:fetch-suggestions="fetchTaskDriverSuggestions"
|
||||
:loading="taskDriverLoading"
|
||||
@select="item => handleTaskDriverSelect('dispatch', item)"
|
||||
/>
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<span>{{ driverOptionText(item) }}</span>
|
||||
</template>
|
||||
</el-autocomplete>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
:label="dispatchItemForm.carrierType === '承运商' ? '手机号' : '司机手机号'"
|
||||
@@ -1893,7 +1897,11 @@
|
||||
:fetch-suggestions="fetchTaskEscortSuggestions"
|
||||
:loading="taskEscortLoading"
|
||||
@select="handleTaskEscortSelect"
|
||||
/>
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<span>{{ driverOptionText(item) }}</span>
|
||||
</template>
|
||||
</el-autocomplete>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="dispatchItemForm.carrierType !== '承运商'" label="押运人手机号">
|
||||
<el-input v-model="dispatchItemForm.escortPhone" placeholder="请输入" />
|
||||
@@ -2760,7 +2768,7 @@ import {
|
||||
getList as getProjectList,
|
||||
} from '@/api/business/project-apply';
|
||||
import { getList as getCommonRouteList } from '@/api/business/common-route';
|
||||
import { fetchDriversByCarrierOrganizations } from '@/utils/carrier-org-resource';
|
||||
import { fetchDriverCandidates, driverOptionText } from '@/utils/carrier-org-resource';
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
import { getDictionary as getSystemDictionary } from '@/api/system/dict';
|
||||
import { addressTypeOptions } from '@/option/base/common-address';
|
||||
@@ -3809,6 +3817,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
formatContractOptionLabel,
|
||||
driverOptionText,
|
||||
initStandaloneFormPage() {
|
||||
if (!this.isStandaloneFormPage) return;
|
||||
const mode = this.$route.query.mode === 'edit' ? 'edit' : 'add';
|
||||
@@ -4178,6 +4187,7 @@ export default {
|
||||
waiting: ['待执行', 'is-pending'],
|
||||
processing: ['进行中', 'is-processing'],
|
||||
running: ['进行中', 'is-processing'],
|
||||
pending_return_confirm: ['待回单确认', 'is-pending'],
|
||||
completed: ['已完成', 'is-completed'],
|
||||
cancelled: ['已取消', 'is-cancelled'],
|
||||
};
|
||||
@@ -4988,16 +4998,8 @@ export default {
|
||||
},
|
||||
fetchTaskDriverSuggestions(queryString, callback) {
|
||||
const keyword = String(queryString || '').trim();
|
||||
const carrier = this.getDispatchCarrierFilter();
|
||||
if (!carrier.carrierId && !carrier.carrierName) {
|
||||
callback([]);
|
||||
return;
|
||||
}
|
||||
this.taskDriverLoading = true;
|
||||
fetchDriversByCarrierOrganizations(
|
||||
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' },
|
||||
carrier
|
||||
)
|
||||
fetchDriverCandidates({ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' })
|
||||
.then(records => {
|
||||
this.taskDriverOptions = records;
|
||||
callback(
|
||||
@@ -5017,16 +5019,12 @@ export default {
|
||||
},
|
||||
fetchTaskEscortSuggestions(queryString, callback) {
|
||||
const keyword = String(queryString || '').trim();
|
||||
const carrier = this.getDispatchCarrierFilter();
|
||||
if (!carrier.carrierId && !carrier.carrierName) {
|
||||
callback([]);
|
||||
return;
|
||||
}
|
||||
this.taskEscortLoading = true;
|
||||
fetchDriversByCarrierOrganizations(
|
||||
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '押运员' },
|
||||
carrier
|
||||
)
|
||||
fetchDriverCandidates({
|
||||
size: 20,
|
||||
...(keyword ? { driverName: keyword } : {}),
|
||||
posts: '押运员',
|
||||
})
|
||||
.then(records => {
|
||||
this.taskEscortOptions = records;
|
||||
callback(records.map(item => ({ ...item, value: item.driverName || item.name || '' })));
|
||||
@@ -8044,12 +8042,6 @@ export default {
|
||||
this.taskDriverOptions = [];
|
||||
this.taskEscortOptions = [];
|
||||
},
|
||||
getDispatchCarrierFilter() {
|
||||
return {
|
||||
carrierId: this.dispatchItemForm.carrierId || '',
|
||||
carrierName: this.dispatchItemForm.carrierName || '',
|
||||
};
|
||||
},
|
||||
handleDispatchCargoQuantityInput(row, value) {
|
||||
const text = String(value || '').replace(/[^\d.]/g, '');
|
||||
const parts = text.split('.');
|
||||
|
||||
@@ -1381,11 +1381,23 @@ const formatOptionalNumber = value => {
|
||||
const normalized = normalizeOptionalImportNumber(value);
|
||||
return normalized === null || normalized === undefined ? '' : normalized;
|
||||
};
|
||||
// 明细接口返回运单实体字段 startDate/endDate/estimatedStartTime/estimatedEndTime,
|
||||
// 而明细表与编辑回填按导入列字段名 actualStartDate 等读取,重载后在此对齐,否则实际/预计时间列为空。
|
||||
const detailDateAliases = {
|
||||
actualStartDate: 'startDate',
|
||||
actualEndDate: 'endDate',
|
||||
planStartDate: 'estimatedStartTime',
|
||||
planEndDate: 'estimatedEndTime',
|
||||
};
|
||||
const normalizeDetailRow = row => {
|
||||
const next = { ...(row || {}) };
|
||||
OPTIONAL_NUMBER_PROPS.forEach(prop => {
|
||||
next[prop] = formatOptionalNumber(next[prop]);
|
||||
});
|
||||
Object.entries(detailDateAliases).forEach(([prop, source]) => {
|
||||
if (String(next[prop] ?? '').trim()) return;
|
||||
if (String(next[source] ?? '').trim()) next[prop] = next[source];
|
||||
});
|
||||
return next;
|
||||
};
|
||||
const normalizeImportRow = row => ({
|
||||
|
||||
@@ -1200,7 +1200,11 @@
|
||||
:loading="taskDriverLoading"
|
||||
:disabled="dialogReadonly"
|
||||
@select="handleTaskDriverSelect"
|
||||
/>
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<span>{{ driverOptionText(item) }}</span>
|
||||
</template>
|
||||
</el-autocomplete>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="isRoadTransport"
|
||||
@@ -1279,7 +1283,11 @@
|
||||
:loading="taskEscortLoading"
|
||||
:disabled="dialogReadonly"
|
||||
@select="handleTaskEscortSelect"
|
||||
/>
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<span>{{ driverOptionText(item) }}</span>
|
||||
</template>
|
||||
</el-autocomplete>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="showRoadEscortFields" label="押运人手机号">
|
||||
<el-input
|
||||
@@ -1594,6 +1602,13 @@
|
||||
<el-link type="primary" v-if="canComplete(row)" @click="handleAction('complete', row)">
|
||||
{{ completeActionLabel }}
|
||||
</el-link>
|
||||
<el-link
|
||||
type="primary"
|
||||
v-if="canConfirmReturn(row)"
|
||||
@click="handleAction('confirmReturn', row)"
|
||||
>
|
||||
确认回单
|
||||
</el-link>
|
||||
<el-link type="primary" v-if="canMaintainMileage(row)" @click="openMileageDialog(row)">
|
||||
维护里程
|
||||
</el-link>
|
||||
@@ -1653,7 +1668,11 @@
|
||||
style="width: 100%"
|
||||
@select="handleReassignDriverSelect"
|
||||
@change="handleReassignDriverChange"
|
||||
/>
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<span>{{ driverOptionText(item) }}</span>
|
||||
</template>
|
||||
</el-autocomplete>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
label="手机号"
|
||||
@@ -2252,6 +2271,13 @@
|
||||
<span v-if="record.volume">体积 {{ record.volume }}方</span>
|
||||
<span v-if="record.quantity">数量 {{ record.quantity }}</span>
|
||||
</div>
|
||||
<!-- 里程只挂在末位节点上(needMileage 由后端下发),显示的是运单里程 -->
|
||||
<div
|
||||
v-if="record.needMileage && Number(record.mileage) > 0"
|
||||
class="waybill-manage-page__punch-record-line"
|
||||
>
|
||||
里程 {{ record.mileage }} 公里
|
||||
</div>
|
||||
<div
|
||||
v-if="record.remark"
|
||||
class="waybill-manage-page__punch-record-line"
|
||||
@@ -2848,6 +2874,20 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<!-- 里程只在末位打卡节点出现:后端按「末位 + 计费规则含里程」下发 needMileage,前端不自己推 -->
|
||||
<el-col v-if="waybillPunchEditMileageVisible" :span="8">
|
||||
<el-form-item label="里程">
|
||||
<el-input
|
||||
v-model="waybillPunchEditForm.mileage"
|
||||
maxlength="10"
|
||||
:disabled="waybillPunchEditMileageLocked"
|
||||
placeholder="请输入"
|
||||
@input="handleWaybillPunchMileageInput"
|
||||
>
|
||||
<template #append>公里</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="备注">
|
||||
<el-input
|
||||
@@ -3683,6 +3723,7 @@ import {
|
||||
getVoucherImages as getProcessConfigVoucherImages,
|
||||
} from '@/api/business/process-config';
|
||||
import {
|
||||
getList as getWaybillList,
|
||||
getPunchRecords as getWaybillPunchRecords,
|
||||
locateVehicle,
|
||||
savePunchRecord,
|
||||
@@ -3692,9 +3733,7 @@ import VoucherExecutionDetail from './voucher-execution-detail.vue';
|
||||
import PunchRecordPhotos from './punch-record-photos.vue';
|
||||
import AddressMapPicker from '@/components/address-map-picker/main.vue';
|
||||
import { getList as getDriverList } from '@/api/transportCapacity/driver';
|
||||
import {
|
||||
fetchDriversByCarrierOrganizations,
|
||||
} from '@/utils/carrier-org-resource';
|
||||
import { fetchDriverCandidates, driverOptionText } from '@/utils/carrier-org-resource';
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
import { getDictionary as getSystemDictionary } from '@/api/system/dict';
|
||||
import { addressTypeOptions } from '@/option/base/common-address';
|
||||
@@ -4011,6 +4050,7 @@ export default {
|
||||
weight: '',
|
||||
volume: '',
|
||||
quantity: '',
|
||||
mileage: '',
|
||||
remark: '',
|
||||
photos: [],
|
||||
},
|
||||
@@ -4370,9 +4410,15 @@ export default {
|
||||
addBtn: this.canCreate,
|
||||
};
|
||||
},
|
||||
// 打卡记录编辑入口与运单编辑同权限:能改运单的人才能订正它的打卡流水
|
||||
// 打卡记录编辑入口与运单编辑同权限:能改运单的人才能订正它的打卡流水。
|
||||
// 时间轴的「编辑打卡 / 补卡」链接是本页唯一写入入口,提交固定 punched=true。
|
||||
// 取消打卡不在本页;待回单确认的运单不显示该链接。
|
||||
canEditWaybillPunchRecord() {
|
||||
return this.hasPermission(`${this.config.permission}_edit`) && !this.readonlyScope;
|
||||
return (
|
||||
this.hasPermission(`${this.config.permission}_edit`) &&
|
||||
!this.readonlyScope &&
|
||||
this.statusValue(this.detailRow) !== 'pending_return_confirm'
|
||||
);
|
||||
},
|
||||
waybillPunchEditNode() {
|
||||
return this.findWaybillPunchEditNodeConfig(this.waybillPunchEditRecord);
|
||||
@@ -4407,6 +4453,15 @@ export default {
|
||||
waybillPunchEditPunchDisabled() {
|
||||
return this.waybillPunchEditNode?.punch === false;
|
||||
},
|
||||
// 里程:后端下发 needMileage 才出现(末位打卡节点 + 计费规则含里程);在途打卡不采集里程
|
||||
waybillPunchEditMileageVisible() {
|
||||
const record = this.waybillPunchEditRecord || {};
|
||||
return record.needMileage === true && record.type !== 'enroute';
|
||||
},
|
||||
// 已生成结算单的运单里程冻结:输入框置灰,改动由后端拒绝
|
||||
waybillPunchEditMileageLocked() {
|
||||
return this.waybillPunchEditRecord?.mileageLocked === true;
|
||||
},
|
||||
waybillPunchEditRules() {
|
||||
return { punchTime: [{ required: true, message: '请选择打卡时间', trigger: 'change' }] };
|
||||
},
|
||||
@@ -4800,6 +4855,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
formatContractOptionLabel,
|
||||
driverOptionText,
|
||||
parseRouteChangeContent,
|
||||
// 页面被 keep-alive 缓存/卸载时调用,关闭本页所有 append-to-body 的弹窗,
|
||||
// 避免 Teleport 出去的 DOM 残留在 body 上盖住后续页面(详见 data 里的说明)。
|
||||
@@ -5099,7 +5155,8 @@ export default {
|
||||
return false;
|
||||
}
|
||||
const status = this.statusValue(row);
|
||||
return status === 'pending';
|
||||
// 待回单确认可取消,取消不生成应收应付
|
||||
return status === 'pending' || status === 'pending_return_confirm';
|
||||
},
|
||||
canComplete(row) {
|
||||
if (
|
||||
@@ -5111,7 +5168,17 @@ export default {
|
||||
return false;
|
||||
}
|
||||
const status = this.statusValue(row);
|
||||
return status === 'processing' || status === 'running';
|
||||
// 待回单确认也可直接完成,不经确认回单
|
||||
return status === 'processing' || status === 'running' || status === 'pending_return_confirm';
|
||||
},
|
||||
canConfirmReturn(row) {
|
||||
return (
|
||||
this.hasAction('confirmReturn') &&
|
||||
this.hasPermission(`${this.config.permission}_complete`) &&
|
||||
!this.readonlyScope &&
|
||||
!row.readonly &&
|
||||
this.statusValue(row) === 'pending_return_confirm'
|
||||
);
|
||||
},
|
||||
canMaintainMileage(row) {
|
||||
return (
|
||||
@@ -5926,6 +5993,8 @@ export default {
|
||||
weight: record.weight || '',
|
||||
volume: record.volume || '',
|
||||
quantity: record.quantity || '',
|
||||
// 里程只有正整数口径,后端若仍下发 "200.00" 会过不了提交时的正整数校验
|
||||
mileage: Number(record.mileage) > 0 ? String(Number(record.mileage)) : '',
|
||||
remark: record.remark || '',
|
||||
photos: (record.photos || [])
|
||||
.filter(photo => photo?.url)
|
||||
@@ -6001,10 +6070,22 @@ export default {
|
||||
photos.splice(index, 1);
|
||||
this.waybillPunchEditForm.photos = photos;
|
||||
},
|
||||
// 里程只收整数:非数字直接丢掉,长度与「维护里程」一致(≤10 位)
|
||||
handleWaybillPunchMileageInput(value) {
|
||||
this.waybillPunchEditForm.mileage = String(value || '')
|
||||
.replace(/\D/g, '')
|
||||
.slice(0, 10);
|
||||
},
|
||||
async submitWaybillPunchRecordEdit() {
|
||||
await this.$refs.waybillPunchRecordEditFormRef.validate();
|
||||
const record = this.waybillPunchEditRecord;
|
||||
const form = this.waybillPunchEditForm;
|
||||
// 管理端里程选填:留空表示不改动运单里程,所以只校验填了值的情况
|
||||
const mileageText = String(form.mileage || '').trim();
|
||||
if (this.waybillPunchEditMileageVisible && mileageText && !/^[1-9]\d{0,9}$/.test(mileageText)) {
|
||||
this.$message.warning('里程必须为不超过10位的正整数');
|
||||
return;
|
||||
}
|
||||
const submit =
|
||||
typeof this.api?.savePunchRecord === 'function' ? this.api.savePunchRecord : savePunchRecord;
|
||||
this.waybillPunchRecordEditDialog.submitting = true;
|
||||
@@ -6023,6 +6104,8 @@ export default {
|
||||
weight: form.weight,
|
||||
volume: form.volume,
|
||||
quantity: form.quantity,
|
||||
// 不传代表不改;后端按 null 处理
|
||||
mileage: mileageText ? Number(mileageText) : undefined,
|
||||
remark: form.remark,
|
||||
photos: form.photos.map(photo => ({ url: photo.url, type: photo.type })),
|
||||
});
|
||||
@@ -6543,6 +6626,9 @@ export default {
|
||||
if (status === 'completed') {
|
||||
return 'primary';
|
||||
}
|
||||
if (status === 'pending_return_confirm') {
|
||||
return 'warning';
|
||||
}
|
||||
return 'warning';
|
||||
},
|
||||
normalizeRow(row, saveAsDraft = false) {
|
||||
@@ -7942,19 +8028,10 @@ export default {
|
||||
this.taskDriverOptions = [];
|
||||
this.taskEscortOptions = [];
|
||||
},
|
||||
getTaskCarrierFilter() {
|
||||
return {
|
||||
carrierId: this.form.carrierId || '',
|
||||
carrierName: this.form.carrierName || '',
|
||||
};
|
||||
},
|
||||
loadTaskDriverOptions() {
|
||||
if (!this.taskInfoFormEnabled || this.taskDriverLoading) return Promise.resolve([]);
|
||||
this.taskDriverLoading = true;
|
||||
return fetchDriversByCarrierOrganizations(
|
||||
{ size: 9999, posts: '司机' },
|
||||
this.getTaskCarrierFilter()
|
||||
)
|
||||
return fetchDriverCandidates({ size: 9999, posts: '司机' })
|
||||
.then(records => {
|
||||
this.taskDriverOptions = records;
|
||||
return this.taskDriverOptions;
|
||||
@@ -7965,16 +8042,8 @@ export default {
|
||||
},
|
||||
fetchTaskDriverSuggestions(queryString, callback) {
|
||||
const keyword = String(queryString || '').trim();
|
||||
const carrier = this.getTaskCarrierFilter();
|
||||
if (!carrier.carrierId && !carrier.carrierName) {
|
||||
callback([]);
|
||||
return;
|
||||
}
|
||||
this.taskDriverLoading = true;
|
||||
fetchDriversByCarrierOrganizations(
|
||||
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' },
|
||||
carrier
|
||||
)
|
||||
fetchDriverCandidates({ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' })
|
||||
.then(records => {
|
||||
this.taskDriverOptions = records;
|
||||
callback(
|
||||
@@ -7994,16 +8063,12 @@ export default {
|
||||
},
|
||||
fetchTaskEscortSuggestions(queryString, callback) {
|
||||
const keyword = String(queryString || '').trim();
|
||||
const carrier = this.getTaskCarrierFilter();
|
||||
if (!carrier.carrierId && !carrier.carrierName) {
|
||||
callback([]);
|
||||
return;
|
||||
}
|
||||
this.taskEscortLoading = true;
|
||||
fetchDriversByCarrierOrganizations(
|
||||
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '押运员' },
|
||||
carrier
|
||||
)
|
||||
fetchDriverCandidates({
|
||||
size: 20,
|
||||
...(keyword ? { driverName: keyword } : {}),
|
||||
posts: '押运员',
|
||||
})
|
||||
.then(records => {
|
||||
this.taskEscortOptions = records;
|
||||
callback(records.map(item => ({ ...item, value: item.driverName || item.name || '' })));
|
||||
@@ -10150,8 +10215,14 @@ export default {
|
||||
cancel: '取消',
|
||||
complete: this.completeActionLabel,
|
||||
reassign: '重新派单',
|
||||
confirmReturn: '确认回单',
|
||||
}[action];
|
||||
this.$confirm(`确定${actionName}该${this.config.title}?`, '提示', {
|
||||
// 确认回单是终态动作(本轮不做撤销),把后果写在提示里,别让人以为还能退回
|
||||
const confirmText =
|
||||
action === 'confirmReturn'
|
||||
? '确认回单后该运单将置为已完成,且不可撤销。是否继续?'
|
||||
: `确定${actionName}该${this.config.title}?`;
|
||||
this.$confirm(confirmText, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
@@ -10204,20 +10275,8 @@ export default {
|
||||
},
|
||||
fetchReassignDriverSuggestions(queryString, callback) {
|
||||
const keyword = String(queryString || '').trim();
|
||||
const row = this.reassignDrawer.row || {};
|
||||
const carrier = {
|
||||
carrierId: row.carrierId || '',
|
||||
carrierName: row.carrierName || '',
|
||||
};
|
||||
if (!carrier.carrierId && !carrier.carrierName) {
|
||||
callback([]);
|
||||
return;
|
||||
}
|
||||
this.reassignDrawer.driverLoading = true;
|
||||
fetchDriversByCarrierOrganizations(
|
||||
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' },
|
||||
carrier
|
||||
)
|
||||
fetchDriverCandidates({ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' })
|
||||
.then(records => {
|
||||
this.reassignDriverOptions = records;
|
||||
callback(
|
||||
@@ -10356,7 +10415,14 @@ export default {
|
||||
this.$message.warning('请选择至少一条数据');
|
||||
return;
|
||||
}
|
||||
this.$confirm('确定完成选中的运单?', '提示', {
|
||||
const pendingReturnCount = this.selectionList.filter(
|
||||
row => this.statusValue(row) === 'pending_return_confirm'
|
||||
).length;
|
||||
const tip =
|
||||
pendingReturnCount > 0
|
||||
? `确定完成选中的运单?其中 ${pendingReturnCount} 条为待回单确认,确认后将转为已完成。`
|
||||
: '确定完成选中的运单?';
|
||||
this.$confirm(tip, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
@@ -10370,25 +10436,38 @@ export default {
|
||||
this.onLoad(this.page, this.query);
|
||||
});
|
||||
},
|
||||
handleRoadLoading() {
|
||||
async handleRoadLoading() {
|
||||
if (!this.selectionList.length) {
|
||||
this.$message.warning('请选择至少一条数据');
|
||||
return;
|
||||
}
|
||||
const nonRoadWaybills = this.selectionList.filter(row => !this.waybillIsRoadTransport(row));
|
||||
if (nonRoadWaybills.length) {
|
||||
this.$message.warning('公路配载仅支持运输方式为“公路运输”的运单');
|
||||
const selectedIds = this.selectionList.map(row => row.id).filter(Boolean);
|
||||
if (!selectedIds.length) {
|
||||
this.$message.warning('请选择至少一条数据');
|
||||
return;
|
||||
}
|
||||
const unavailableWaybills = this.selectionList.filter(
|
||||
row =>
|
||||
!['processing', 'running'].includes(this.statusValue(row)) ||
|
||||
row.loadingId ||
|
||||
row.loadingManageId ||
|
||||
row.loadingNo
|
||||
);
|
||||
if (unavailableWaybills.length) {
|
||||
this.$message.warning('公路配载仅支持进行中且未配载的运单');
|
||||
// 可配载与否只看后端查询结果
|
||||
let loadableIds;
|
||||
try {
|
||||
const res = await getWaybillList(1, selectedIds.length, {
|
||||
loadableOnly: 1,
|
||||
ids: selectedIds.join(','),
|
||||
});
|
||||
loadableIds = new Set(extractRecords(res).map(row => String(row.id)));
|
||||
} catch (error) {
|
||||
this.$message.error('校验运单可配载状态失败,请重试');
|
||||
return;
|
||||
}
|
||||
const rejectedRows = this.selectionList.filter(row => !loadableIds.has(String(row.id)));
|
||||
if (rejectedRows.length) {
|
||||
const rejectedText = rejectedRows
|
||||
.map(row => {
|
||||
const reason = this.roadLoadingRejectReasonText(row);
|
||||
const no = row.waybillNo || row.id || '';
|
||||
return `${no}(${reason})`;
|
||||
})
|
||||
.join('、');
|
||||
this.$message.warning(`${rejectedText};公路配载仅支持公路、待执行且未配载的运单`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -10411,6 +10490,13 @@ export default {
|
||||
},
|
||||
});
|
||||
},
|
||||
// 只给后端判定为不可配载的运单生成可读原因,本身不参与准入判定
|
||||
roadLoadingRejectReasonText(row = {}) {
|
||||
if (!this.waybillIsRoadTransport(row)) return '非公路运输';
|
||||
if (String(row.businessStatus || '') !== 'pending') return '状态不是待执行';
|
||||
if (row.loadingNo) return '已关联配载单';
|
||||
return '运单数据可能已变化,请刷新后重试';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -8,11 +8,17 @@
|
||||
<el-form-item label="变更类型" class="contract-basic-section__change-type">
|
||||
<el-radio-group v-model="form.changeType">
|
||||
<el-radio label="合同信息变更" />
|
||||
<el-radio label="结算规则变更" />
|
||||
<el-radio label="终止合同" />
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="签约类型" prop="signType">
|
||||
<el-select v-model="form.signType" placeholder="请选择签约类型" clearable>
|
||||
<el-select
|
||||
v-model="form.signType"
|
||||
placeholder="请选择签约类型"
|
||||
clearable
|
||||
:disabled="isSettlementRuleChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in signTypeOptions"
|
||||
:key="item.value"
|
||||
@@ -21,8 +27,14 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="合同编号">
|
||||
<el-input v-model="form.contractNo" disabled />
|
||||
<el-form-item label="合同编号" prop="contractNo">
|
||||
<el-input
|
||||
v-model="form.contractNo"
|
||||
maxlength="100"
|
||||
clearable
|
||||
placeholder="请输入合同编号"
|
||||
:disabled="isSettlementRuleChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="合同名称" prop="contractName">
|
||||
<el-input
|
||||
@@ -30,6 +42,7 @@
|
||||
maxlength="100"
|
||||
show-word-limit
|
||||
placeholder="请输入合同名称"
|
||||
:disabled="isSettlementRuleChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="合同类型">
|
||||
@@ -80,6 +93,7 @@
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="请输入"
|
||||
:disabled="isSettlementRuleChange"
|
||||
/>
|
||||
<span>至</span>
|
||||
<el-date-picker
|
||||
@@ -88,6 +102,7 @@
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="请输入"
|
||||
:disabled="isSettlementRuleChange"
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
@@ -95,6 +110,7 @@
|
||||
<el-input
|
||||
v-model="form.paymentDays"
|
||||
placeholder="请输入"
|
||||
:disabled="isSettlementRuleChange"
|
||||
@input="value => positiveIntegerInput('paymentDays', value)"
|
||||
>
|
||||
<template #suffix>天</template>
|
||||
@@ -105,22 +121,36 @@
|
||||
:model-value="contractAmountDisplay"
|
||||
placeholder="请输入"
|
||||
clearable
|
||||
:disabled="isSettlementRuleChange"
|
||||
@input="handleContractAmountInput"
|
||||
@blur="formatContractAmountField"
|
||||
@clear="form.contractAmount = null"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否范本">
|
||||
<el-select v-model="form.templateFlag" placeholder="请选择">
|
||||
<el-select
|
||||
v-model="form.templateFlag"
|
||||
placeholder="请选择"
|
||||
:disabled="isSettlementRuleChange"
|
||||
>
|
||||
<el-option label="否" :value="0" />
|
||||
<el-option label="是" :value="1" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="原件合同编号">
|
||||
<el-input v-model="form.originalContractNo" maxlength="100" placeholder="请输入" />
|
||||
<el-input
|
||||
v-model="form.originalContractNo"
|
||||
maxlength="100"
|
||||
placeholder="请输入"
|
||||
:disabled="isSettlementRuleChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否电子章">
|
||||
<el-select v-model="form.electronicSealFlag" placeholder="请选择">
|
||||
<el-select
|
||||
v-model="form.electronicSealFlag"
|
||||
placeholder="请选择"
|
||||
:disabled="isSettlementRuleChange"
|
||||
>
|
||||
<el-option label="否" :value="0" />
|
||||
<el-option label="是" :value="1" />
|
||||
</el-select>
|
||||
@@ -129,13 +159,19 @@
|
||||
<el-input
|
||||
v-model="form.invoiceCycle"
|
||||
placeholder="请输入"
|
||||
:disabled="isSettlementRuleChange"
|
||||
@input="value => positiveIntegerInput('invoiceCycle', value)"
|
||||
>
|
||||
<template #suffix>天</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="结算币种" prop="settlementCurrency">
|
||||
<el-select v-model="form.settlementCurrency" placeholder="请选择结算币种" clearable>
|
||||
<el-select
|
||||
v-model="form.settlementCurrency"
|
||||
placeholder="请选择结算币种"
|
||||
clearable
|
||||
:disabled="isSettlementRuleChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in settlementCurrencyOptions"
|
||||
:key="item.value"
|
||||
@@ -145,7 +181,12 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="合同格式">
|
||||
<el-select v-model="form.contractFormat" placeholder="请选择合同格式" clearable>
|
||||
<el-select
|
||||
v-model="form.contractFormat"
|
||||
placeholder="请选择合同格式"
|
||||
clearable
|
||||
:disabled="isSettlementRuleChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in contractFormatOptions"
|
||||
:key="item.value"
|
||||
@@ -158,19 +199,30 @@
|
||||
<el-input
|
||||
v-model="form.copyCount"
|
||||
placeholder="请输入"
|
||||
:disabled="isSettlementRuleChange"
|
||||
@input="value => positiveIntegerInput('copyCount', value)"
|
||||
>
|
||||
<template #suffix>份</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否需要加盖法人章">
|
||||
<el-select v-model="form.legalSealFlag" placeholder="请选择" clearable>
|
||||
<el-select
|
||||
v-model="form.legalSealFlag"
|
||||
placeholder="请选择"
|
||||
clearable
|
||||
:disabled="isSettlementRuleChange"
|
||||
>
|
||||
<el-option label="否" :value="0" />
|
||||
<el-option label="是" :value="1" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="结算方式">
|
||||
<el-select v-model="form.settlementMode" placeholder="请选择结算方式" clearable>
|
||||
<el-select
|
||||
v-model="form.settlementMode"
|
||||
placeholder="请选择结算方式"
|
||||
clearable
|
||||
:disabled="isSettlementRuleChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in settlementModeOptions"
|
||||
:key="item.value"
|
||||
@@ -191,6 +243,7 @@
|
||||
maxlength="1000"
|
||||
show-word-limit
|
||||
placeholder="请输入合同主要内容"
|
||||
:disabled="isSettlementRuleChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="其他事项说明" class="contract-basic-section__remark">
|
||||
@@ -201,6 +254,7 @@
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
placeholder="请输入其他事项说明"
|
||||
:disabled="isSettlementRuleChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<!-- 风险客商提示:合同已推送合规并留痕高风险/黑名单时展示,变更提交同样要求特批附件 -->
|
||||
@@ -214,6 +268,7 @@
|
||||
title="用印材料"
|
||||
description
|
||||
attachment-type
|
||||
:readonly="isSettlementRuleChange"
|
||||
:attachment-location-options="changeAttachmentLocationOptions"
|
||||
:rows="contractFileRows"
|
||||
:preview="previewAttachment"
|
||||
@@ -224,12 +279,36 @@
|
||||
<section class="change-section">
|
||||
<div class="section-head">
|
||||
<div class="dialog-section-title">计费信息</div>
|
||||
<el-button v-if="feeGenerationMode !== 'manual'" type="primary" plain @click="addPlan"
|
||||
<el-button
|
||||
v-if="feeGenerationMode !== 'manual'"
|
||||
type="primary"
|
||||
plain
|
||||
@click="addPlan"
|
||||
>添加</el-button
|
||||
>
|
||||
</div>
|
||||
<el-radio-group v-model="feeGenerationMode"><el-radio label="system">系统生成</el-radio><el-radio label="manual">账单导入生成</el-radio></el-radio-group>
|
||||
<el-table :data="plans" border class="change-table"><el-table-column type="index" label="序号" width="70" /><el-table-column prop="planName" label="方案名称" min-width="220" /><el-table-column label="默认方案" min-width="120"><template #default="{ row }">{{ row.defaultPlan ? '是' : '否' }}</template></el-table-column><el-table-column prop="remark" label="备注" min-width="260" /><el-table-column label="操作" width="180"><template #default="{ row, $index }"><el-link type="primary" @click="editPlan(row, $index)">编辑</el-link><el-link type="danger" @click="plans.splice($index, 1)">删除</el-link></template></el-table-column></el-table>
|
||||
<el-radio-group v-model="feeGenerationMode"
|
||||
><el-radio label="system">系统生成</el-radio
|
||||
><el-radio label="manual">账单导入生成</el-radio></el-radio-group
|
||||
>
|
||||
<el-table :data="plans" border class="change-table"
|
||||
><el-table-column type="index" label="序号" width="70" /><el-table-column
|
||||
prop="planName"
|
||||
label="方案名称"
|
||||
min-width="220"
|
||||
/><el-table-column label="默认方案" min-width="120"
|
||||
><template #default="{ row }">{{
|
||||
row.defaultPlan ? '是' : '否'
|
||||
}}</template></el-table-column
|
||||
><el-table-column prop="remark" label="备注" min-width="260" /><el-table-column
|
||||
label="操作"
|
||||
width="180"
|
||||
><template #default="{ row, $index }"
|
||||
><el-link type="primary" @click="editPlan(row, $index)">编辑</el-link
|
||||
><el-link type="danger" @click="plans.splice($index, 1)">删除</el-link></template
|
||||
></el-table-column
|
||||
></el-table
|
||||
>
|
||||
</section>
|
||||
|
||||
<section class="change-section">
|
||||
@@ -248,19 +327,106 @@
|
||||
<el-radio :label="0">关闭</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<el-form v-if="settlementRule.autoGenerate === 1" :model="settlementRule" label-position="right" label-width="auto" class="settlement-form">
|
||||
<el-form-item label="账单起始日" required><el-date-picker v-model="settlementRule.billStartDate" type="date" placeholder="请选择账单起始日" format="YYYY-MM-DD" value-format="YYYY-MM-DD" /></el-form-item>
|
||||
<el-form-item label="结算类型" required><el-select v-model="settlementRule.settlementType" placeholder="请选择结算类型" @change="handleSettlementTypeChange"><el-option v-for="item in settlementTypeOptions" :key="item" :label="item" :value="item" /></el-select></el-form-item>
|
||||
<el-form-item v-if="showSettlementBillCycleType" label="结算周期" required><el-select v-model="settlementRule.billCycleType" placeholder="请选择结算周期" @change="handleCycleTypeChange"><el-option v-for="item in billCycleTypeOptions" :key="item" :label="item" :value="item" /></el-select></el-form-item>
|
||||
<el-form-item v-if="showSettlementBillCutoffDay" label="账单截单日" required><el-select v-model="settlementRule.billCutoffDay" placeholder="请选择账单截单日"><el-option v-for="item in billCutoffDayOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select></el-form-item>
|
||||
<el-form-item v-if="showSettlementCycleDays" label="周期天数" required><el-select v-model="settlementRule.cycleDays" placeholder="请选择周期天数"><el-option v-for="item in cycleDayOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select></el-form-item>
|
||||
<el-form
|
||||
v-if="settlementRule.autoGenerate === 1"
|
||||
:model="settlementRule"
|
||||
label-position="right"
|
||||
label-width="auto"
|
||||
class="settlement-form"
|
||||
>
|
||||
<el-form-item label="账单起始日" required
|
||||
><el-date-picker
|
||||
v-model="settlementRule.billStartDate"
|
||||
type="date"
|
||||
placeholder="请选择账单起始日"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
/></el-form-item>
|
||||
<el-form-item label="结算类型" required
|
||||
><el-select
|
||||
v-model="settlementRule.settlementType"
|
||||
placeholder="请选择结算类型"
|
||||
@change="handleSettlementTypeChange"
|
||||
><el-option
|
||||
v-for="item in settlementTypeOptions"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item"
|
||||
/></el-select></el-form-item>
|
||||
<el-form-item v-if="showSettlementBillCycleType" label="结算周期" required
|
||||
><el-select
|
||||
v-model="settlementRule.billCycleType"
|
||||
placeholder="请选择结算周期"
|
||||
@change="handleCycleTypeChange"
|
||||
><el-option
|
||||
v-for="item in billCycleTypeOptions"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item"
|
||||
/></el-select></el-form-item>
|
||||
<el-form-item v-if="showSettlementBillCutoffDay" label="账单截单日" required
|
||||
><el-select v-model="settlementRule.billCutoffDay" placeholder="请选择账单截单日"
|
||||
><el-option
|
||||
v-for="item in billCutoffDayOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/></el-select></el-form-item>
|
||||
<el-form-item v-if="showSettlementCycleDays" label="周期天数" required
|
||||
><el-select v-model="settlementRule.cycleDays" placeholder="请选择周期天数"
|
||||
><el-option
|
||||
v-for="item in cycleDayOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/></el-select></el-form-item>
|
||||
</el-form>
|
||||
<el-table v-if="settlementRule.autoGenerate === 1 && showSettlementCustomPeriods" :data="settlementRule.customPeriods || []" border class="change-table custom-periods-table">
|
||||
<el-table
|
||||
v-if="settlementRule.autoGenerate === 1 && showSettlementCustomPeriods"
|
||||
:data="settlementRule.customPeriods || []"
|
||||
border
|
||||
class="change-table custom-periods-table"
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="70" />
|
||||
<el-table-column label="运单区间-开始日" min-width="180"><template #default="{ row, $index }"><el-select :model-value="row.startDay" placeholder="请选择" clearable @update:model-value="value => handleCustomPeriodStartDayChange($index, value)"><el-option v-for="item in billCutoffDayOptions" :key="`start-${item.value}`" :label="item.label" :value="item.value" /></el-select></template></el-table-column>
|
||||
<el-table-column label="运单区间-结束日" min-width="200"><template #default="{ row, $index }"><el-select :model-value="row.endDay" placeholder="请选择" clearable @update:model-value="value => handleCustomPeriodEndDayChange($index, value)"><el-option v-for="item in customPeriodEndDayOptions(row)" :key="item.value" :label="item.label" :value="item.value" /></el-select></template></el-table-column>
|
||||
<el-table-column label="操作" width="100"><template #default="{ $index }"><el-link v-if="$index === 0" type="primary" @click="addCustomPeriodRow">添加</el-link><el-link v-else type="danger" @click="removeCustomPeriodRow($index)">删除</el-link></template></el-table-column>
|
||||
<template #empty><el-link type="primary" @click="addCustomPeriodRow">添加</el-link></template>
|
||||
<el-table-column label="运单区间-开始日" min-width="180"
|
||||
><template #default="{ row, $index }"
|
||||
><el-select
|
||||
:model-value="row.startDay"
|
||||
placeholder="请选择"
|
||||
clearable
|
||||
@update:model-value="value => handleCustomPeriodStartDayChange($index, value)"
|
||||
><el-option
|
||||
v-for="item in billCutoffDayOptions"
|
||||
:key="`start-${item.value}`"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/></el-select></template
|
||||
></el-table-column>
|
||||
<el-table-column label="运单区间-结束日" min-width="200"
|
||||
><template #default="{ row, $index }"
|
||||
><el-select
|
||||
:model-value="row.endDay"
|
||||
placeholder="请选择"
|
||||
clearable
|
||||
@update:model-value="value => handleCustomPeriodEndDayChange($index, value)"
|
||||
><el-option
|
||||
v-for="item in customPeriodEndDayOptions(row)"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/></el-select></template
|
||||
></el-table-column>
|
||||
<el-table-column label="操作" width="100"
|
||||
><template #default="{ $index }"
|
||||
><el-link v-if="$index === 0" type="primary" @click="addCustomPeriodRow">添加</el-link
|
||||
><el-link v-else type="danger" @click="removeCustomPeriodRow($index)"
|
||||
>删除</el-link
|
||||
></template
|
||||
></el-table-column
|
||||
>
|
||||
<template #empty
|
||||
><el-link type="primary" @click="addCustomPeriodRow">添加</el-link></template
|
||||
>
|
||||
</el-table>
|
||||
</section>
|
||||
|
||||
@@ -268,17 +434,48 @@
|
||||
<div class="dialog-section-title">
|
||||
付款比例设置
|
||||
<el-tooltip content="非必填;配置付款比例时,合计必须等于100%。" placement="top">
|
||||
<el-icon style="margin-left: 4px; color: #909399; cursor: pointer"><el-icon-question-filled /></el-icon>
|
||||
<el-icon style="margin-left: 4px; color: #909399; cursor: pointer"
|
||||
><el-icon-question-filled
|
||||
/></el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-table :data="paymentRatioRows" border class="change-table">
|
||||
<el-table-column type="index" label="序号" width="70" />
|
||||
<el-table-column prop="paymentTerm" label="付款笔数" min-width="180" />
|
||||
<el-table-column label="付款比例上限(%)" min-width="220"><template #default="{ row }"><el-input-number v-model="row.ratioLimit" :min="0" :max="100" :precision="2" controls-position="right" /></template></el-table-column>
|
||||
<el-table-column label="备注" min-width="220"><template #default="{ row }"><el-input v-model="row.remark" maxlength="200" /></template></el-table-column>
|
||||
<el-table-column label="操作" width="100"><template #default="{ $index }"><el-link type="danger" @click="paymentRatioRows.splice($index, 1)">删除</el-link></template></el-table-column>
|
||||
<el-table-column label="付款比例上限(%)" min-width="220"
|
||||
><template #default="{ row }"
|
||||
><el-input-number
|
||||
v-model="row.ratioLimit"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:precision="2"
|
||||
controls-position="right"
|
||||
:disabled="isSettlementRuleChange"
|
||||
/></template
|
||||
></el-table-column>
|
||||
<el-table-column label="备注" min-width="220"
|
||||
><template #default="{ row }"
|
||||
><el-input
|
||||
v-model="row.remark"
|
||||
maxlength="200"
|
||||
:disabled="isSettlementRuleChange"
|
||||
/></template
|
||||
></el-table-column>
|
||||
<el-table-column v-if="!isSettlementRuleChange" label="操作" width="100"
|
||||
><template #default="{ $index }"
|
||||
><el-link type="danger" @click="paymentRatioRows.splice($index, 1)"
|
||||
>删除</el-link
|
||||
></template
|
||||
></el-table-column
|
||||
>
|
||||
</el-table>
|
||||
<el-button type="primary" plain @click="addPaymentRatioRow">添加</el-button>
|
||||
<el-button
|
||||
v-if="!isSettlementRuleChange"
|
||||
type="primary"
|
||||
plain
|
||||
@click="addPaymentRatioRow"
|
||||
>添加</el-button
|
||||
>
|
||||
</section>
|
||||
|
||||
<billing-plan-editor
|
||||
@@ -295,6 +492,7 @@
|
||||
attachment-type
|
||||
attachment-location="附件(佐证材料等)"
|
||||
attachment-type-dict-code="contract_supporting_attachment_type"
|
||||
:readonly="isSettlementRuleChange"
|
||||
:attachment-location-options="changeAttachmentLocationOptions"
|
||||
:rows="attachments"
|
||||
:preview="previewAttachment"
|
||||
@@ -475,6 +673,24 @@ export default {
|
||||
'按数量',
|
||||
],
|
||||
rules: {
|
||||
contractNo: [
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (this.isSettlementRuleChange) {
|
||||
return callback();
|
||||
}
|
||||
const contractNo = String(value || '').trim();
|
||||
if (!contractNo) {
|
||||
return callback(new Error('请输入合同编号'));
|
||||
}
|
||||
if (contractNo.length > 100) {
|
||||
return callback(new Error('合同编号不能超过100个字符'));
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
contractName: [{ required: true, message: '请输入合同名称', trigger: 'blur' }],
|
||||
contractAmount: [
|
||||
{ required: true, message: '请输入合同金额', trigger: 'blur' },
|
||||
@@ -510,7 +726,13 @@ export default {
|
||||
.join('、');
|
||||
return `【${parties || '相对方'}】:被列入外部黑名单;涉贪污受贿裁判相关提及方(3年以上及其他);被执行人信息;买卖合同纠纷;劳动纠纷;重大纠纷。提交变更前须上传「特批附件」类型附件`;
|
||||
},
|
||||
isSettlementRuleChange() {
|
||||
return this.form.changeType === '结算规则变更';
|
||||
},
|
||||
changeAttachmentLocationOptions() {
|
||||
if (this.isSettlementRuleChange) {
|
||||
return ['变更材料'];
|
||||
}
|
||||
return ['用印材料', '附件(佐证材料等)', '变更材料'];
|
||||
},
|
||||
contractAmountDisplay() {
|
||||
@@ -736,6 +958,12 @@ export default {
|
||||
previewAttachment(row, rows = this.attachments) { const url = this.attachmentUrl(row); if (!url) { this.$message.warning('附件地址为空,无法预览'); return; } if (this.isAttachmentImage(row)) { this.imagePreviewUrls = (rows || []).filter(item => this.isAttachmentImage(item) && this.attachmentUrl(item)).map(item => this.attachmentUrl(item)); this.imagePreviewIndex = Math.max(this.imagePreviewUrls.indexOf(url), 0); this.imagePreviewVisible = true; return; } this.previewFile = { name: this.attachmentName(row), url, mimeType: row.mimeType || row.contentType || '' }; this.documentPreviewVisible = true; },
|
||||
handleAttachmentUploadToLocation({ location, rows = [] }) {
|
||||
if (!rows.length) return;
|
||||
if (this.isSettlementRuleChange) {
|
||||
if (location === '变更材料') {
|
||||
this.changeMaterials = [...(this.changeMaterials || []), ...rows];
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (isContractSealMaterialLocation(location)) {
|
||||
this.contractFileRows = [...(this.contractFileRows || []), ...rows];
|
||||
return;
|
||||
@@ -759,7 +987,91 @@ 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) }); await submitMkApprovalFlow({ bizType: 'contract-manage', bizId: this.form.id, subjectName: this.form.contractName || '', approvalStatus: this.form.approvalStatus || 'change_rejected', mkEnabled: await loadMkEnabled().catch(() => false) }); this.$message.success('变更已提交'); this.$router.back(); },
|
||||
async submit() {
|
||||
if (this.form.changeType === '终止合同') {
|
||||
await api.terminate(this.form.id, this.form.changeReason);
|
||||
this.$message.success('合同已终止');
|
||||
this.$router.back();
|
||||
return;
|
||||
}
|
||||
await this.$refs.formRef.validate();
|
||||
if (!this.isSettlementRuleChange) {
|
||||
const contractNo = String(this.form.contractNo || '').trim();
|
||||
if (!contractNo) {
|
||||
this.$message.warning('请输入合同编号');
|
||||
return;
|
||||
}
|
||||
if (contractNo.length > 100) {
|
||||
this.$message.warning('合同编号不能超过100个字符');
|
||||
return;
|
||||
}
|
||||
this.form.contractNo = contractNo;
|
||||
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,
|
||||
};
|
||||
const payload = this.isSettlementRuleChange
|
||||
? {
|
||||
id: this.form.id,
|
||||
changeType: this.form.changeType,
|
||||
changeContent: this.form.changeContent,
|
||||
changeReason: this.form.changeReason,
|
||||
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),
|
||||
changeAttachmentsJson: JSON.stringify(this.changeMaterials),
|
||||
}
|
||||
: {
|
||||
...this.form,
|
||||
changeType: this.form.changeType || '合同信息变更',
|
||||
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 api.submitChange(payload);
|
||||
await submitMkApprovalFlow({
|
||||
bizType: 'contract-manage',
|
||||
bizId: this.form.id,
|
||||
subjectName: this.form.contractName || '',
|
||||
approvalStatus: this.form.approvalStatus || 'change_rejected',
|
||||
mkEnabled: await loadMkEnabled().catch(() => false),
|
||||
});
|
||||
this.$message.success('变更已提交');
|
||||
this.$router.back();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1771,6 +1771,12 @@ export default {
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
// 同步推送合规耗时较长,加遮罩防重复操作(照审批通过遮罩)
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
text: '正在推送合规,请稍候…',
|
||||
background: 'rgba(255, 255, 255, 0.6)',
|
||||
});
|
||||
this.hcsRepushingId = row.id;
|
||||
hcsRepush('contract', row.id)
|
||||
.then(res => {
|
||||
@@ -1781,14 +1787,12 @@ export default {
|
||||
this.$message.success(res.data?.data || '推送合规成功');
|
||||
this.onLoad(this.page, this.query);
|
||||
})
|
||||
.catch(error => {
|
||||
// 拦截器已弹出后端 msg(如特批审核中禁止重推),此处不再重复提示
|
||||
if (!error?._notified) {
|
||||
this.$message.error(error?.response?.data?.msg || '推送合规失败');
|
||||
}
|
||||
.catch(() => {
|
||||
// 失败原因由 axios 拦截器统一弹出后端 msg,此处不再重复提示
|
||||
})
|
||||
.finally(() => {
|
||||
this.hcsRepushingId = null;
|
||||
loading.close();
|
||||
});
|
||||
});
|
||||
},
|
||||
@@ -1808,6 +1812,12 @@ export default {
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
// 同步推送合规耗时较长,加遮罩防重复操作(照审批通过遮罩)
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
text: '正在批量推送合规,请稍候…',
|
||||
background: 'rgba(255, 255, 255, 0.6)',
|
||||
});
|
||||
this.hcsRepushingId = 'batch';
|
||||
hcsRepushBatch('contract', this.selectionList.map(item => item.id))
|
||||
.then(res => {
|
||||
@@ -1818,14 +1828,12 @@ export default {
|
||||
this.$message.success(res.data?.data || '批量推送合规完成');
|
||||
this.onLoad(this.page, this.query);
|
||||
})
|
||||
.catch(error => {
|
||||
// 拦截器已弹出后端 msg,此处不再重复提示
|
||||
if (!error?._notified) {
|
||||
this.$message.error(error?.response?.data?.msg || '批量推送合规失败');
|
||||
}
|
||||
.catch(() => {
|
||||
// 失败原因由 axios 拦截器统一弹出后端 msg,此处不再重复提示
|
||||
})
|
||||
.finally(() => {
|
||||
this.hcsRepushingId = null;
|
||||
loading.close();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
@@ -635,10 +635,11 @@
|
||||
min-width="100" /><el-table-column
|
||||
prop="quantity"
|
||||
label="数量"
|
||||
min-width="90" /><el-table-column
|
||||
prop="quantityUnit"
|
||||
label="数量单位"
|
||||
min-width="100" /><el-table-column
|
||||
min-width="120"
|
||||
><template #default="{ row }">{{
|
||||
formatCargoQuantityDisplay(row)
|
||||
}}</template></el-table-column
|
||||
><el-table-column
|
||||
prop="materialCode"
|
||||
label="物料编码"
|
||||
min-width="120" /><el-table-column
|
||||
@@ -897,6 +898,16 @@
|
||||
>
|
||||
<div title="配载信息" class="loading-manage-dialog__load-info-card">
|
||||
<section-card>
|
||||
<div class="loading-manage-dialog__admission-tip">
|
||||
<div class="loading-manage-dialog__admission-title">可配载条件</div>
|
||||
<div class="loading-manage-dialog__admission-text">
|
||||
同时满足以下条件的运单可被配载:运输方式为「公路」;运单状态为「待执行」或「进行中」;未关联其他有效配载单;同一配载单的子运单须属同一项目,且承运商合同乙方须存在共同可选值(按各子单客户合同甲方匹配后取交集)。
|
||||
</div>
|
||||
<div class="loading-manage-dialog__admission-title">配载后状态说明</div>
|
||||
<div class="loading-manage-dialog__admission-text">
|
||||
子运单:按项目过程配置是否需司机接单,无需接单则为进行中,需接单则为待执行。配载单:按所选运单所属项目的过程配置,需接单则为待执行,无需接单则为进行中。
|
||||
</div>
|
||||
</div>
|
||||
<div class="loading-manage-dialog__candidate-search">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="6">
|
||||
@@ -1068,31 +1079,12 @@
|
||||
<el-option
|
||||
v-for="item in driverOptions"
|
||||
:key="item.id || item.driverName || item.name"
|
||||
:label="item.driverName || item.name"
|
||||
:label="driverOptionText(item)"
|
||||
:value="item.driverName || item.name"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="运输方式">
|
||||
<el-select
|
||||
v-model="candidateQuery.transportType"
|
||||
clearable
|
||||
filterable
|
||||
placeholder="请选择"
|
||||
:disabled="dialogMode === 'add'"
|
||||
@visible-change="visible => visible && loadTransportTypeOptions()"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in transportTypeOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</template>
|
||||
<el-col
|
||||
:span="candidateSearchVisible ? 24 : 6"
|
||||
@@ -1201,6 +1193,16 @@
|
||||
min-width="160"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="carrierContractName"
|
||||
label="承运商合同"
|
||||
min-width="160"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.carrierContractName || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="planName"
|
||||
label="计划名称"
|
||||
@@ -1357,19 +1359,29 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="110" fixed="right">
|
||||
<template #default="{ $index }">
|
||||
<template #default="{ row, $index }">
|
||||
<el-link
|
||||
v-if="!dialogReadonly"
|
||||
v-if="!dialogReadonly && !isSelectedWaybillRemoveLocked(row)"
|
||||
type="danger"
|
||||
@click="removeSelectedWaybill($index)"
|
||||
>删除</el-link
|
||||
>
|
||||
<span
|
||||
v-else-if="!dialogReadonly && isSelectedWaybillRemoveLocked(row)"
|
||||
class="loading-manage-dialog__locked-tip"
|
||||
>已打卡</span
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</section-card>
|
||||
|
||||
<section-card title="线路编辑">
|
||||
<template v-if="isRunningEdit" #extra>
|
||||
<span class="loading-manage-dialog__route-hint"
|
||||
>已打卡地点置灰不可拖拽,新增地址仅可在未打卡节点间调整</span
|
||||
>
|
||||
</template>
|
||||
<div class="loading-manage-dialog__route-list" v-if="routeNodes.length">
|
||||
<div
|
||||
v-for="(node, index) in routeNodes"
|
||||
@@ -1415,8 +1427,9 @@
|
||||
<el-table-column prop="cargoName" label="货物名称" min-width="140" />
|
||||
<el-table-column prop="cargoType" label="货物类型" min-width="140" />
|
||||
<el-table-column prop="packageType" label="包装" min-width="100" />
|
||||
<el-table-column prop="quantity" label="数量" min-width="100" />
|
||||
<el-table-column prop="quantityUnit" label="数量单位" min-width="100" />
|
||||
<el-table-column prop="quantity" label="数量" min-width="120">
|
||||
<template #default="{ row }">{{ formatCargoQuantityDisplay(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="materialCode" label="物料编码" min-width="130" />
|
||||
<el-table-column prop="equipmentCode" label="设备编码" min-width="130" />
|
||||
<el-table-column prop="brand" label="品牌" min-width="120" />
|
||||
@@ -1436,7 +1449,7 @@
|
||||
<el-segmented
|
||||
v-model="dialogForm.carrierType"
|
||||
:options="carrierTypeOptions"
|
||||
:disabled="dialogReadonly"
|
||||
:disabled="dialogReadonly || isRunningTaskRequiredLocked"
|
||||
@change="handleCarrierTypeChange"
|
||||
/>
|
||||
</div>
|
||||
@@ -1450,7 +1463,11 @@
|
||||
"
|
||||
clearable
|
||||
filterable
|
||||
:disabled="dialogReadonly || !selectedWaybillRows.length"
|
||||
:disabled="
|
||||
dialogReadonly ||
|
||||
isRunningTaskRequiredLocked ||
|
||||
!selectedWaybillRows.length
|
||||
"
|
||||
:loading="taskCarrierLoading"
|
||||
:placeholder="taskCarrierPlaceholder"
|
||||
@change="handleTaskCarrierChange"
|
||||
@@ -1483,20 +1500,40 @@
|
||||
:fetch-suggestions="fetchDriverSuggestions"
|
||||
:loading="driverLoading"
|
||||
placeholder="请输入"
|
||||
:disabled="dialogReadonly || isRunningSelfDriverLocked"
|
||||
@select="item => handleDriverSuggestionSelect('dialog', item)"
|
||||
/>
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<span>{{ driverOptionText(item) }}</span>
|
||||
</template>
|
||||
</el-autocomplete>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
label="手机号"
|
||||
:required="dialogForm.carrierType === '自运'"
|
||||
>
|
||||
<el-input v-model="dialogForm.driverPhone" clearable placeholder="请输入" />
|
||||
<el-input
|
||||
v-model="dialogForm.driverPhone"
|
||||
clearable
|
||||
placeholder="请输入"
|
||||
:disabled="dialogReadonly || isRunningSelfDriverLocked"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="车牌号" required>
|
||||
<el-input v-model="dialogForm.vehicleNo" clearable placeholder="请输入" />
|
||||
<el-input
|
||||
v-model="dialogForm.vehicleNo"
|
||||
clearable
|
||||
placeholder="请输入"
|
||||
:disabled="dialogReadonly || isRunningTaskRequiredLocked"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="dialogForm.carrierType !== '承运商'" label="挂车车牌号">
|
||||
<el-input v-model="dialogForm.trailerVehicleNo" clearable placeholder="请输入" />
|
||||
<el-input
|
||||
v-model="dialogForm.trailerVehicleNo"
|
||||
clearable
|
||||
placeholder="请输入"
|
||||
:disabled="dialogReadonly"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="dialogForm.carrierType !== '承运商'" label="押运人">
|
||||
<el-autocomplete
|
||||
@@ -1506,11 +1543,21 @@
|
||||
:fetch-suggestions="fetchEscortSuggestions"
|
||||
:loading="escortLoading"
|
||||
placeholder="请输入"
|
||||
:disabled="dialogReadonly"
|
||||
@select="item => handleEscortSuggestionSelect('dialog', item)"
|
||||
/>
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<span>{{ driverOptionText(item) }}</span>
|
||||
</template>
|
||||
</el-autocomplete>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="dialogForm.carrierType !== '承运商'" label="押运人手机号">
|
||||
<el-input v-model="dialogForm.escortPhone" clearable placeholder="请输入" />
|
||||
<el-input
|
||||
v-model="dialogForm.escortPhone"
|
||||
clearable
|
||||
placeholder="请输入"
|
||||
:disabled="dialogReadonly"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="里程(km)">
|
||||
<el-input
|
||||
@@ -1528,6 +1575,7 @@
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="请选择"
|
||||
:disabled="dialogReadonly"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="预计完成日期">
|
||||
@@ -1537,6 +1585,7 @@
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="请选择"
|
||||
:disabled="dialogReadonly"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" class="loading-manage-dialog__span-2">
|
||||
@@ -1547,6 +1596,7 @@
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
placeholder="请输入,500字以内"
|
||||
:disabled="dialogReadonly"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
@@ -1561,11 +1611,17 @@
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button @click="closeLoadingDialog">关闭</el-button>
|
||||
<el-button type="primary" plain :loading="dialogSaving === 'draft'" @click="saveDraft">
|
||||
<el-button
|
||||
v-if="!isRunningEdit"
|
||||
type="primary"
|
||||
plain
|
||||
:loading="dialogSaving === 'draft'"
|
||||
@click="saveDraft"
|
||||
>
|
||||
暂存
|
||||
</el-button>
|
||||
<el-button type="primary" :loading="dialogSaving === 'submit'" @click="submitLoading">
|
||||
{{ dialogMode === 'reassign' ? '确认派单' : '确认' }}
|
||||
{{ dialogMode === 'reassign' ? '确认派单' : isRunningEdit ? '保存' : '确认' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</div>
|
||||
@@ -1823,7 +1879,7 @@ import { getList as getContractList } from '@/api/business/contract-manage';
|
||||
import { getParentOptions as getCargoTypeOptions } from '@/api/base/cargo-type';
|
||||
import { getList as getCustomerList } from '@/api/vehicle/customer-archive';
|
||||
import { getList as getDriverList } from '@/api/transportCapacity/driver';
|
||||
import { fetchDriversByCarrierOrganizations } from '@/utils/carrier-org-resource';
|
||||
import { fetchDriverCandidates, driverOptionText } from '@/utils/carrier-org-resource';
|
||||
import { formatContractOptionLabel } from '@/utils/contract';
|
||||
import {
|
||||
getList as getProcessConfigList,
|
||||
@@ -1848,7 +1904,6 @@ import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-sear
|
||||
import { parseRouteChangeContent } from '@/utils/route-change';
|
||||
|
||||
const defaultTransportType = '';
|
||||
const defaultCandidateTransportType = '';
|
||||
const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
|
||||
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
|
||||
const loadingCreateWaybillsStorageKey = 'loading-manage-create-waybills';
|
||||
@@ -1889,7 +1944,6 @@ const createCandidateSearchForm = () => ({
|
||||
vehicleNo: '',
|
||||
carrierName: '',
|
||||
driverName: '',
|
||||
transportType: defaultCandidateTransportType,
|
||||
});
|
||||
|
||||
const createDialogForm = () => ({
|
||||
@@ -2107,6 +2161,8 @@ export default {
|
||||
routeNodes: [],
|
||||
cargoRows: [],
|
||||
routeDragIndex: -1,
|
||||
/** 进行中编辑:子运单打卡记录映射 waybillId -> records[] */
|
||||
waybillPunchMap: {},
|
||||
transportTypeOptions: [],
|
||||
projectOptions: [],
|
||||
customerOptions: [],
|
||||
@@ -2170,6 +2226,23 @@ export default {
|
||||
dialogReadonly() {
|
||||
return this.dialogMode === 'view';
|
||||
},
|
||||
/** 进行中配载编辑:可加子单/调未打卡路线,但任务必填字段锁定 */
|
||||
isRunningEdit() {
|
||||
return (
|
||||
this.dialogMode === 'edit' &&
|
||||
String(this.dialogForm.businessStatus || '') === 'running'
|
||||
);
|
||||
},
|
||||
/** 进行中任务必填字段(已派车)不可改 */
|
||||
isRunningTaskRequiredLocked() {
|
||||
return this.isRunningEdit;
|
||||
},
|
||||
/** 自运进行中:司机/手机号同属必填派车信息,不可改 */
|
||||
isRunningSelfDriverLocked() {
|
||||
return (
|
||||
this.isRunningEdit && String(this.dialogForm.carrierType || '').trim() === '自运'
|
||||
);
|
||||
},
|
||||
// 打卡时间轴:已打卡按时间升序,未打卡保持原节点顺序排在其后
|
||||
orderedPunchRecords() {
|
||||
const records = Array.isArray(this.punchRecords) ? this.punchRecords : [];
|
||||
@@ -2325,6 +2398,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
parseRouteChangeContent,
|
||||
driverOptionText,
|
||||
formatTaskCarrierContractLabel(item = {}) {
|
||||
const carrierName = item.carrierName || item.partyB || '';
|
||||
const contractLabel = formatContractOptionLabel(item);
|
||||
@@ -2913,6 +2987,7 @@ export default {
|
||||
if (status === 'running') {
|
||||
return [
|
||||
{ type: 'view', label: '详情' },
|
||||
{ type: 'edit', label: '编辑' },
|
||||
{ type: 'complete', label: '完成' },
|
||||
];
|
||||
}
|
||||
@@ -3014,7 +3089,12 @@ export default {
|
||||
query: {
|
||||
mode,
|
||||
...(mode === 'edit' && row?.id ? { id: row.id } : {}),
|
||||
name: mode === 'edit' ? '编辑配载管理' : '新增配载管理',
|
||||
name:
|
||||
mode === 'edit'
|
||||
? row?.businessStatus === 'running'
|
||||
? '编辑进行中配载'
|
||||
: '编辑配载管理'
|
||||
: '新增配载管理',
|
||||
},
|
||||
});
|
||||
return;
|
||||
@@ -3022,7 +3102,6 @@ export default {
|
||||
this.dialogMode = mode;
|
||||
this.dialogLoading = true;
|
||||
this.resetDialogData();
|
||||
if (mode === 'add') this.candidateQuery.transportType = 'road';
|
||||
try {
|
||||
if (row?.id) {
|
||||
const res = await loadingApi.getDetail(row.id);
|
||||
@@ -3037,6 +3116,9 @@ export default {
|
||||
this.dialogForm.loadingSubNos
|
||||
);
|
||||
this.syncProcessWaybillOptions();
|
||||
if (this.isRunningEdit) {
|
||||
await this.refreshWaybillPunchMap();
|
||||
}
|
||||
this.restoreRouteAndCargo();
|
||||
this.restoreRouteChangeRecords();
|
||||
this.loadPunchRecords();
|
||||
@@ -3046,7 +3128,6 @@ export default {
|
||||
} else {
|
||||
this.dialogForm = createDialogForm();
|
||||
this.candidateQuery = createCandidateSearchForm();
|
||||
this.candidateQuery.transportType = 'road';
|
||||
await this.loadCandidateWaybills();
|
||||
}
|
||||
} finally {
|
||||
@@ -3071,6 +3152,8 @@ export default {
|
||||
this.taskCarrierLoading = false;
|
||||
this.routeNodes = [];
|
||||
this.cargoRows = [];
|
||||
this.routeDragIndex = -1;
|
||||
this.waybillPunchMap = {};
|
||||
this.candidatePage.currentPage = 1;
|
||||
this.candidatePage.total = 0;
|
||||
this.candidateSearchVisible = false;
|
||||
@@ -3435,20 +3518,144 @@ export default {
|
||||
const statusName = String(row.businessStatusName || row.statusName || '');
|
||||
return statusName.includes('已完成');
|
||||
},
|
||||
isPunchRecordDone(record = {}) {
|
||||
if (record.punched === false || record.statusName === '未打卡') return false;
|
||||
return Boolean(record.punched || record.punchTime || record.punchAt || record.punchDateTime);
|
||||
},
|
||||
isLoadPunchRecord(record = {}) {
|
||||
if (!this.isPunchRecordDone(record)) return false;
|
||||
const code = String(record.nodeCode || '')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const name = String(record.nodeName || '').trim();
|
||||
return code === 'load' || name.includes('装货');
|
||||
},
|
||||
isUnloadPunchRecord(record = {}) {
|
||||
if (!this.isPunchRecordDone(record)) return false;
|
||||
const code = String(record.nodeCode || '')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const name = String(record.nodeName || '').trim();
|
||||
return (
|
||||
code === 'unload' ||
|
||||
name.includes('卸货') ||
|
||||
code === 'arrival' ||
|
||||
name.includes('到货') ||
|
||||
code === 'sign' ||
|
||||
name.includes('签收')
|
||||
);
|
||||
},
|
||||
waybillPunchRecords(row = {}) {
|
||||
const key = String(row?.id || '');
|
||||
if (!key) return [];
|
||||
const records = this.waybillPunchMap[key];
|
||||
return Array.isArray(records) ? records : [];
|
||||
},
|
||||
isWaybillLoadPunched(row = {}) {
|
||||
return this.waybillPunchRecords(row).some(record => this.isLoadPunchRecord(record));
|
||||
},
|
||||
isWaybillUnloadPunched(row = {}) {
|
||||
return this.waybillPunchRecords(row).some(record => this.isUnloadPunchRecord(record));
|
||||
},
|
||||
isWaybillPunchLocked(row = {}) {
|
||||
return this.isWaybillLoadPunched(row) || this.isWaybillUnloadPunched(row);
|
||||
},
|
||||
isSelectedWaybillRemoveLocked(row = {}) {
|
||||
if (this.isWaybillCompleted(row)) return true;
|
||||
return this.isRunningEdit && this.isWaybillPunchLocked(row);
|
||||
},
|
||||
isRouteNodeTagLocked(tag, rows = this.selectedWaybillRows) {
|
||||
const match = String(tag || '').match(/^(装|卸)(\d+)$/);
|
||||
if (!match) return false;
|
||||
const row = rows[Number(match[2]) - 1];
|
||||
if (!row) return false;
|
||||
if (this.isRunningEdit) {
|
||||
return match[1] === '装' ? this.isWaybillLoadPunched(row) : this.isWaybillUnloadPunched(row);
|
||||
}
|
||||
return this.isWaybillCompleted(row);
|
||||
},
|
||||
isRouteChangeNodeLocked(node = {}) {
|
||||
return Boolean(node?.locked);
|
||||
},
|
||||
syncRouteNodeLockedFlags(nodes = [], rows = this.selectedWaybillRows) {
|
||||
return (nodes || []).map(node => {
|
||||
const locked = (node.tags || []).some(tag => {
|
||||
const match = String(tag).match(/^(装|卸)(\d+)$/);
|
||||
if (!match) return false;
|
||||
const row = rows[Number(match[2]) - 1];
|
||||
return row ? this.isWaybillCompleted(row) : false;
|
||||
});
|
||||
const locked = (node.tags || []).some(tag => this.isRouteNodeTagLocked(tag, rows));
|
||||
return { ...node, locked };
|
||||
});
|
||||
},
|
||||
async refreshWaybillPunchMap() {
|
||||
const rows = (this.selectedWaybillRows || []).filter(row => row?.id);
|
||||
if (!rows.length) {
|
||||
this.waybillPunchMap = {};
|
||||
return;
|
||||
}
|
||||
const entries = await Promise.all(
|
||||
rows.map(async row => {
|
||||
try {
|
||||
const res = await getWaybillPunchRecords(row.id);
|
||||
const data = res?.data?.data || res?.data || {};
|
||||
return [String(row.id), Array.isArray(data.records) ? data.records : []];
|
||||
} catch (error) {
|
||||
return [String(row.id), []];
|
||||
}
|
||||
})
|
||||
);
|
||||
this.waybillPunchMap = Object.fromEntries(entries);
|
||||
},
|
||||
/**
|
||||
* 进行中编辑:已打卡地点保持在前且不可插入,新增地址只能排在已打卡节点之后。
|
||||
*/
|
||||
mergeRouteNodesKeepingPunched(prevNodes = [], builtNodes = []) {
|
||||
const prev = Array.isArray(prevNodes) ? prevNodes : [];
|
||||
const built = Array.isArray(builtNodes) ? builtNodes : [];
|
||||
if (!this.isRunningEdit) {
|
||||
return built;
|
||||
}
|
||||
const builtByAddress = new Map();
|
||||
built.forEach(node => {
|
||||
if (node?.address && !builtByAddress.has(node.address)) {
|
||||
builtByAddress.set(node.address, node);
|
||||
}
|
||||
});
|
||||
const lockedAddresses = new Set(
|
||||
built.filter(node => node?.locked && node.address).map(node => node.address)
|
||||
);
|
||||
const used = new Set();
|
||||
const lockedResult = [];
|
||||
prev.forEach(node => {
|
||||
if (!node?.address || !lockedAddresses.has(node.address) || used.has(node.address)) {
|
||||
return;
|
||||
}
|
||||
const latest = builtByAddress.get(node.address);
|
||||
if (!latest) return;
|
||||
lockedResult.push({ ...latest, locked: true });
|
||||
used.add(node.address);
|
||||
});
|
||||
built.forEach(node => {
|
||||
if (!node?.locked || !node.address || used.has(node.address)) return;
|
||||
lockedResult.push({ ...node, locked: true });
|
||||
used.add(node.address);
|
||||
});
|
||||
const unlockedResult = [];
|
||||
prev.forEach(node => {
|
||||
if (!node?.address || lockedAddresses.has(node.address) || used.has(node.address)) {
|
||||
return;
|
||||
}
|
||||
const latest = builtByAddress.get(node.address);
|
||||
if (!latest) return;
|
||||
unlockedResult.push({ ...latest, locked: false });
|
||||
used.add(node.address);
|
||||
});
|
||||
built.forEach(node => {
|
||||
if (node?.locked || !node?.address || used.has(node.address)) return;
|
||||
unlockedResult.push({ ...node, locked: false });
|
||||
used.add(node.address);
|
||||
});
|
||||
return [...lockedResult, ...unlockedResult].map((node, index) => ({
|
||||
...node,
|
||||
key: `${node.address}-${index}`,
|
||||
}));
|
||||
},
|
||||
openRouteChangeMapDialog(row, field) {
|
||||
if (this.isWaybillCompleted(row)) return;
|
||||
this.routeChangeMapTarget = { row, field };
|
||||
@@ -3699,20 +3906,24 @@ export default {
|
||||
async loadCandidateWaybills() {
|
||||
this.candidateLoading = true;
|
||||
try {
|
||||
const selectedProjectId = this.selectedWaybillRows.map(row => row.projectId).find(Boolean);
|
||||
const query = {
|
||||
...this.buildQueryParams(this.candidateQuery),
|
||||
// 待执行(需接单未接)与进行中(无需接单或已接单)均可配载
|
||||
businessStatus: 'pending,running',
|
||||
onlyUnassignedLoading: 1,
|
||||
// 仅查当前账户所属组织及子组织下的运单
|
||||
// 公路、待执行、未配载的判定都在后端可配载开关内
|
||||
// 该开关已把候选限定为公路,再按精确运输方式取值筛会漏掉「公路运输」这类历史写法
|
||||
loadableOnly: 1,
|
||||
// allDept=0 表示不指定组织:超管与「数据权限范围=全部组织」的账户后端不过滤组织,
|
||||
// 其余账户限定本组织(不展开下级)
|
||||
allDept: 0,
|
||||
...(this.dialogMode === 'add' ? { transportType: 'road' } : {}),
|
||||
};
|
||||
const transportType = this.getCandidateTransportTypeQuery(query.transportType);
|
||||
// 已选运单后,候选列表优先限定同一项目,便于再按承运商合同乙方交集筛选
|
||||
if (selectedProjectId && !query.projectId && !query.projectName) {
|
||||
query.projectId = selectedProjectId;
|
||||
}
|
||||
const res = await getWaybillList(
|
||||
this.candidatePage.currentPage,
|
||||
this.candidatePage.pageSize,
|
||||
{ ...query, transportType }
|
||||
query
|
||||
);
|
||||
const page = unwrapPage(res);
|
||||
this.candidateRows = page.records;
|
||||
@@ -3721,20 +3932,12 @@ export default {
|
||||
this.candidateLoading = false;
|
||||
}
|
||||
},
|
||||
getCandidateTransportTypeQuery(value) {
|
||||
const normalized = String(value || '')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (['road', '公路运输'].includes(normalized)) return 'road,公路运输';
|
||||
return value || '';
|
||||
},
|
||||
handleCandidateSearch() {
|
||||
this.candidatePage.currentPage = 1;
|
||||
this.loadCandidateWaybills();
|
||||
},
|
||||
handleCandidateReset() {
|
||||
this.candidateQuery = createCandidateSearchForm();
|
||||
if (this.dialogMode === 'add') this.candidateQuery.transportType = 'road';
|
||||
this.candidatePage.currentPage = 1;
|
||||
this.loadCandidateWaybills();
|
||||
},
|
||||
@@ -3766,19 +3969,89 @@ export default {
|
||||
}
|
||||
})
|
||||
);
|
||||
if (this.isRunningEdit && String(this.dialogForm.carrierType || '').trim() === '承运商') {
|
||||
const contractId = this.dialogForm.carrierContractId;
|
||||
if (contractId) {
|
||||
const projectIds = [
|
||||
...new Set(
|
||||
[...this.selectedWaybillRows, ...selectedRows]
|
||||
.map(row => row.projectId)
|
||||
.filter(Boolean)
|
||||
),
|
||||
];
|
||||
if (projectIds.length) {
|
||||
try {
|
||||
const waybillIds = [...this.selectedWaybillRows, ...selectedRows]
|
||||
.map(row => row.id)
|
||||
.filter(Boolean);
|
||||
const response = await loadingApi.getCarrierContracts(projectIds, waybillIds);
|
||||
const contracts = response?.data?.data || response?.data || [];
|
||||
const stillAllowed = (Array.isArray(contracts) ? contracts : []).some(
|
||||
item => String(item.id) === String(contractId)
|
||||
);
|
||||
if (!stillAllowed) {
|
||||
ElMessage.warning('新增子运单须兼容当前配载单已选定的承运商合同');
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
// 合同校验失败时仍允许添加,最终由后端确认拦截
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const exists = new Set(
|
||||
this.selectedWaybillRows.map(row => String(row.id || row.waybillNo))
|
||||
);
|
||||
// 一张配载单的全部子运单必须同项目:以已选运单(无已选时以本批首张)的项目为准
|
||||
const anchorRow = [...this.selectedWaybillRows, ...selectedRows].find(row =>
|
||||
this.waybillProjectKey(row)
|
||||
);
|
||||
const anchorKey = anchorRow ? this.waybillProjectKey(anchorRow) : '';
|
||||
const mismatchedRows = anchorKey
|
||||
? selectedRows.filter(
|
||||
row => this.waybillProjectKey(row) && this.waybillProjectKey(row) !== anchorKey
|
||||
)
|
||||
: [];
|
||||
if (mismatchedRows.length) {
|
||||
const anchorName = anchorRow.projectName || anchorRow.projectId;
|
||||
const mismatchedText = mismatchedRows
|
||||
.map(
|
||||
row =>
|
||||
`${row.waybillNo || row.id}(项目:${row.projectName || row.projectId || '未知'})`
|
||||
)
|
||||
.join('、');
|
||||
this.$message.warning(
|
||||
`运单 ${mismatchedText} 与已选运单项目「${anchorName}」不一致,不能加入同一配载单`
|
||||
);
|
||||
}
|
||||
const nextRows = [...this.selectedWaybillRows];
|
||||
selectedRows.forEach(row => {
|
||||
if (mismatchedRows.includes(row)) return;
|
||||
const key = String(row.id || row.waybillNo);
|
||||
if (!exists.has(key)) {
|
||||
nextRows.push(row);
|
||||
exists.add(key);
|
||||
}
|
||||
});
|
||||
// 承运商类型:新增后须仍存在共同的承运商合同乙方(按客户合同甲方匹配后取交集)
|
||||
if (
|
||||
String(this.dialogForm.carrierType || '').trim() === '承运商' &&
|
||||
nextRows.length > this.selectedWaybillRows.length
|
||||
) {
|
||||
const compatible = await this.hasCompatibleCarrierContracts(nextRows);
|
||||
if (!compatible) {
|
||||
this.$message.warning(
|
||||
'所选运单在同一项目下没有共同的承运商合同乙方,不能加入同一配载单'
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const prevRouteNodes = [...this.routeNodes];
|
||||
this.selectedWaybillRows = nextRows;
|
||||
this.rebuildSummaryFromWaybills();
|
||||
if (this.isRunningEdit) {
|
||||
await this.refreshWaybillPunchMap();
|
||||
}
|
||||
this.rebuildSummaryFromWaybills(true, prevRouteNodes);
|
||||
await this.loadTaskCarrierOptions();
|
||||
this.$refs.candidateTableRef?.clearSelection?.();
|
||||
this.candidateSelection = [];
|
||||
@@ -3786,15 +4059,42 @@ export default {
|
||||
this.candidateAdding = false;
|
||||
}
|
||||
},
|
||||
async hasCompatibleCarrierContracts(rows = []) {
|
||||
const projectId = (rows || []).map(row => row.projectId).find(Boolean);
|
||||
const waybillIds = (rows || []).map(row => row.id).filter(Boolean);
|
||||
if (!projectId || waybillIds.length < 2) return true;
|
||||
try {
|
||||
const response = await loadingApi.getCarrierContracts([projectId], waybillIds);
|
||||
const contracts = response?.data?.data || response?.data || [];
|
||||
return (Array.isArray(contracts) ? contracts : []).length > 0;
|
||||
} catch (error) {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
async addSelectedWaybills() {
|
||||
await this.addWaybillRows(this.candidateSelection);
|
||||
},
|
||||
waybillProjectKey(row = {}) {
|
||||
return String(row.projectId || row.projectName || '').trim();
|
||||
},
|
||||
removeSelectedWaybill(index) {
|
||||
const row = this.selectedWaybillRows[index];
|
||||
if (this.isSelectedWaybillRemoveLocked(row)) {
|
||||
ElMessage.warning('已打卡的子运单不可移除');
|
||||
return;
|
||||
}
|
||||
this.selectedWaybillRows.splice(index, 1);
|
||||
if (this.isRunningEdit) {
|
||||
this.refreshWaybillPunchMap().then(() => {
|
||||
this.rebuildSummaryFromWaybills();
|
||||
this.loadTaskCarrierOptions();
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.rebuildSummaryFromWaybills();
|
||||
this.loadTaskCarrierOptions();
|
||||
},
|
||||
rebuildSummaryFromWaybills(rebuildRoute = true) {
|
||||
rebuildSummaryFromWaybills(rebuildRoute = true, prevRouteNodes = null) {
|
||||
const rows = this.selectedWaybillRows;
|
||||
this.dialogForm.loadingSubNos = rows
|
||||
.map(row => row.waybillNo)
|
||||
@@ -3819,7 +4119,13 @@ export default {
|
||||
this.dialogForm.transportType = rows.find(row => row.transportType)?.transportType || '';
|
||||
}
|
||||
if (rebuildRoute) {
|
||||
this.routeNodes = this.buildRouteNodes(rows);
|
||||
const builtNodes = this.buildRouteNodes(rows);
|
||||
this.routeNodes = this.isRunningEdit
|
||||
? this.mergeRouteNodesKeepingPunched(
|
||||
prevRouteNodes || this.routeNodes,
|
||||
builtNodes
|
||||
)
|
||||
: builtNodes;
|
||||
}
|
||||
this.cargoRows = this.buildCargoRows(rows);
|
||||
// 编辑回填阶段 routeNodes 尚未恢复时跳过,避免清空已有发到货地
|
||||
@@ -3839,7 +4145,9 @@ export default {
|
||||
if (locked) node.locked = true;
|
||||
};
|
||||
rows.forEach((row, rowIndex) => {
|
||||
const locked = this.isWaybillCompleted(row);
|
||||
const locked = this.isRunningEdit
|
||||
? this.isWaybillLoadPunched(row)
|
||||
: this.isWaybillCompleted(row);
|
||||
const departure = row.departureAddress || row.departureName;
|
||||
appendNode(
|
||||
departure,
|
||||
@@ -3849,7 +4157,9 @@ export default {
|
||||
);
|
||||
});
|
||||
rows.forEach((row, rowIndex) => {
|
||||
const locked = this.isWaybillCompleted(row);
|
||||
const locked = this.isRunningEdit
|
||||
? this.isWaybillUnloadPunched(row)
|
||||
: this.isWaybillCompleted(row);
|
||||
const arrival = row.arrivalAddress || row.arrivalName;
|
||||
appendNode(
|
||||
arrival,
|
||||
@@ -3901,7 +4211,9 @@ export default {
|
||||
if (
|
||||
originalLockedKeys.some((key, lockedIndex) => key && key !== nextLockedKeys[lockedIndex])
|
||||
) {
|
||||
ElMessage.warning('已完成运单对应的路线不可调整');
|
||||
ElMessage.warning(
|
||||
this.isRunningEdit ? '已打卡地点不可调整,新地址只能在未打卡节点间拖拽' : '已完成运单对应的路线不可调整'
|
||||
);
|
||||
this.routeDragIndex = -1;
|
||||
return;
|
||||
}
|
||||
@@ -3941,14 +4253,28 @@ export default {
|
||||
return [this.normalizeCargoRow(row, row)];
|
||||
});
|
||||
},
|
||||
formatCargoQuantityDisplay(row = {}) {
|
||||
const quantity = blankCargoSentinel(row.quantity ?? row.cargoQuantity ?? '');
|
||||
const unit = String(
|
||||
blankCargoSentinel(
|
||||
row.quantityUnit ||
|
||||
row.goodsQuantityUnit ||
|
||||
row.cargoUnit ||
|
||||
row.unit ||
|
||||
''
|
||||
) || ''
|
||||
).trim();
|
||||
if (quantity === '' || quantity === undefined || quantity === null) {
|
||||
return unit || '';
|
||||
}
|
||||
return unit ? `${quantity} ${unit}` : String(quantity);
|
||||
},
|
||||
normalizeCargoRow(waybill, item = {}) {
|
||||
return sanitizeCargoRow({
|
||||
projectName: item.projectName || waybill.projectName || '',
|
||||
cargoName: item.cargoName || item.goodsName || waybill.cargoName || '',
|
||||
cargoType: item.cargoType || item.goodsType || waybill.cargoType || '',
|
||||
packageType: item.packageType || item.package || '',
|
||||
weight: item.weight || item.cargoWeight || '',
|
||||
volume: item.volume || item.cargoVolume || '',
|
||||
quantity: item.quantity || item.cargoQuantity || waybill.quantity || '',
|
||||
quantityUnit:
|
||||
item.quantityUnit ||
|
||||
@@ -4145,15 +4471,15 @@ export default {
|
||||
try {
|
||||
const payload = {
|
||||
...this.normalizePayload(),
|
||||
// 确认生成正式配载单(待执行),覆盖草稿状态
|
||||
businessStatus: 'pending',
|
||||
// 进行中编辑保持 running;新建/草稿确认由后端按项目是否需接单写入 pending/running
|
||||
...(this.isRunningEdit ? { businessStatus: 'running' } : {}),
|
||||
};
|
||||
if (this.dialogMode === 'reassign') {
|
||||
await loadingApi.reassign(payload);
|
||||
ElMessage.success('派单成功');
|
||||
} else {
|
||||
await loadingApi.submit(payload);
|
||||
ElMessage.success('确认成功');
|
||||
ElMessage.success(this.isRunningEdit ? '保存成功' : '确认成功');
|
||||
}
|
||||
sessionStorage.setItem(loadingManageListRefreshKey, '1');
|
||||
this.closeLoadingDialog();
|
||||
@@ -4273,12 +4599,6 @@ export default {
|
||||
this.dialogForm.escortPhone = '';
|
||||
this.driverOptions = [];
|
||||
},
|
||||
getDialogCarrierFilter() {
|
||||
return {
|
||||
carrierId: this.dialogForm.carrierId || '',
|
||||
carrierName: this.dialogForm.carrierName || '',
|
||||
};
|
||||
},
|
||||
handleDriverChange(value) {
|
||||
const driver = this.driverOptions.find(item => (item.driverName || item.name) === value);
|
||||
if (driver && !this.dialogForm.driverPhone) {
|
||||
@@ -4312,16 +4632,12 @@ export default {
|
||||
},
|
||||
fetchEscortSuggestions(queryString, callback) {
|
||||
const keyword = String(queryString || '').trim();
|
||||
const carrier = this.getDialogCarrierFilter();
|
||||
if (!carrier.carrierId && !carrier.carrierName) {
|
||||
callback([]);
|
||||
return;
|
||||
}
|
||||
this.escortLoading = true;
|
||||
fetchDriversByCarrierOrganizations(
|
||||
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '押运员' },
|
||||
carrier
|
||||
)
|
||||
fetchDriverCandidates({
|
||||
size: 20,
|
||||
...(keyword ? { driverName: keyword } : {}),
|
||||
posts: '押运员',
|
||||
})
|
||||
.then(records => {
|
||||
callback(
|
||||
records.map(item => ({
|
||||
@@ -4401,17 +4717,13 @@ export default {
|
||||
}
|
||||
},
|
||||
async loadDialogDriverOptions(keyword = '') {
|
||||
const carrier = this.getDialogCarrierFilter();
|
||||
if (!carrier.carrierId && !carrier.carrierName) {
|
||||
this.driverOptions = [];
|
||||
return [];
|
||||
}
|
||||
this.driverLoading = true;
|
||||
try {
|
||||
this.driverOptions = await fetchDriversByCarrierOrganizations(
|
||||
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' },
|
||||
carrier
|
||||
);
|
||||
this.driverOptions = await fetchDriverCandidates({
|
||||
size: 20,
|
||||
...(keyword ? { driverName: keyword } : {}),
|
||||
posts: '司机',
|
||||
});
|
||||
return this.driverOptions;
|
||||
} finally {
|
||||
this.driverLoading = false;
|
||||
@@ -4442,10 +4754,13 @@ export default {
|
||||
}
|
||||
this.taskCarrierLoading = true;
|
||||
try {
|
||||
const projectIds = [
|
||||
...new Set(this.selectedWaybillRows.map(row => row.projectId).filter(Boolean)),
|
||||
];
|
||||
const response = await loadingApi.getCarrierContracts(projectIds);
|
||||
// 配载单只有一个项目;传入子运单后按客户合同甲方匹配并取承运商合同乙方交集
|
||||
const projectId = this.selectedWaybillRows.map(row => row.projectId).find(Boolean);
|
||||
const waybillIds = this.selectedWaybillRows.map(row => row.id).filter(Boolean);
|
||||
const response = await loadingApi.getCarrierContracts(
|
||||
projectId ? [projectId] : [],
|
||||
waybillIds
|
||||
);
|
||||
if (requestId !== this.taskCarrierRequestId) return [];
|
||||
const contracts = response.data?.data || response.data || [];
|
||||
if (this.dialogForm.carrierType !== '自运') {
|
||||
@@ -4715,6 +5030,30 @@ export default {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.loading-manage-dialog__admission-tip {
|
||||
margin-bottom: 12px;
|
||||
padding: 12px 14px;
|
||||
background: #f5f9ff;
|
||||
border: 1px solid #d9ecff;
|
||||
border-radius: 4px;
|
||||
line-height: 1.6;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.loading-manage-dialog__admission-title {
|
||||
margin-bottom: 4px;
|
||||
color: #303133;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.loading-manage-dialog__admission-title + .loading-manage-dialog__admission-text + .loading-manage-dialog__admission-title {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.loading-manage-dialog__admission-text {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.loading-manage-dialog__candidate-search {
|
||||
padding: 0;
|
||||
margin-bottom: 0;
|
||||
@@ -4787,6 +5126,16 @@ export default {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.loading-manage-dialog__route-hint {
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.loading-manage-dialog__locked-tip {
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.loading-manage-dialog__route-type {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
|
||||
@@ -245,7 +245,7 @@
|
||||
<el-radio-group
|
||||
v-model="row.confirmMode"
|
||||
:disabled="dialogReadonly || !row.enabled"
|
||||
@change="handleReturnConfirmChange(row)"
|
||||
@change="syncNodeFields"
|
||||
>
|
||||
<el-radio label="yes">是</el-radio>
|
||||
<el-radio label="no_confirm_complete">无需确认完成</el-radio>
|
||||
@@ -253,10 +253,7 @@
|
||||
</div>
|
||||
<div class="process-config-form__require-line">
|
||||
<span class="process-config-form__require-label">确认完成</span>
|
||||
<el-checkbox
|
||||
v-model="row.confirmInternal"
|
||||
:disabled="dialogReadonly || !row.enabled || row.confirmMode !== 'yes'"
|
||||
@change="syncNodeFields"
|
||||
<el-checkbox v-model="row.confirmInternal" :disabled="true"
|
||||
>内部人员</el-checkbox
|
||||
>
|
||||
</div>
|
||||
@@ -549,15 +546,18 @@ export default {
|
||||
row.confirmDriver = row.confirmMode === 'yes';
|
||||
this.syncNodeFields();
|
||||
},
|
||||
handleReturnConfirmChange(row) {
|
||||
row.confirmInternal = row.confirmMode === 'yes';
|
||||
this.syncNodeFields();
|
||||
},
|
||||
enforceAcceptDriver(node) {
|
||||
if (!node || (node.key !== 'accept' && node.name !== '接单')) return node;
|
||||
node.confirmDriver = node.confirmMode === 'yes';
|
||||
return node;
|
||||
},
|
||||
// 回单「确认完成」只有内部人员一种角色,是否确认为「是」时即由内部人员确认,
|
||||
// 界面上只读展示,值始终由 confirmMode 推导
|
||||
enforceReturnInternal(node) {
|
||||
if (!node || (node.key !== 'return' && node.name !== '回单')) return node;
|
||||
node.confirmInternal = node.confirmMode === 'yes';
|
||||
return node;
|
||||
},
|
||||
initDeptOptions() {
|
||||
getDeptTree(this.userInfo.tenantId).then(res => {
|
||||
const deptColumn = this.findColumn(this.option.column, 'searchDeptId');
|
||||
@@ -724,7 +724,7 @@ export default {
|
||||
this.projectLoading = true;
|
||||
return getProjectList(1, 50, {
|
||||
projectName: keyword,
|
||||
approvalStatus: 'approved',
|
||||
approvalStatuses: 'approved,change_approved',
|
||||
allDept: this.isAdmin ? 1 : 0,
|
||||
})
|
||||
.then(res => {
|
||||
@@ -808,7 +808,10 @@ export default {
|
||||
this.syncNodeFields();
|
||||
},
|
||||
syncNodeFields() {
|
||||
this.nodeRows.forEach(item => this.enforceAcceptDriver(item));
|
||||
this.nodeRows.forEach(item => {
|
||||
this.enforceAcceptDriver(item);
|
||||
this.enforceReturnInternal(item);
|
||||
});
|
||||
const enabledNodes = this.nodeRows.filter(item => item.enabled);
|
||||
this.form.includedNodes = enabledNodes.map(item => item.name).join(',');
|
||||
this.form.nodeConfigJson = JSON.stringify(
|
||||
|
||||
@@ -955,7 +955,7 @@ import {
|
||||
getList as getCustomerArchiveList,
|
||||
getDetail as getCustomerArchiveDetail,
|
||||
} from '@/api/vehicle/customer-archive';
|
||||
import { getDeptTree, getPlatformCompanySelect } from '@/api/system/dept';
|
||||
import { getBusinessDeptSelect, getDeptTree, getPlatformCompanySelect } from '@/api/system/dept';
|
||||
import { getDictionary as getSystemDictionary } from '@/api/system/dict';
|
||||
import { getDictionary as getBizDictionary } from '@/api/system/dictbiz';
|
||||
import { getList as getUserList } from '@/api/system/user';
|
||||
@@ -1596,6 +1596,7 @@ export default {
|
||||
projectType: type === 'majorSupplement' ? '重大项目' : '普通项目',
|
||||
});
|
||||
this.fillDefaultUsers();
|
||||
this.fillDefaultBusinessDept();
|
||||
return;
|
||||
}
|
||||
if (!id) return;
|
||||
@@ -1670,6 +1671,12 @@ export default {
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
// 同步推送合规耗时较长,加遮罩防重复操作(照审批通过遮罩)
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
text: '正在推送合规,请稍候…',
|
||||
background: 'rgba(255, 255, 255, 0.6)',
|
||||
});
|
||||
this.hcsRepushingId = row.id;
|
||||
hcsRepush('project', row.id)
|
||||
.then(res => {
|
||||
@@ -1680,14 +1687,12 @@ export default {
|
||||
this.$message.success(res.data?.data || '推送合规成功');
|
||||
this.onLoad(this.page, this.query);
|
||||
})
|
||||
.catch(error => {
|
||||
// 拦截器已弹出后端 msg,此处不再重复提示
|
||||
if (!error?._notified) {
|
||||
this.$message.error(error?.response?.data?.msg || '推送合规失败');
|
||||
}
|
||||
.catch(() => {
|
||||
// 失败原因由 axios 拦截器统一弹出后端 msg,此处不再重复提示
|
||||
})
|
||||
.finally(() => {
|
||||
this.hcsRepushingId = null;
|
||||
loading.close();
|
||||
});
|
||||
});
|
||||
},
|
||||
@@ -1707,6 +1712,12 @@ export default {
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
// 同步推送合规耗时较长,加遮罩防重复操作(照审批通过遮罩)
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
text: '正在批量推送合规,请稍候…',
|
||||
background: 'rgba(255, 255, 255, 0.6)',
|
||||
});
|
||||
this.hcsRepushingId = 'batch';
|
||||
hcsRepushBatch('project', this.selectionList.map(item => item.id))
|
||||
.then(res => {
|
||||
@@ -1717,14 +1728,12 @@ export default {
|
||||
this.$message.success(res.data?.data || '批量推送合规完成');
|
||||
this.onLoad(this.page, this.query);
|
||||
})
|
||||
.catch(error => {
|
||||
// 拦截器已弹出后端 msg,此处不再重复提示
|
||||
if (!error?._notified) {
|
||||
this.$message.error(error?.response?.data?.msg || '批量推送合规失败');
|
||||
}
|
||||
.catch(() => {
|
||||
// 失败原因由 axios 拦截器统一弹出后端 msg,此处不再重复提示
|
||||
})
|
||||
.finally(() => {
|
||||
this.hcsRepushingId = null;
|
||||
loading.close();
|
||||
});
|
||||
});
|
||||
},
|
||||
@@ -2024,6 +2033,7 @@ export default {
|
||||
.filter(Boolean);
|
||||
this.syncSelectedCustomerOptions();
|
||||
this.ensureCurrentPlatformOption();
|
||||
this.ensureCurrentBusinessDeptOption();
|
||||
},
|
||||
fillDefaultUsers() {
|
||||
const userId = this.userInfo?.userId || this.userInfo?.id || '';
|
||||
@@ -2042,6 +2052,25 @@ export default {
|
||||
this.form.principalUserName = userName;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 新增/重大补录:业务部门默认取当前用户所属部门中第一个「是否业务部门=是」的组织。
|
||||
*/
|
||||
fillDefaultBusinessDept() {
|
||||
if (!['add', 'majorSupplement'].includes(this.dialogType)) return;
|
||||
if (this.form.businessDeptId) return;
|
||||
const raw = this.userInfo?.deptId || this.userInfo?.dept_id || '';
|
||||
const userDeptIds = String(raw)
|
||||
.split(/[,,]/)
|
||||
.map(item => String(item || '').trim())
|
||||
.filter(item => item && item !== '-1');
|
||||
if (!userDeptIds.length || !this.deptOptions.length) return;
|
||||
const matched = userDeptIds.find(id =>
|
||||
this.deptOptions.some(item => String(item.value) === String(id))
|
||||
);
|
||||
if (!matched) return;
|
||||
this.form.businessDeptId = matched;
|
||||
this.handleBusinessDeptChange(matched);
|
||||
},
|
||||
resolveUserRealName(row = {}, role) {
|
||||
return (
|
||||
row[`${role}RealName`] ||
|
||||
@@ -2624,17 +2653,61 @@ export default {
|
||||
this.$refs.projectForm?.validateField('projectType');
|
||||
},
|
||||
loadDeptOptions() {
|
||||
getDeptTree().then(res => {
|
||||
const deptTree = res.data.data || [];
|
||||
// 新增/编辑/重大补录/项目变更均可选择业务部门,排除外部组织
|
||||
const businessDeptTree = ['add', 'edit', 'majorSupplement', 'change'].includes(this.dialogType)
|
||||
? this.excludeExternalOrganization(deptTree)
|
||||
: deptTree;
|
||||
this.deptOptions = this.flattenDept(businessDeptTree);
|
||||
this.businessDeptTreeOptions = this.toDeptCascaderOptions(businessDeptTree);
|
||||
});
|
||||
Promise.all([getDeptTree(), getBusinessDeptSelect()])
|
||||
.then(([treeRes, bizRes]) => {
|
||||
const deptTree = this.excludeExternalOrganization(treeRes.data.data || []);
|
||||
const bizList = bizRes.data.data || [];
|
||||
const bizIds = new Set(
|
||||
bizList
|
||||
.map(item => (item.id == null || item.id === '' ? '' : String(item.id)))
|
||||
.filter(Boolean)
|
||||
);
|
||||
// 仅保留「是否业务部门=是」的节点及其祖先,祖先不可选
|
||||
const businessDeptTree = this.filterBusinessDeptTree(deptTree, bizIds);
|
||||
this.deptOptions = this.flattenDept(businessDeptTree).filter(item =>
|
||||
bizIds.has(String(item.value))
|
||||
);
|
||||
this.businessDeptTreeOptions = this.toDeptCascaderOptions(businessDeptTree, bizIds);
|
||||
this.fillDefaultBusinessDept();
|
||||
this.ensureCurrentBusinessDeptOption();
|
||||
})
|
||||
.catch(() => {
|
||||
this.deptOptions = [];
|
||||
this.businessDeptTreeOptions = [];
|
||||
});
|
||||
this.loadPlatformCompanyOptions();
|
||||
},
|
||||
/**
|
||||
* 剪枝组织树:只保留业务部门节点及其祖先路径。
|
||||
*/
|
||||
filterBusinessDeptTree(list = [], bizIds = new Set()) {
|
||||
return (list || []).reduce((result, item) => {
|
||||
const children = item.children?.length
|
||||
? this.filterBusinessDeptTree(item.children, bizIds)
|
||||
: [];
|
||||
const isBiz = bizIds.has(String(item.id));
|
||||
if (!isBiz && !children.length) {
|
||||
return result;
|
||||
}
|
||||
result.push({
|
||||
...item,
|
||||
children: children.length ? children : undefined,
|
||||
});
|
||||
return result;
|
||||
}, []);
|
||||
},
|
||||
ensureCurrentBusinessDeptOption() {
|
||||
const id = this.form.businessDeptId;
|
||||
if (!id) return;
|
||||
const exists = this.deptOptions.some(item => String(item.value) === String(id));
|
||||
if (exists) return;
|
||||
const label = this.form.businessDeptName || String(id);
|
||||
this.deptOptions.unshift({ label, rawLabel: label, value: id });
|
||||
const pathExists = this.findDeptPath(id, this.businessDeptTreeOptions);
|
||||
if (!pathExists) {
|
||||
this.businessDeptTreeOptions.unshift({ label, value: id });
|
||||
}
|
||||
},
|
||||
loadPlatformCompanyOptions() {
|
||||
getPlatformCompanySelect().then(res => {
|
||||
const list = res.data.data || [];
|
||||
@@ -2678,12 +2751,22 @@ export default {
|
||||
return result;
|
||||
}, []);
|
||||
},
|
||||
toDeptCascaderOptions(list = []) {
|
||||
return list.map(item => ({
|
||||
label: item.title || item.deptName || item.name,
|
||||
value: item.id,
|
||||
children: item.children?.length ? this.toDeptCascaderOptions(item.children) : undefined,
|
||||
}));
|
||||
toDeptCascaderOptions(list = [], bizIds = null) {
|
||||
return list.map(item => {
|
||||
const value = item.id == null || item.id === '' ? item.value : item.id;
|
||||
const option = {
|
||||
label: item.title || item.deptName || item.name || item.label,
|
||||
value: value == null || value === '' ? '' : String(value),
|
||||
children: item.children?.length
|
||||
? this.toDeptCascaderOptions(item.children, bizIds)
|
||||
: undefined,
|
||||
};
|
||||
// 仅业务部门可选;祖先节点保留路径但禁用
|
||||
if (bizIds && !bizIds.has(String(option.value))) {
|
||||
option.disabled = true;
|
||||
}
|
||||
return option;
|
||||
});
|
||||
},
|
||||
flattenDept(list = [], level = 0) {
|
||||
return list.reduce((result, item) => {
|
||||
|
||||
@@ -577,10 +577,7 @@ export default {
|
||||
this.$message.warning(`第${index + 1}条调度明细缺少承运商信息`);
|
||||
return;
|
||||
}
|
||||
if (!row.driverName) {
|
||||
this.$message.warning(`第${index + 1}条调度明细缺少司机信息`);
|
||||
return;
|
||||
}
|
||||
// 司机是否必填由调度明细弹窗按承运类型判定(承运商承运时司机选填),此处不再重复校验
|
||||
if (!row.vehicleNo) {
|
||||
this.$message.warning(`第${index + 1}条调度明细缺少车牌号`);
|
||||
return;
|
||||
|
||||
@@ -203,7 +203,11 @@
|
||||
@click="openBatchDialog(row)"
|
||||
>更换运单批次</el-link
|
||||
><el-link
|
||||
v-if="(row.processStatus !== '处理完成' || row.auditStatus === '待审核') && row.auditStatus !== '审核驳回'"
|
||||
v-if="
|
||||
row.processStatus !== '处理失败' &&
|
||||
(row.processStatus !== '处理完成' || row.auditStatus === '待审核') &&
|
||||
row.auditStatus !== '审核驳回'
|
||||
"
|
||||
type="primary"
|
||||
@click="openBatchDialog(row)"
|
||||
>更换运单批次</el-link
|
||||
@@ -320,7 +324,7 @@
|
||||
<el-form-item label="执行凭证" required
|
||||
><el-upload
|
||||
action="#"
|
||||
accept=".zip,.7z"
|
||||
accept=".zip"
|
||||
:auto-upload="false"
|
||||
:limit="1"
|
||||
:on-change="selectVoucherFile"
|
||||
@@ -332,7 +336,7 @@
|
||||
>上传</el-button
|
||||
><template #tip
|
||||
><span class="voucher-manage-page__upload-tip"
|
||||
>请上传车辆的执行凭证压缩包,支持zip、7z格式。 上传附件大于50M时为超大附件,确认提交后可在“批量补录->上传任务”中查看上传进度。</span
|
||||
>请上传车辆的执行凭证压缩包,仅支持 zip 格式;无法解析的文件将标记为「处理失败」,可重新上传。上传附件大于50M时为超大附件,确认提交后可在“批量补录->上传任务”中查看上传进度。</span
|
||||
></template
|
||||
></el-upload
|
||||
><el-progress
|
||||
@@ -549,7 +553,7 @@
|
||||
ref="resumeFileInput"
|
||||
class="voucher-manage-page__hidden-file"
|
||||
type="file"
|
||||
accept=".zip,.7z"
|
||||
accept=".zip"
|
||||
@change="resumeFileSelected"
|
||||
/>
|
||||
</template>
|
||||
@@ -998,6 +1002,12 @@ const uploadVoucher = async file => {
|
||||
};
|
||||
const selectVoucherFile = file => {
|
||||
if (!file?.raw) return;
|
||||
const fileName = String(file.raw.name || '').toLowerCase();
|
||||
if (!fileName.endsWith('.zip')) {
|
||||
ElMessage.warning('仅支持 zip 压缩包,无法解析的文件将标记为处理失败');
|
||||
clearFile();
|
||||
return;
|
||||
}
|
||||
selectedVoucherFile.value = file.raw;
|
||||
editing.fileName = file.raw.name;
|
||||
editing.fileUrl = '';
|
||||
|
||||
@@ -271,7 +271,7 @@ export default {
|
||||
this.unwrapData(
|
||||
await billLedgerApi.getAvailableOptions(
|
||||
this.billDialog.keyword,
|
||||
this.form.deptId,
|
||||
undefined,
|
||||
this.form.billLedgerId
|
||||
)
|
||||
) || [];
|
||||
|
||||
@@ -251,10 +251,10 @@
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="不含税单价" min-width="130">
|
||||
<el-table-column label="合计金额" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-model="row.unitPriceNoTax"
|
||||
v-model="row.totalAmount"
|
||||
:disabled="readonly"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
@@ -263,9 +263,6 @@
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="不含税金额" min-width="130" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.amountNoTax) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="税率" min-width="110">
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
@@ -282,11 +279,14 @@
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="税额" min-width="120">
|
||||
<template #default="{ row }">{{ formatMoney(row.taxAmount) }}</template>
|
||||
<el-table-column label="不含税单价" min-width="140" align="right">
|
||||
<template #default="{ row }">{{ formatUnitPrice(row.unitPriceNoTax) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="合计" min-width="130" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.totalAmount) }}</template>
|
||||
<el-table-column label="不含税金额" min-width="130" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.amountNoTax) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="税额" min-width="120" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.taxAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" min-width="170">
|
||||
<template #default="{ row }">
|
||||
@@ -1108,15 +1108,30 @@ export default {
|
||||
addLine(sheet) {
|
||||
sheet.lines.push(emptyLine());
|
||||
},
|
||||
/**
|
||||
* 填写合计金额 + 税率后反算:
|
||||
* 合计金额 = 数量 × 不含税单价 × (1 + 税率)
|
||||
* 不含税单价 = 合计金额 / 数量 / (1 + 税率),保留 6 位小数
|
||||
* 不含税金额 = 数量 × 不含税单价,保留 2 位小数
|
||||
*/
|
||||
recalculateLine(row) {
|
||||
const quantity = Number(row.quantity || 0);
|
||||
const unitPrice = Number(row.unitPriceNoTax || 0);
|
||||
const amountNoTax = Number((quantity * unitPrice).toFixed(2));
|
||||
const totalAmount = Number(Number(row.totalAmount || 0).toFixed(2));
|
||||
const rate = Number(row.taxRate || 0) / 100;
|
||||
row.totalAmount = totalAmount;
|
||||
row.amountWithTax = totalAmount;
|
||||
if (quantity <= 0 || totalAmount <= 0) {
|
||||
row.unitPriceNoTax = 0;
|
||||
row.amountNoTax = 0;
|
||||
row.taxAmount = 0;
|
||||
return;
|
||||
}
|
||||
const factor = 1 + rate;
|
||||
const unitPriceNoTax = Number((totalAmount / quantity / factor).toFixed(6));
|
||||
const amountNoTax = Number((quantity * unitPriceNoTax).toFixed(2));
|
||||
row.unitPriceNoTax = unitPriceNoTax;
|
||||
row.amountNoTax = amountNoTax;
|
||||
row.taxAmount = rate ? Number((amountNoTax * rate).toFixed(2)) : 0;
|
||||
row.totalAmount = Number((amountNoTax + row.taxAmount).toFixed(2));
|
||||
row.amountWithTax = row.totalAmount;
|
||||
row.taxAmount = Number((totalAmount - amountNoTax).toFixed(2));
|
||||
},
|
||||
validateBusiness() {
|
||||
if (!this.selectedDetailIds.length) throw new Error('请至少选择一条开票明细');
|
||||
@@ -1129,12 +1144,14 @@ export default {
|
||||
for (const line of sheet.lines) {
|
||||
if (!line.goodsCategory || !line.goodsName) throw new Error('请完整填写商品和服务信息');
|
||||
if (
|
||||
[line.quantity, line.unitPriceNoTax, line.amountNoTax, line.taxRate].some(
|
||||
[line.quantity, line.totalAmount, line.unitPriceNoTax, line.amountNoTax, line.taxRate].some(
|
||||
Number.isNaN
|
||||
)
|
||||
) {
|
||||
throw new Error('开票商品行金额格式不正确');
|
||||
}
|
||||
if (Number(line.quantity || 0) <= 0) throw new Error('开票商品行数量必须大于0');
|
||||
if (Number(line.totalAmount || 0) <= 0) throw new Error('开票商品行合计金额必须大于0');
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1234,6 +1251,9 @@ export default {
|
||||
formatMoney(value) {
|
||||
return Number(value || 0).toFixed(2);
|
||||
},
|
||||
formatUnitPrice(value) {
|
||||
return Number(value || 0).toFixed(6);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -249,7 +249,7 @@
|
||||
<el-table-column prop="paymentRatio" label="付款比例(%)" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-if="multipleSettlementRows && !readonly"
|
||||
v-if="!readonly"
|
||||
:model-value="row.paymentRatio"
|
||||
:min="0"
|
||||
:max="rowPaymentRatioMax(row)"
|
||||
@@ -267,7 +267,7 @@
|
||||
<el-table-column prop="appliedAmount" label="付款金额" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-if="multipleSettlementRows && !readonly"
|
||||
v-if="!readonly"
|
||||
:model-value="row.appliedAmount"
|
||||
:min="0"
|
||||
:max="Number(row.payableAmount || 0)"
|
||||
@@ -1025,62 +1025,37 @@ export default {
|
||||
referenceSettlementCount() {
|
||||
return this.referenceSettlementNos.length;
|
||||
},
|
||||
multipleSettlementRows() {
|
||||
return this.referenceRows.length > 1;
|
||||
},
|
||||
// 结算单号展示:单张显示单号;多张仅提示数量,明细在下方「结算信息」表,悬停可看全部单号
|
||||
referenceDisplayText() {
|
||||
if (this.referenceSettlementCount > 1) return `已选 ${this.referenceSettlementCount} 张结算单`;
|
||||
return this.referenceLabel;
|
||||
},
|
||||
settlementRows() {
|
||||
if (this.referenceRows.length > 1) {
|
||||
// 多结算单:行内比例/金额独立维护,不回落到表头比例(表头与结算信息不关联)
|
||||
return this.referenceRows.map(item => {
|
||||
const paymentRatio =
|
||||
item.paymentRatio === undefined || item.paymentRatio === null || item.paymentRatio === ''
|
||||
? 0
|
||||
: Number(item.paymentRatio);
|
||||
const appliedAmount = Number(item.appliedAmount || 0);
|
||||
return {
|
||||
...item,
|
||||
paymentRatio,
|
||||
appliedAmount,
|
||||
...this.buildSettlementRatioHint({ ...item, paymentRatio }),
|
||||
};
|
||||
});
|
||||
}
|
||||
if (this.referenceRows.length) {
|
||||
return this.referenceRows.map(item => {
|
||||
const appliedAmount =
|
||||
item.fixedAppliedAmount === true
|
||||
? Number(item.appliedAmount || 0)
|
||||
: Number(
|
||||
(
|
||||
(Number(item.settlementAmount || 0) * Number(this.form.paymentRatio || 0)) /
|
||||
100
|
||||
).toFixed(2)
|
||||
);
|
||||
return {
|
||||
...item,
|
||||
paymentRatio: this.form.paymentRatio,
|
||||
appliedAmount,
|
||||
...this.buildSettlementRatioHint(item),
|
||||
};
|
||||
});
|
||||
}
|
||||
if (!this.referenceLabel) return [];
|
||||
return [
|
||||
{
|
||||
settlementNo: this.referenceLabel,
|
||||
settlementAmount: this.form.settlementAmount,
|
||||
payableAmount: this.form.payableAmount,
|
||||
paymentRatio: this.form.paymentRatio,
|
||||
appliedAmount: this.form.appliedAmount,
|
||||
paymentSequence: 1,
|
||||
...this.buildSettlementRatioHint({ paymentSequence: 1 }),
|
||||
},
|
||||
];
|
||||
const source = this.referenceRows.length
|
||||
? this.referenceRows
|
||||
: this.referenceLabel
|
||||
? [
|
||||
{
|
||||
settlementNo: this.referenceLabel,
|
||||
settlementAmount: this.form.settlementAmount,
|
||||
payableAmount: this.form.payableAmount,
|
||||
paymentRatio: null,
|
||||
appliedAmount: null,
|
||||
paymentSequence: 1,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
// 结算行比例/金额只取行内数据,不回落到付款基本信息
|
||||
return source.map(item => {
|
||||
const paymentRatio = this.nullableNumber(item.paymentRatio);
|
||||
const appliedAmount = this.nullableNumber(item.appliedAmount);
|
||||
return {
|
||||
...item,
|
||||
paymentRatio,
|
||||
appliedAmount,
|
||||
...this.buildSettlementRatioHint({ ...item, paymentRatio }),
|
||||
};
|
||||
});
|
||||
},
|
||||
highRiskPayee() {
|
||||
// 收款方 C101-1 高风险/黑名单,或客商等级 D/E/F(本地评级口径),命中须上传特批附件
|
||||
@@ -1149,11 +1124,12 @@ export default {
|
||||
if (
|
||||
this.amountSyncing ||
|
||||
this.form.paymentType === 'project_advance' ||
|
||||
!this.paymentAmountBase
|
||||
!this.paymentAmountBase ||
|
||||
this.nullableNumber(this.form.paymentRatio) === null
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// 结算金额变化时按上方比例重算申请付款金额,仍不联动结算信息明细
|
||||
// 结算金额变化时只按上方比例重算申请付款金额,不改结算信息明细
|
||||
this.amountSyncing = true;
|
||||
this.form.appliedAmount = Number(
|
||||
((this.paymentAmountBase * Number(this.form.paymentRatio || 0)) / 100).toFixed(2)
|
||||
@@ -1251,14 +1227,6 @@ export default {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
if (
|
||||
selected.billType &&
|
||||
selected.billType !== 'received' &&
|
||||
!this.billMatchesSettlementParties(selected)
|
||||
) {
|
||||
callback(new Error('汇票付款仅可选择收票类型,或出票单位与收票单位均为当前结算收付款方的汇票'));
|
||||
return;
|
||||
}
|
||||
if (selected.maturityDate) {
|
||||
const maturity = this.$dayjs(selected.maturityDate).startOf('day');
|
||||
const today = this.$dayjs().startOf('day');
|
||||
@@ -1562,8 +1530,6 @@ export default {
|
||||
paymentSequence: amount.paymentSequence,
|
||||
}),
|
||||
settlementNo: item.formalSettlementNo || item.settlementNo || '',
|
||||
appliedAmount: Number(item.appliedAmount || 0),
|
||||
fixedAppliedAmount: true,
|
||||
};
|
||||
});
|
||||
this.seedMultipleRowPayments(referenceRows);
|
||||
@@ -1575,27 +1541,6 @@ export default {
|
||||
(total, item) => total + Number(item.payableAmount || 0),
|
||||
0
|
||||
);
|
||||
const multiple = referenceRows.length > 1;
|
||||
// 多结算单:表头比例/申请金额独立;单结算单:沿用行金额或按比例计算
|
||||
const paymentRatio = Number(this.form.paymentRatio || 0);
|
||||
const rowsAppliedTotal = referenceRows.reduce(
|
||||
(total, item) => total + Number(item.appliedAmount || 0),
|
||||
0
|
||||
);
|
||||
const headerAppliedAmount = multiple
|
||||
? Number(((settlementAmount * paymentRatio) / 100).toFixed(2))
|
||||
: Number(
|
||||
(
|
||||
rowsAppliedTotal > 0
|
||||
? rowsAppliedTotal
|
||||
: (payableAmount * paymentRatio) / 100
|
||||
).toFixed(2)
|
||||
);
|
||||
const headerPaymentRatio = multiple
|
||||
? paymentRatio
|
||||
: settlementAmount
|
||||
? Number(((headerAppliedAmount / settlementAmount) * 100).toFixed(2))
|
||||
: paymentRatio;
|
||||
const settlementIds = rows.map(item => item.settlementId || item.id).filter(Boolean);
|
||||
this.amountSyncing = true;
|
||||
this.referenceRows = referenceRows;
|
||||
@@ -1622,8 +1567,7 @@ export default {
|
||||
settlementAmount: Number(settlementAmount.toFixed(2)),
|
||||
payableAmount: Number(payableAmount.toFixed(2)),
|
||||
billType: '',
|
||||
paymentRatio: headerPaymentRatio,
|
||||
appliedAmount: headerAppliedAmount,
|
||||
appliedAmount: this.headerAppliedAmountByRatio(settlementAmount),
|
||||
invoices: rows.flatMap(item =>
|
||||
this.createInvoiceRows(item.invoices || [], item.formalSettlementNo || item.settlementNo)
|
||||
),
|
||||
@@ -1679,7 +1623,7 @@ export default {
|
||||
referenceType: isPre ? 'pre' : 'formal',
|
||||
settlementAmount: Number(Number(item.settlementAmount || 0).toFixed(2)),
|
||||
payableAmount: Number(Number(item.payableAmount ?? item.settlementAmount ?? 0).toFixed(2)),
|
||||
appliedAmount: Number(Number(item.appliedAmount || 0).toFixed(2)),
|
||||
appliedAmount: this.nullableNumber(item.appliedAmount),
|
||||
paymentRatio: Number(item.settlementAmount || 0)
|
||||
? Number(
|
||||
(
|
||||
@@ -1688,11 +1632,10 @@ export default {
|
||||
).toFixed(2)
|
||||
)
|
||||
: null,
|
||||
fixedAppliedAmount: this.readonly,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
if (rows.length > 1) {
|
||||
if (rows.length) {
|
||||
this.form.preSettlementIds = rows.filter(item => item.referenceType === 'pre').map(item => item.id);
|
||||
this.form.settlementIds = rows.filter(item => item.referenceType === 'formal').map(item => item.id);
|
||||
if (this.form.preSettlementIds.length) this.form.preSettlementId = this.form.preSettlementIds[0];
|
||||
@@ -1726,18 +1669,27 @@ export default {
|
||||
settlementAmount: Number(Number(settlementAmount || 0).toFixed(2)),
|
||||
payableAmount: Number(Math.max(0, payableAmount).toFixed(2)),
|
||||
paymentSequence: Number.isFinite(paymentSequence) && paymentSequence > 0 ? paymentSequence : 1,
|
||||
paymentRatio: null,
|
||||
appliedAmount: null,
|
||||
};
|
||||
},
|
||||
nullableNumber(value) {
|
||||
if (value === undefined || value === null || value === '') return null;
|
||||
const amount = Number(value);
|
||||
return Number.isFinite(amount) ? amount : null;
|
||||
},
|
||||
/** 付款基本信息内部:已填写比例时,按结算金额重算申请付款金额;不读取结算行付款金额。 */
|
||||
headerAppliedAmountByRatio(settlementAmount) {
|
||||
if (this.nullableNumber(this.form.paymentRatio) === null) return this.form.appliedAmount;
|
||||
return Number(((Number(settlementAmount || 0) * Number(this.form.paymentRatio)) / 100).toFixed(2));
|
||||
},
|
||||
/**
|
||||
* 按合同付款比例配置,判断当前付款次序是否超上限。
|
||||
* @param {Object} row 结算信息行(含 paymentSequence)
|
||||
*/
|
||||
buildSettlementRatioHint(row = {}) {
|
||||
const sequence = Number(row.paymentSequence || 1);
|
||||
const rawRatio =
|
||||
row.paymentRatio !== undefined && row.paymentRatio !== null && row.paymentRatio !== ''
|
||||
? row.paymentRatio
|
||||
: this.form.paymentRatio;
|
||||
const rawRatio = row.paymentRatio;
|
||||
const paymentRatio = Number(rawRatio);
|
||||
const ratioConfig = this.contractPaymentRatioRows[sequence - 1];
|
||||
const ratioLimitValue = ratioConfig?.ratioLimit;
|
||||
@@ -1880,21 +1832,6 @@ export default {
|
||||
(total, item) => total + Number(item.payableAmount || 0),
|
||||
0
|
||||
);
|
||||
const multiple = this.referenceRows.length > 1;
|
||||
const paymentRatio = Number(this.form.paymentRatio || 0);
|
||||
const rowsAppliedTotal = this.referenceRows.reduce(
|
||||
(total, item) => total + Number(item.appliedAmount || 0),
|
||||
0
|
||||
);
|
||||
// 多结算单:表头申请金额只由上方比例计算;单结算单仍按可付款金额×比例
|
||||
const headerAppliedAmount = multiple
|
||||
? Number(((settlementAmount * paymentRatio) / 100).toFixed(2))
|
||||
: Number(((payableAmount * paymentRatio) / 100).toFixed(2));
|
||||
const headerPaymentRatio = multiple
|
||||
? paymentRatio
|
||||
: settlementAmount && rowsAppliedTotal > 0
|
||||
? Number(((rowsAppliedTotal / settlementAmount) * 100).toFixed(2))
|
||||
: paymentRatio;
|
||||
// 尾款付款场景下选择预付结算单时保留尾款付款类型,不强制改为进度预付
|
||||
this.amountSyncing = true;
|
||||
Object.assign(this.form, {
|
||||
@@ -1920,8 +1857,7 @@ export default {
|
||||
settlementAmount: Number(settlementAmount.toFixed(2)),
|
||||
payableAmount: Number(payableAmount.toFixed(2)),
|
||||
billType: '',
|
||||
paymentRatio: headerPaymentRatio,
|
||||
appliedAmount: headerAppliedAmount,
|
||||
appliedAmount: this.headerAppliedAmountByRatio(settlementAmount),
|
||||
});
|
||||
this.rememberSettlementParties(first.payerName, first.payeeName);
|
||||
this.paymentRecords = [];
|
||||
@@ -2483,8 +2419,7 @@ export default {
|
||||
}
|
||||
},
|
||||
payeeReceiptLabel(item) {
|
||||
const name = item.accountName || item.accountHolderName || '未命名收款方';
|
||||
return item.bankAccount ? `${name}(${item.bankAccount})` : name;
|
||||
return item.accountName || item.accountHolderName || '未命名收款方';
|
||||
},
|
||||
receiptAccountLabel(item) {
|
||||
return [item.accountName || item.accountHolderName, item.bankName, item.bankAccount]
|
||||
@@ -2642,7 +2577,7 @@ export default {
|
||||
this.unwrapData(
|
||||
await billLedgerApi.getAvailableOptions(
|
||||
keyword,
|
||||
this.form.deptId,
|
||||
undefined,
|
||||
selectedId,
|
||||
this.settlementPayerName,
|
||||
this.settlementPayeeName
|
||||
@@ -2667,7 +2602,7 @@ export default {
|
||||
this.billPage.current,
|
||||
this.billPage.size,
|
||||
String(this.billQuery.keyword || '').trim() || undefined,
|
||||
this.form.deptId,
|
||||
undefined,
|
||||
this.form.billLedgerId,
|
||||
this.settlementPayerName,
|
||||
this.settlementPayeeName
|
||||
@@ -2706,14 +2641,6 @@ export default {
|
||||
);
|
||||
},
|
||||
selectBill(row) {
|
||||
if (
|
||||
row?.billType &&
|
||||
row.billType !== 'received' &&
|
||||
!this.billMatchesSettlementParties(row)
|
||||
) {
|
||||
ElMessage.warning('汇票付款仅可选择收票类型,或出票单位与收票单位均为当前结算收付款方的汇票');
|
||||
return;
|
||||
}
|
||||
if (row?.maturityDate) {
|
||||
const maturity = this.$dayjs(row.maturityDate).startOf('day');
|
||||
const today = this.$dayjs().startOf('day');
|
||||
@@ -3136,22 +3063,54 @@ export default {
|
||||
return Number(Math.min(100, (payableAmount / settlementAmount) * 100).toFixed(2));
|
||||
},
|
||||
/**
|
||||
* 多结算单行金额规范化:仅在行内反算比例,不使用表头比例灌入(表头与结算信息不关联)。
|
||||
* 结算行金额规范化:只在行内反算比例,不使用付款基本信息的比例和申请金额。
|
||||
*/
|
||||
seedMultipleRowPayments(rows = []) {
|
||||
if (rows.length <= 1) return;
|
||||
rows.forEach(item => {
|
||||
const settlementAmount = Number(item.settlementAmount || 0);
|
||||
const payableAmount = Number(item.payableAmount || 0);
|
||||
let applied = Number(item.appliedAmount || 0);
|
||||
if (!Number.isFinite(applied) || applied < 0) applied = 0;
|
||||
if (applied > payableAmount) applied = Number(payableAmount.toFixed(2));
|
||||
item.appliedAmount = Number(applied.toFixed(2));
|
||||
item.paymentRatio = settlementAmount
|
||||
? Number(((item.appliedAmount / settlementAmount) * 100).toFixed(2))
|
||||
: 0;
|
||||
if (this.nullableNumber(item.appliedAmount) === null && this.nullableNumber(item.paymentRatio) === null) {
|
||||
item.appliedAmount = null;
|
||||
item.paymentRatio = null;
|
||||
return;
|
||||
}
|
||||
this.syncRowPayment(item, 'amount');
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 结算行付款比例与付款金额联动。比例 = 付款金额 / 结算总金额。
|
||||
* @param {'ratio'|'amount'} source 以哪一侧为准
|
||||
*/
|
||||
syncRowPayment(row, source) {
|
||||
const settlementAmount = Number(row.settlementAmount || 0);
|
||||
const payableAmount = Number(row.payableAmount || 0);
|
||||
if (source === 'ratio') {
|
||||
const ratio = this.nullableNumber(row.paymentRatio);
|
||||
if (ratio === null) {
|
||||
row.paymentRatio = null;
|
||||
row.appliedAmount = null;
|
||||
return;
|
||||
}
|
||||
let applied = Number(((settlementAmount * ratio) / 100).toFixed(2));
|
||||
if (applied > payableAmount) applied = Number(payableAmount.toFixed(2));
|
||||
row.appliedAmount = applied;
|
||||
row.paymentRatio = settlementAmount
|
||||
? Number(((applied / settlementAmount) * 100).toFixed(2))
|
||||
: Number(ratio.toFixed(2));
|
||||
return;
|
||||
}
|
||||
const appliedValue = this.nullableNumber(row.appliedAmount);
|
||||
if (appliedValue === null) {
|
||||
row.appliedAmount = null;
|
||||
row.paymentRatio = null;
|
||||
return;
|
||||
}
|
||||
let applied = appliedValue;
|
||||
if (applied < 0) applied = 0;
|
||||
if (applied > payableAmount) applied = payableAmount;
|
||||
row.appliedAmount = Number(applied.toFixed(2));
|
||||
row.paymentRatio = settlementAmount
|
||||
? Number(((row.appliedAmount / settlementAmount) * 100).toFixed(2))
|
||||
: 0;
|
||||
},
|
||||
findReferenceRow(row) {
|
||||
return this.referenceRows.find(
|
||||
item =>
|
||||
@@ -3160,55 +3119,19 @@ export default {
|
||||
item.settlementNo === row.settlementNo
|
||||
);
|
||||
},
|
||||
/** 结算信息付款金额合计 */
|
||||
settlementRowsAppliedTotal(rows = this.referenceRows) {
|
||||
return (rows || []).reduce((total, item) => total + Number(item.appliedAmount || 0), 0);
|
||||
},
|
||||
/** 结算信息合计不得超过上方申请付款金额 */
|
||||
ensureSettlementAppliedWithinHeader(showWarning = true) {
|
||||
if (!this.multipleSettlementRows) return true;
|
||||
const headerApplied = Number(this.form.appliedAmount || 0);
|
||||
const rowsTotal = Number(this.settlementRowsAppliedTotal().toFixed(2));
|
||||
if (rowsTotal - headerApplied > 1e-8) {
|
||||
if (showWarning) {
|
||||
this.$message.warning(
|
||||
`结算信息付款金额合计(${rowsTotal.toFixed(2)})不能大于申请付款金额(${headerApplied.toFixed(2)})`
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
handleRowPaymentRatioChange(row, value) {
|
||||
const target = this.findReferenceRow(row);
|
||||
if (!target) return;
|
||||
const settlementAmount = Number(target.settlementAmount || 0);
|
||||
const payableAmount = Number(target.payableAmount || 0);
|
||||
const ratio = value === null || value === undefined || value === '' ? 0 : Number(value);
|
||||
let applied = Number(((settlementAmount * ratio) / 100).toFixed(2));
|
||||
if (applied > payableAmount) applied = Number(payableAmount.toFixed(2));
|
||||
target.appliedAmount = applied;
|
||||
target.paymentRatio = settlementAmount
|
||||
? Number(((applied / settlementAmount) * 100).toFixed(2))
|
||||
: Number(ratio.toFixed(2));
|
||||
// 仅刷新结算金额/可付款金额合计,不回写表头申请金额与比例
|
||||
target.paymentRatio = value;
|
||||
this.syncRowPayment(target, 'ratio');
|
||||
this.syncHeaderSettlementTotalsFromRows();
|
||||
this.ensureSettlementAppliedWithinHeader(true);
|
||||
},
|
||||
handleRowAppliedAmountChange(row, value) {
|
||||
const target = this.findReferenceRow(row);
|
||||
if (!target) return;
|
||||
const settlementAmount = Number(target.settlementAmount || 0);
|
||||
const payableAmount = Number(target.payableAmount || 0);
|
||||
let applied = value === null || value === undefined || value === '' ? 0 : Number(value);
|
||||
if (!Number.isFinite(applied) || applied < 0) applied = 0;
|
||||
if (applied > payableAmount) applied = payableAmount;
|
||||
target.appliedAmount = Number(applied.toFixed(2));
|
||||
target.paymentRatio = settlementAmount
|
||||
? Number(((target.appliedAmount / settlementAmount) * 100).toFixed(2))
|
||||
: 0;
|
||||
target.appliedAmount = value;
|
||||
this.syncRowPayment(target, 'amount');
|
||||
this.syncHeaderSettlementTotalsFromRows();
|
||||
this.ensureSettlementAppliedWithinHeader(true);
|
||||
},
|
||||
/** 多结算单时仅同步结算金额/可付款金额合计,申请付款金额与比例由上方独立维护 */
|
||||
syncHeaderSettlementTotalsFromRows() {
|
||||
@@ -3317,8 +3240,8 @@ export default {
|
||||
settlementLines: (this.referenceRows || []).map(item => ({
|
||||
formalSettlementId: item.referenceType === 'formal' ? item.id : null,
|
||||
preSettlementId: item.referenceType === 'pre' ? item.id : null,
|
||||
paymentRatio: item.paymentRatio,
|
||||
appliedAmount: item.appliedAmount,
|
||||
paymentRatio: this.nullableNumber(item.paymentRatio),
|
||||
appliedAmount: this.nullableNumber(item.appliedAmount),
|
||||
})),
|
||||
paymentMethod,
|
||||
billLedgerId,
|
||||
@@ -3379,21 +3302,22 @@ export default {
|
||||
ElMessage.warning('申请付款金额必填且必须大于0');
|
||||
return false;
|
||||
}
|
||||
if (this.multipleSettlementRows) {
|
||||
const invalidRow = this.referenceRows.find(
|
||||
item => Number(item.appliedAmount || 0) > Number(item.payableAmount || 0)
|
||||
);
|
||||
if (this.isSettlementRelatedType && this.referenceRows.length) {
|
||||
const invalidRow = this.referenceRows.find(item => {
|
||||
const rowApplied = Number(item.appliedAmount || 0);
|
||||
return !Number.isFinite(rowApplied) || rowApplied <= 0;
|
||||
});
|
||||
if (invalidRow) {
|
||||
this.clearMessages();
|
||||
ElMessage.warning(`${invalidRow.settlementNo || '结算单'}付款金额不能超过可付款金额`);
|
||||
ElMessage.warning(`${invalidRow.settlementNo || '结算单'}请填写付款金额,且必须大于0`);
|
||||
return false;
|
||||
}
|
||||
const rowsTotal = Number(this.settlementRowsAppliedTotal().toFixed(2));
|
||||
if (rowsTotal - appliedAmount > 1e-8) {
|
||||
const exceededRow = this.referenceRows.find(
|
||||
item => Number(item.appliedAmount || 0) > Number(item.payableAmount || 0)
|
||||
);
|
||||
if (exceededRow) {
|
||||
this.clearMessages();
|
||||
ElMessage.warning(
|
||||
`结算信息付款金额合计(${rowsTotal.toFixed(2)})不能大于申请付款金额(${appliedAmount.toFixed(2)})`
|
||||
);
|
||||
ElMessage.warning(`${exceededRow.settlementNo || '结算单'}付款金额不能超过可付款金额`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1388,16 +1388,12 @@ export default {
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
summaryTotal(value) {
|
||||
this.form.settlementAmount = Number(value || 0).toFixed(2);
|
||||
this.form.localSettlementAmount = (
|
||||
Number(value || 0) * Number(this.form.exchangeRate || 1)
|
||||
).toFixed(2);
|
||||
summaryTotal() {
|
||||
this.syncSettlementAmountsFromSummary();
|
||||
},
|
||||
'form.exchangeRate'(value) {
|
||||
this.form.localSettlementAmount = (
|
||||
Number(this.summaryTotal || 0) * Number(value || 1)
|
||||
).toFixed(2);
|
||||
'form.exchangeRate'() {
|
||||
if (this._skipExchangeRateRebuild) return;
|
||||
this.buildSummaryFeesFromDetails();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
@@ -1683,8 +1679,9 @@ export default {
|
||||
current.settlementAmount = current.originalAmount;
|
||||
summaryMap.set(key, current);
|
||||
});
|
||||
// 先保留预结算手工费用行,再按明细本位币 + 结算汇率重算系统归集行
|
||||
this.summaryFees = [...summaryMap.values()];
|
||||
if (!this.summaryFees.length) await this.refreshSummaryFees();
|
||||
await this.refreshSummaryFees();
|
||||
},
|
||||
async loadAllContracts() {
|
||||
const response = await getContractOptions('');
|
||||
@@ -1786,13 +1783,19 @@ export default {
|
||||
},
|
||||
async fetchBfiExchangeRate() {
|
||||
const currency = this.form.currency;
|
||||
if (!currency || currency === 'RMB') return;
|
||||
if (!currency || currency === 'RMB') {
|
||||
this.buildSummaryFeesFromDetails();
|
||||
return;
|
||||
}
|
||||
if (!this.form.exchangeRateDate) return;
|
||||
try {
|
||||
const { data } = await getBfiExchangeRate(currency, this.form.exchangeRateDate);
|
||||
const rateData = data?.data;
|
||||
if (rateData?.excval != null) {
|
||||
this._skipExchangeRateRebuild = true;
|
||||
this.form.exchangeRate = Number(rateData.excval);
|
||||
this._skipExchangeRateRebuild = false;
|
||||
this.buildSummaryFeesFromDetails();
|
||||
} else {
|
||||
this.$message.warning(`未查询到 ${currency} 的BFI汇率数据`);
|
||||
}
|
||||
@@ -1800,6 +1803,37 @@ export default {
|
||||
this.$message.warning('查询BFI汇率失败,请手动输入结算汇率');
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 明细金额按本位币理解;外币结算时结算合计原金额/结算金额 = 本位币金额 / 结算汇率。
|
||||
*/
|
||||
convertLocalToSettlementAmount(localAmount) {
|
||||
const amount = Number(localAmount || 0);
|
||||
if (!Number.isFinite(amount)) return 0;
|
||||
if (this.isLocalCurrencySettlement) {
|
||||
return Number(amount.toFixed(2));
|
||||
}
|
||||
const rate = Number(this.form.exchangeRate || 0);
|
||||
if (rate <= 0) return 0;
|
||||
return Number((amount / rate).toFixed(2));
|
||||
},
|
||||
/**
|
||||
* 结算金额取结算合计原币汇总值;本位币合计 = 结算金额 × 结算汇率。
|
||||
*/
|
||||
syncSettlementAmountsFromSummary() {
|
||||
const settlementTotal = this.summaryFees.reduce(
|
||||
(total, row) => total + Number(row.settlementAmount || 0),
|
||||
0
|
||||
);
|
||||
this.form.settlementAmount = Number(settlementTotal || 0).toFixed(2);
|
||||
if (this.isLocalCurrencySettlement) {
|
||||
this.form.localSettlementAmount = this.form.settlementAmount;
|
||||
this.form.exchangeRate = 1;
|
||||
return;
|
||||
}
|
||||
const rate = Number(this.form.exchangeRate || 0);
|
||||
this.form.localSettlementAmount =
|
||||
rate > 0 ? (Number(this.form.settlementAmount || 0) * rate).toFixed(2) : '0.00';
|
||||
},
|
||||
openCandidateDialog() {
|
||||
this.candidate.visible = true;
|
||||
this.candidate.page.current = 1;
|
||||
@@ -2021,8 +2055,9 @@ export default {
|
||||
.map(row => [`${row.feeType}\u0000${row.feeItem}`, row])
|
||||
);
|
||||
const append = (feeItem, originalValue, settlementValue, fallbackFeeType = '') => {
|
||||
const originalAmount = Number(originalValue || 0);
|
||||
const settlementAmount = Number(settlementValue || 0);
|
||||
// 明细费用按本位币归集,外币结算时再按汇率换算成结算原币金额
|
||||
const originalAmount = this.convertLocalToSettlementAmount(originalValue);
|
||||
const settlementAmount = this.convertLocalToSettlementAmount(settlementValue);
|
||||
if (!feeItem || !Number.isFinite(originalAmount) || !Number.isFinite(settlementAmount)) {
|
||||
return;
|
||||
}
|
||||
@@ -2060,6 +2095,7 @@ export default {
|
||||
});
|
||||
const manualRows = this.summaryFees.filter(row => Number(row.manualFlag) === 1);
|
||||
this.summaryFees = [...generatedMap.values(), ...manualRows];
|
||||
this.syncSettlementAmountsFromSummary();
|
||||
},
|
||||
// 每条明细对应的费用行:[调整前基准行, 调整后当前行],缺少费用快照时退化为明细自身的汇总行。
|
||||
resolveDetailFeePairs(detail) {
|
||||
|
||||
@@ -1332,9 +1332,12 @@ export default {
|
||||
if (value) this.initialize();
|
||||
},
|
||||
},
|
||||
summaryTotal(value) {
|
||||
this.form.settlementAmount = Number(value || 0).toFixed(2);
|
||||
this.recalculateLocalAmount();
|
||||
summaryTotal() {
|
||||
this.syncSettlementAmountsFromSummary();
|
||||
},
|
||||
'form.exchangeRate'() {
|
||||
if (this._skipExchangeRateRebuild) return;
|
||||
this.buildSummaryFeesFromDetails();
|
||||
},
|
||||
filteredDetails: {
|
||||
handler(list) {
|
||||
@@ -1890,8 +1893,9 @@ export default {
|
||||
.map(row => [`${row.feeType}\u0000${row.feeItem}`, row])
|
||||
);
|
||||
const appendSummary = (feeItem, originalValue, settlementValue, fallbackFeeType = '') => {
|
||||
const originalAmount = Number(originalValue || 0);
|
||||
const settlementAmount = Number(settlementValue || 0);
|
||||
// 明细费用按本位币归集,外币结算时再按汇率换算成结算原币金额
|
||||
const originalAmount = this.convertLocalToSettlementAmount(originalValue);
|
||||
const settlementAmount = this.convertLocalToSettlementAmount(settlementValue);
|
||||
if (!feeItem || !Number.isFinite(originalAmount) || !Number.isFinite(settlementAmount)) {
|
||||
return;
|
||||
}
|
||||
@@ -1946,7 +1950,7 @@ export default {
|
||||
});
|
||||
nextFeeMap.forEach(row => orderedFees.push(row));
|
||||
this.summaryFees = orderedFees;
|
||||
this.recalculateLocalAmount();
|
||||
this.syncSettlementAmountsFromSummary();
|
||||
},
|
||||
// 每条明细对应的费用行:[调整前基准行, 调整后当前行],缺少来源费用快照时退化为明细自身的汇总行。
|
||||
resolveDetailFeePairs(detail) {
|
||||
@@ -2034,17 +2038,21 @@ export default {
|
||||
},
|
||||
handleExchangeRateDateChange() {
|
||||
this.fetchBfiExchangeRate();
|
||||
this.recalculateLocalAmount();
|
||||
},
|
||||
async fetchBfiExchangeRate() {
|
||||
if (!this.form.currency || this.form.currency === 'RMB') return;
|
||||
if (!this.form.currency || this.form.currency === 'RMB') {
|
||||
this.buildSummaryFeesFromDetails();
|
||||
return;
|
||||
}
|
||||
if (!this.form.exchangeRateDate) return;
|
||||
try {
|
||||
const { data } = await getBfiExchangeRate(this.form.currency, this.form.exchangeRateDate);
|
||||
const rateData = data?.data;
|
||||
if (rateData?.excval != null) {
|
||||
this._skipExchangeRateRebuild = true;
|
||||
this.form.exchangeRate = Number(rateData.excval);
|
||||
this.recalculateLocalAmount();
|
||||
this._skipExchangeRateRebuild = false;
|
||||
this.buildSummaryFeesFromDetails();
|
||||
} else {
|
||||
this.$message.warning(`未查询到 ${this.form.currency} 的BFI汇率数据`);
|
||||
}
|
||||
@@ -2052,9 +2060,39 @@ export default {
|
||||
this.$message.warning('查询BFI汇率失败,请手动输入结算汇率');
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 明细金额按本位币理解;外币结算时结算合计原金额/结算金额 = 本位币金额 / 结算汇率。
|
||||
*/
|
||||
convertLocalToSettlementAmount(localAmount) {
|
||||
const amount = Number(localAmount || 0);
|
||||
if (!Number.isFinite(amount)) return 0;
|
||||
if (this.isLocalCurrencySettlement) {
|
||||
return Number(amount.toFixed(2));
|
||||
}
|
||||
const rate = Number(this.form.exchangeRate || 0);
|
||||
if (rate <= 0) return 0;
|
||||
return Number((amount / rate).toFixed(2));
|
||||
},
|
||||
/**
|
||||
* 结算金额取结算合计原币汇总值;本位币合计 = 结算金额 × 结算汇率。
|
||||
*/
|
||||
syncSettlementAmountsFromSummary() {
|
||||
const settlementTotal = this.summaryFees.reduce(
|
||||
(total, row) => total + Number(row.settlementAmount || 0),
|
||||
0
|
||||
);
|
||||
this.form.settlementAmount = Number(settlementTotal || 0).toFixed(2);
|
||||
if (this.isLocalCurrencySettlement) {
|
||||
this.form.localSettlementAmount = this.form.settlementAmount;
|
||||
this.form.exchangeRate = 1;
|
||||
return;
|
||||
}
|
||||
const rate = Number(this.form.exchangeRate || 0);
|
||||
this.form.localSettlementAmount =
|
||||
rate > 0 ? (Number(this.form.settlementAmount || 0) * rate).toFixed(2) : '0.00';
|
||||
},
|
||||
recalculateLocalAmount() {
|
||||
const rate = this.form.currency === 'RMB' ? 1 : Number(this.form.exchangeRate || 0);
|
||||
this.form.localSettlementAmount = (Number(this.form.settlementAmount || 0) * rate).toFixed(2);
|
||||
this.buildSummaryFeesFromDetails();
|
||||
},
|
||||
openCandidateDialog() {
|
||||
if (!this.form.contractId) {
|
||||
|
||||
@@ -380,7 +380,13 @@ export default {
|
||||
},
|
||||
async loadFormalSettlements(keyword = '') {
|
||||
const { data } = await api.getFormalSettlements(keyword);
|
||||
this.candidates = data?.data || data || [];
|
||||
const payload = data?.data ?? data;
|
||||
// 兼容误返回分页对象的情况,只取正式结算单列表
|
||||
this.candidates = Array.isArray(payload)
|
||||
? payload
|
||||
: Array.isArray(payload?.records)
|
||||
? payload.records
|
||||
: [];
|
||||
},
|
||||
async loadFeeOptions() {
|
||||
const { data } = await api.getFeeOptions();
|
||||
|
||||
@@ -926,14 +926,34 @@ const stripInternalRowFields = row => {
|
||||
};
|
||||
};
|
||||
|
||||
/** 合并费用列名与各行 feeItems 键,避免保存/回显时丢动态费用列 */
|
||||
const collectFeeItemNames = (preferred = [], detailRows = []) => {
|
||||
const names = [];
|
||||
const seen = new Set();
|
||||
const pushName = value => {
|
||||
const name = String(value || '').trim();
|
||||
if (!name || seen.has(name)) return;
|
||||
seen.add(name);
|
||||
names.push(name);
|
||||
};
|
||||
(preferred || []).forEach(pushName);
|
||||
(detailRows || []).forEach(row => {
|
||||
const feeItems = row?.feeItems;
|
||||
if (!feeItems || typeof feeItems !== 'object' || Array.isArray(feeItems)) return;
|
||||
Object.keys(feeItems).forEach(pushName);
|
||||
});
|
||||
return names;
|
||||
};
|
||||
|
||||
const buildPayload = () => {
|
||||
const { file, ...batch } = form;
|
||||
const payloadRows = rows.value.map(stripInternalRowFields);
|
||||
return {
|
||||
...batch,
|
||||
settlementBillType: form.settlementType === 'payable' ? form.settlementBillType || 'formal' : 'formal',
|
||||
waybillMode: form.waybillMode === 'history' ? 'history' : 'new',
|
||||
feeItemNames: [...feeItemNames.value],
|
||||
rows: rows.value.map(stripInternalRowFields),
|
||||
feeItemNames: collectFeeItemNames(feeItemNames.value, payloadRows),
|
||||
rows: payloadRows,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1436,9 +1456,13 @@ const loadDetail = async id => {
|
||||
if (detail.projectId) {
|
||||
await loadContractsByProject(detail.projectId, detail.projectName || '');
|
||||
}
|
||||
feeItemNames.value = Array.isArray(detail.feeItemNames) ? detail.feeItemNames : [];
|
||||
parsedHeaders.value = [];
|
||||
const detailRows = Array.isArray(detail.rows) ? detail.rows : [];
|
||||
// 先按库中列名与明细 feeItems 合并,再规范化行数据,保证草稿再打开仍展示全部费用列
|
||||
feeItemNames.value = collectFeeItemNames(
|
||||
Array.isArray(detail.feeItemNames) ? detail.feeItemNames : [],
|
||||
detailRows
|
||||
);
|
||||
rows.value = detailRows.map((item, index) => normalizeParsedRow(item, index));
|
||||
refreshDuplicateFlags();
|
||||
// 查看模式始终只读;编辑时仅草稿可改,已完成/已作废强制只读
|
||||
|
||||
@@ -125,15 +125,17 @@
|
||||
<span class="reconciliation-editor__filter-label">货物名称</span>
|
||||
<el-input v-model="internalQuery.cargoName" placeholder="请输入" clearable />
|
||||
</div>
|
||||
<el-button @click="resetInternalQuery">重置</el-button>
|
||||
<el-button type="primary" @click="internalFilterTick++">查询</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
:disabled="!filteredInternalRows.length"
|
||||
@click="exportInternalDetails"
|
||||
>导出</el-button
|
||||
>
|
||||
<div class="reconciliation-editor__filter-actions">
|
||||
<el-button @click="resetInternalQuery">重置</el-button>
|
||||
<el-button type="primary" @click="internalFilterTick++">查询</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
:disabled="!filteredInternalRows.length"
|
||||
@click="exportInternalDetails"
|
||||
>导出</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<el-table
|
||||
:data="filteredInternalRows"
|
||||
@@ -189,6 +191,9 @@
|
||||
"
|
||||
>{{ formatDate(row[column.prop]) }}</span
|
||||
>
|
||||
<span v-else-if="column.prop === 'unitPrice'">{{
|
||||
formatOptionalMoney(row.unitPrice)
|
||||
}}</span>
|
||||
<span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
|
||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||||
</template>
|
||||
@@ -318,6 +323,9 @@
|
||||
"
|
||||
>{{ formatDate(row[column.prop]) }}</span
|
||||
>
|
||||
<span v-else-if="column.prop === 'unitPrice'">{{
|
||||
formatOptionalMoney(row.unitPrice)
|
||||
}}</span>
|
||||
<span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
|
||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||||
</template>
|
||||
@@ -476,6 +484,7 @@
|
||||
<span v-else-if="column.feeItemName">{{ formatMoney(getFeeItemAmount(row, column.feeItemName)) }}</span>
|
||||
<span v-else-if="column.prop === 'mileage'">{{ formatMileage(row.mileage) }}</span>
|
||||
<span v-else-if="column.prop === 'actualDepartureTime' || column.prop === 'actualCompletionTime'">{{ formatDate(row[column.prop]) }}</span>
|
||||
<span v-else-if="column.prop === 'unitPrice'">{{ formatOptionalMoney(row.unitPrice) }}</span>
|
||||
<span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
|
||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||||
</template>
|
||||
@@ -630,7 +639,10 @@ import { getDictionary } from '@/api/system/dictbiz';
|
||||
import { Search } from '@element-plus/icons-vue';
|
||||
import { h } from 'vue';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import { isExcelResponse, parseBlobJson } from '@/utils/import-excel';
|
||||
import {
|
||||
consumeImportResponse,
|
||||
resolveImportErrorMessage,
|
||||
} from '@/utils/import-excel';
|
||||
import { transportReconciliationFormFields } from '@/option/settlement/transportReconciliationForm';
|
||||
import {
|
||||
internalColumns,
|
||||
@@ -1042,12 +1054,24 @@ export default {
|
||||
.map(normalized)
|
||||
.join('|');
|
||||
},
|
||||
blankOptionalNumber(value) {
|
||||
if (value === null || value === undefined || value === '' || Number(value) === -1) {
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
normalizeInternalRow(row) {
|
||||
const feeItems =
|
||||
row.feeItems && typeof row.feeItems === 'object' && Object.keys(row.feeItems).length
|
||||
? row.feeItems
|
||||
: this.parseFeeItems(row.feeItemsJson);
|
||||
return { ...row, feeItems };
|
||||
return {
|
||||
...row,
|
||||
feeItems,
|
||||
unitPrice: this.blankOptionalNumber(row.unitPrice),
|
||||
mileage: this.blankOptionalNumber(row.mileage),
|
||||
freightAmount: this.blankOptionalNumber(row.freightAmount),
|
||||
};
|
||||
},
|
||||
normalizeExternalRow(row, index) {
|
||||
const feeItems =
|
||||
@@ -1057,6 +1081,9 @@ export default {
|
||||
return {
|
||||
...row,
|
||||
feeItems,
|
||||
unitPrice: this.blankOptionalNumber(row.unitPrice),
|
||||
mileage: this.blankOptionalNumber(row.mileage),
|
||||
freightAmount: this.blankOptionalNumber(row.freightAmount),
|
||||
_externalKey: row.id || row.externalLineNo || `external-${index}`,
|
||||
};
|
||||
},
|
||||
@@ -1284,6 +1311,9 @@ export default {
|
||||
snapshot.feeItemsJson = row.feeItemsJson || JSON.stringify(row.feeItems || {});
|
||||
snapshot.actualDepartureTime = this.normalizeExternalDateTime(row.actualDepartureTime);
|
||||
snapshot.actualCompletionTime = this.normalizeExternalDateTime(row.actualCompletionTime);
|
||||
snapshot.unitPrice = this.blankOptionalNumber(row.unitPrice);
|
||||
snapshot.mileage = this.blankOptionalNumber(row.mileage);
|
||||
snapshot.freightAmount = this.blankOptionalNumber(row.freightAmount);
|
||||
return snapshot;
|
||||
});
|
||||
},
|
||||
@@ -1297,6 +1327,9 @@ export default {
|
||||
snapshot.feeItemsJson = row.feeItemsJson || JSON.stringify(row.feeItems || {});
|
||||
snapshot.actualDepartureTime = this.normalizeExternalDateTime(row.actualDepartureTime);
|
||||
snapshot.actualCompletionTime = this.normalizeExternalDateTime(row.actualCompletionTime);
|
||||
snapshot.unitPrice = this.blankOptionalNumber(row.unitPrice);
|
||||
snapshot.mileage = this.blankOptionalNumber(row.mileage);
|
||||
snapshot.freightAmount = this.blankOptionalNumber(row.freightAmount);
|
||||
snapshot.suspectedDuplicate = duplicateIds.has(String(row.id));
|
||||
return snapshot;
|
||||
});
|
||||
@@ -1326,7 +1359,7 @@ export default {
|
||||
}
|
||||
},
|
||||
async handleUpdate() {
|
||||
await this.$confirm('将以外部账单数据更新运单及内部结算明细,是否继续?', '更新账单', {
|
||||
await this.$confirm('将以外部账单数据更新内部结算明细,是否继续?', '更新账单', {
|
||||
type: 'warning',
|
||||
});
|
||||
this.actionLoading = true;
|
||||
@@ -1508,22 +1541,23 @@ export default {
|
||||
? await api.importCargo(this.currentId, file)
|
||||
: await api.importVehicle(this.currentId, file);
|
||||
// 成功时 axios 拦截器可能已把 JSON Blob 解析成对象,不能再调 .text()
|
||||
if (isExcelResponse(response)) {
|
||||
downloadXls(
|
||||
response.data,
|
||||
`运输对账导入失败明细${this.$dayjs().format('YYYY-MM-DD HH-mm-ss')}.xlsx`
|
||||
);
|
||||
const consumed = await consumeImportResponse(response, {
|
||||
businessName: '运输对账',
|
||||
dayjsFormat: this.$dayjs().format('YYYY-MM-DD HH-mm-ss'),
|
||||
});
|
||||
if (consumed.kind === 'excel') {
|
||||
downloadXls(consumed.blob, consumed.fileName);
|
||||
this.$message.warning('部分数据导入失败,已下载失败明细');
|
||||
} else {
|
||||
const result = await parseBlobJson(response.data);
|
||||
if (Number(result.code) !== 200) throw new Error(result.msg || '导入失败');
|
||||
this.$message.success('外部账单导入成功');
|
||||
}
|
||||
this.matchingStarted = false;
|
||||
await this.loadDetail();
|
||||
this.matchingStarted = false;
|
||||
} catch (error) {
|
||||
this.$message.error(error.message || '外部账单导入失败');
|
||||
if (!error?._notified) {
|
||||
this.$message.error(await resolveImportErrorMessage(error));
|
||||
}
|
||||
}
|
||||
},
|
||||
async downloadTemplate(mode) {
|
||||
@@ -1831,7 +1865,10 @@ export default {
|
||||
: Number(value || 0).toFixed(2);
|
||||
},
|
||||
formatMileage(value) {
|
||||
return Number(value) === -1 ? '' : this.displayValue(value);
|
||||
return this.blankOptionalNumber(value) === null ? '' : this.displayValue(value);
|
||||
},
|
||||
formatOptionalMoney(value, currency = 'RMB') {
|
||||
return this.blankOptionalNumber(value) === null ? '-' : this.formatMoney(value, currency);
|
||||
},
|
||||
formatDate(value) {
|
||||
if (value === null || value === undefined || value === '') return '-';
|
||||
@@ -1952,15 +1989,26 @@ export default {
|
||||
}
|
||||
.reconciliation-editor__filter {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(160px, 1fr)) auto auto;
|
||||
grid-template-columns: repeat(4, minmax(160px, 1fr)) auto;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
.reconciliation-editor__filter-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
.reconciliation-editor__filter-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.reconciliation-editor__filter-actions .el-button {
|
||||
min-width: 64px;
|
||||
margin-left: 0;
|
||||
}
|
||||
.reconciliation-editor__filter-label {
|
||||
flex: 0 0 auto;
|
||||
margin-right: 8px;
|
||||
|
||||
@@ -6,15 +6,33 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { computed, getCurrentInstance, nextTick, onMounted } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useStore } from 'vuex';
|
||||
import SettlementImportPage from './components/settlement-import-page.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const store = useStore();
|
||||
const instance = getCurrentInstance();
|
||||
|
||||
const pageTitle = computed(() => {
|
||||
if (route.query.mode === 'view') return '查看导入结算单';
|
||||
return route.query.id ? '编辑导入结算单' : '新增导入结算单';
|
||||
});
|
||||
|
||||
const syncTagTitle = () => {
|
||||
nextTick(() => {
|
||||
store.commit('SET_TAG', {
|
||||
fullPath: route.fullPath,
|
||||
name: pageTitle.value,
|
||||
});
|
||||
instance?.proxy?.$router?.$avueRouter?.setTitle?.(pageTitle.value);
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
syncTagTitle();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -239,19 +239,22 @@ export default {
|
||||
this.onLoad(this.page);
|
||||
},
|
||||
openCreate() {
|
||||
this.$router.push({ path: '/settlement/settlement-import/form' });
|
||||
this.$router.push({
|
||||
path: '/settlement/settlement-import/form',
|
||||
query: { name: '新增导入结算单' },
|
||||
});
|
||||
},
|
||||
openDetail(row) {
|
||||
if (!row?.id) return;
|
||||
this.$router.push({
|
||||
path: '/settlement/settlement-import/form',
|
||||
query: { id: row.id, mode: 'view' },
|
||||
query: { id: row.id, mode: 'view', name: '查看导入结算单' },
|
||||
});
|
||||
},
|
||||
editDraft(row) {
|
||||
this.$router.push({
|
||||
path: '/settlement/settlement-import/form',
|
||||
query: { id: row.id },
|
||||
query: { id: row.id, name: '编辑导入结算单' },
|
||||
});
|
||||
},
|
||||
async handleDelete(row) {
|
||||
|
||||
@@ -425,6 +425,9 @@ export default {
|
||||
minWidth: 140,
|
||||
order: 61,
|
||||
placeholder: '如 1',
|
||||
// -1 为空占位,列表展示为空
|
||||
formatter: row =>
|
||||
Number(row.contractInitialSerial) === -1 ? '' : row.contractInitialSerial,
|
||||
tip: '该部门当年首个合同时从此流水号起编,后续递增;范围1-999,不填默认从1开始',
|
||||
rules: [
|
||||
{
|
||||
@@ -532,6 +535,27 @@ export default {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '是否业务部门',
|
||||
prop: 'isBusinessDept',
|
||||
type: 'select',
|
||||
dicData: [
|
||||
{ label: '否', value: 0 },
|
||||
{ label: '是', value: 1 },
|
||||
],
|
||||
dataType: 'number',
|
||||
value: 0,
|
||||
width: 120,
|
||||
order: 34,
|
||||
formatter: row => (Number(row.isBusinessDept) === 1 ? '是' : '否'),
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择是否业务部门',
|
||||
trigger: 'change',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '排序',
|
||||
prop: 'sort',
|
||||
@@ -1520,6 +1544,9 @@ export default {
|
||||
if (this.form.isPlatformCompany === undefined || this.form.isPlatformCompany === null) {
|
||||
this.form.isPlatformCompany = 0;
|
||||
}
|
||||
if (this.form.isBusinessDept === undefined || this.form.isBusinessDept === null) {
|
||||
this.form.isBusinessDept = 0;
|
||||
}
|
||||
this.clearEmptyContractInitialSerial(this.form);
|
||||
this.initData(this.form.tenantId);
|
||||
this.loadParentDeptCode(this.form.parentId);
|
||||
|
||||
+175
-39
@@ -20,33 +20,32 @@
|
||||
@on-load="onLoad"
|
||||
>
|
||||
<template #menu-left>
|
||||
<el-button
|
||||
type="primary"
|
||||
v-if="permission.mp_user_add"
|
||||
@click="$refs.crud.rowAdd()"
|
||||
>
|
||||
<el-button type="primary" v-if="permission.mp_user_add" @click="$refs.crud.rowAdd()">
|
||||
新增
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
v-if="permission.mp_user_reset"
|
||||
@click="handleReset"
|
||||
>
|
||||
<el-button type="primary" plain v-if="permission.mp_user_reset" @click="handleReset">
|
||||
密码重置
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
v-if="permission.mp_user_delete"
|
||||
@click="handleDelete"
|
||||
>
|
||||
<el-button type="danger" plain v-if="permission.mp_user_delete" @click="handleDelete">
|
||||
批量删除
|
||||
</el-button>
|
||||
</template>
|
||||
<template #identities="{ row }">
|
||||
{{ row.identitiesName || formatIdentities(row.identities) }}
|
||||
</template>
|
||||
<template #deptName="{ row }">
|
||||
<template v-if="formatDeptNames(row.deptName).length">
|
||||
<el-tag
|
||||
v-for="name in formatDeptNames(row.deptName)"
|
||||
:key="name"
|
||||
size="small"
|
||||
class="dept-tag"
|
||||
>
|
||||
{{ name }}
|
||||
</el-tag>
|
||||
</template>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<template #status="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'primary' : 'info'" class="status-text">
|
||||
{{ row.status === 1 ? '启用' : '停用' }}
|
||||
@@ -55,6 +54,33 @@
|
||||
<template #updateUserName="{ row }">
|
||||
{{ formatUpdateUserName(row) }}
|
||||
</template>
|
||||
<template #deptId-form>
|
||||
<el-cascader
|
||||
v-model="deptCascaderValue"
|
||||
:options="permissionDeptTree"
|
||||
:props="deptCascaderProps"
|
||||
:disabled="isViewMode"
|
||||
clearable
|
||||
filterable
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
placeholder="请选择所属组织(可多选)"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</template>
|
||||
<template #dataScopeDeptIds-form>
|
||||
<el-tree
|
||||
v-if="Number(form.dataScopeRange) === 3"
|
||||
ref="dataScopeTree"
|
||||
:data="permissionDeptTree"
|
||||
show-checkbox
|
||||
node-key="id"
|
||||
:default-checked-keys="form.dataScopeDeptIds || []"
|
||||
:props="{ label: 'title', children: 'children' }"
|
||||
@check="handleDataScopeCheck"
|
||||
/>
|
||||
<span v-else class="form-tip">选择「自定义」后可勾选数据权限组织</span>
|
||||
</template>
|
||||
<template #menu="{ row, index }">
|
||||
<el-link
|
||||
type="primary"
|
||||
@@ -70,25 +96,13 @@
|
||||
>
|
||||
编辑
|
||||
</el-link>
|
||||
<el-link
|
||||
type="primary"
|
||||
v-if="permission.mp_user_status"
|
||||
@click="handleStatus(row)"
|
||||
>
|
||||
<el-link type="primary" v-if="permission.mp_user_status" @click="handleStatus(row)">
|
||||
{{ row.status === 1 ? '停用' : '启用' }}
|
||||
</el-link>
|
||||
<el-link
|
||||
type="primary"
|
||||
v-if="permission.mp_user_reset"
|
||||
@click="handleResetForRow(row)"
|
||||
>
|
||||
<el-link type="primary" v-if="permission.mp_user_reset" @click="handleResetForRow(row)">
|
||||
重置密码
|
||||
</el-link>
|
||||
<el-link
|
||||
type="danger"
|
||||
v-if="permission.mp_user_delete"
|
||||
@click="rowDel(row)"
|
||||
>
|
||||
<el-link type="danger" v-if="permission.mp_user_delete" @click="rowDel(row)">
|
||||
删除
|
||||
</el-link>
|
||||
</template>
|
||||
@@ -108,9 +122,12 @@
|
||||
<script>
|
||||
import { h } from 'vue';
|
||||
import { getList, getDetail, submit, remove, changeStatus, resetPassword } from '@/api/system/mpUser';
|
||||
import { getDeptTree } from '@/api/system/dept';
|
||||
import option from '@/option/system/mpUser';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { formatUpdateUserName } from '@/utils/audit';
|
||||
import { sensitive } from '@/utils/sensitive';
|
||||
import func from '@/utils/func';
|
||||
|
||||
const IDENTITY_LABEL = {
|
||||
driver: '司机',
|
||||
@@ -132,10 +149,12 @@ export default {
|
||||
selectionList: [],
|
||||
option,
|
||||
data: [],
|
||||
permissionDeptTree: [],
|
||||
sensitiveManager: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['permission']),
|
||||
...mapGetters(['permission', 'userInfo']),
|
||||
permissionList() {
|
||||
return {
|
||||
addBtn: this.validData(this.permission.mp_user_add, false),
|
||||
@@ -147,6 +166,46 @@ export default {
|
||||
ids() {
|
||||
return this.selectionList.map(item => item.id).join(',');
|
||||
},
|
||||
isViewMode() {
|
||||
return this.dialogType === 'view';
|
||||
},
|
||||
deptCascaderProps() {
|
||||
return {
|
||||
label: 'title',
|
||||
value: 'id',
|
||||
children: 'children',
|
||||
emitPath: false,
|
||||
multiple: true,
|
||||
checkStrictly: true,
|
||||
expandTrigger: 'click',
|
||||
disabled: 'disabled',
|
||||
};
|
||||
},
|
||||
deptCascaderValue: {
|
||||
get() {
|
||||
const ids = Array.isArray(this.form.deptId)
|
||||
? this.form.deptId
|
||||
: this.form.deptId
|
||||
? String(this.form.deptId).split(',').filter(Boolean)
|
||||
: [];
|
||||
return ids
|
||||
.filter(item => item !== undefined && item !== null && item !== '')
|
||||
.map(item => (typeof item === 'number' ? item : String(item)));
|
||||
},
|
||||
set(value) {
|
||||
this.form.deptId = Array.isArray(value)
|
||||
? value.filter(item => item !== undefined && item !== null && item !== '')
|
||||
: value
|
||||
? [value]
|
||||
: [];
|
||||
},
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.sensitiveManager = sensitive.create({
|
||||
fields: ['phone', 'email'],
|
||||
});
|
||||
this.loadDeptTree();
|
||||
},
|
||||
methods: {
|
||||
formatUpdateUserName,
|
||||
@@ -158,19 +217,59 @@ export default {
|
||||
.filter(Boolean)
|
||||
.join(',');
|
||||
},
|
||||
normalizeSubmitRow(row = {}) {
|
||||
const payload = { ...row };
|
||||
formatDeptNames(deptName) {
|
||||
if (!deptName) {
|
||||
return [];
|
||||
}
|
||||
return String(deptName)
|
||||
.split(',')
|
||||
.map(item => item.trim())
|
||||
.filter(Boolean);
|
||||
},
|
||||
loadDeptTree() {
|
||||
const tenantId = this.userInfo?.tenantId;
|
||||
if (!tenantId) {
|
||||
return;
|
||||
}
|
||||
getDeptTree(tenantId).then(res => {
|
||||
this.permissionDeptTree = res.data.data || [];
|
||||
});
|
||||
},
|
||||
handleDataScopeCheck(_, checked) {
|
||||
if (this.isViewMode) {
|
||||
return;
|
||||
}
|
||||
this.form.dataScopeDeptIds = checked.checkedKeys || [];
|
||||
},
|
||||
normalizeSubmitRow(row = {}, isUpdate = false) {
|
||||
let payload = { ...row };
|
||||
// 编辑时剔除未改动的脱敏字段,避免把 138****8000 写回库
|
||||
if (isUpdate && this.sensitiveManager) {
|
||||
payload = this.sensitiveManager.getSubmitData(payload);
|
||||
}
|
||||
if (Array.isArray(payload.identities)) {
|
||||
payload.identities = payload.identities.filter(Boolean).join(',');
|
||||
}
|
||||
payload.deptId = func.join(payload.deptId);
|
||||
if (payload.phone) {
|
||||
payload.phone = String(payload.phone).trim();
|
||||
}
|
||||
if (!payload.account && payload.phone) {
|
||||
// 脱敏手机号不能作为账号默认值
|
||||
if (!payload.account && payload.phone && !String(payload.phone).includes('*')) {
|
||||
payload.account = payload.phone;
|
||||
}
|
||||
payload.dataScopeRange = Number(payload.dataScopeRange || 1);
|
||||
payload.dataLevelRange = Number(payload.dataLevelRange || 1);
|
||||
if (payload.dataScopeRange === 3) {
|
||||
payload.dataScopeDeptIds = Array.isArray(payload.dataScopeDeptIds)
|
||||
? payload.dataScopeDeptIds
|
||||
: [];
|
||||
} else {
|
||||
payload.dataScopeDeptIds = [];
|
||||
}
|
||||
delete payload.password;
|
||||
delete payload.identitiesName;
|
||||
delete payload.deptName;
|
||||
delete payload.sexName;
|
||||
delete payload.createUserName;
|
||||
delete payload.updateUserName;
|
||||
@@ -178,13 +277,25 @@ export default {
|
||||
},
|
||||
beforeOpen(done, type) {
|
||||
this.dialogType = type;
|
||||
this.loadDeptTree();
|
||||
if (['edit', 'view'].includes(type) && this.form.id) {
|
||||
getDetail(this.form.id).then(res => {
|
||||
const detail = res.data.data || {};
|
||||
if (detail.identities && typeof detail.identities === 'string') {
|
||||
detail.identities = detail.identities.split(',').filter(Boolean);
|
||||
}
|
||||
this.form = detail;
|
||||
const deptIds = func.split(detail.deptId || '');
|
||||
this.form = {
|
||||
...detail,
|
||||
deptId: Array.isArray(deptIds) ? deptIds : [],
|
||||
dataScopeRange: detail.dataScopeRange || 1,
|
||||
dataLevelRange: detail.dataLevelRange || 1,
|
||||
dataScopeDeptIds: detail.dataScopeDeptIds || [],
|
||||
};
|
||||
this.sensitiveManager?.saveInitialData(this.form);
|
||||
this.$nextTick(() => {
|
||||
this.$refs.dataScopeTree?.setCheckedKeys?.(this.form.dataScopeDeptIds || []);
|
||||
});
|
||||
done();
|
||||
});
|
||||
return;
|
||||
@@ -193,12 +304,23 @@ export default {
|
||||
this.form = {
|
||||
status: 1,
|
||||
identities: ['driver'],
|
||||
deptId: [],
|
||||
dataScopeRange: 1,
|
||||
dataLevelRange: 1,
|
||||
dataScopeDeptIds: [],
|
||||
};
|
||||
this.sensitiveManager?.saveInitialData({});
|
||||
}
|
||||
done();
|
||||
},
|
||||
rowSave(row, done, loading) {
|
||||
submit(this.normalizeSubmitRow(row)).then(
|
||||
const payload = this.normalizeSubmitRow(row, false);
|
||||
if (payload.dataScopeRange === 3 && (!payload.dataScopeDeptIds || !payload.dataScopeDeptIds.length)) {
|
||||
this.$message.warning('请选择自定义数据权限组织');
|
||||
loading();
|
||||
return;
|
||||
}
|
||||
submit(payload).then(
|
||||
() => {
|
||||
this.onLoad(this.page);
|
||||
this.$message.success('操作成功');
|
||||
@@ -208,7 +330,13 @@ export default {
|
||||
);
|
||||
},
|
||||
rowUpdate(row, index, done, loading) {
|
||||
submit(this.normalizeSubmitRow(row)).then(
|
||||
const payload = this.normalizeSubmitRow(row, true);
|
||||
if (payload.dataScopeRange === 3 && (!payload.dataScopeDeptIds || !payload.dataScopeDeptIds.length)) {
|
||||
this.$message.warning('请选择自定义数据权限组织');
|
||||
loading();
|
||||
return;
|
||||
}
|
||||
submit(payload).then(
|
||||
() => {
|
||||
this.onLoad(this.page);
|
||||
this.$message.success('操作成功');
|
||||
@@ -351,5 +479,13 @@ export default {
|
||||
:deep(.avue-crud__menu) {
|
||||
min-height: auto;
|
||||
}
|
||||
.dept-tag {
|
||||
margin-right: 4px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.form-tip {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+105
-45
@@ -115,9 +115,10 @@
|
||||
<template #deptName="{ row }">
|
||||
<template v-if="formatDeptNames(row.deptName).length">
|
||||
<el-tag
|
||||
v-for="name in formatDeptNames(row.deptName)"
|
||||
:key="name"
|
||||
v-for="(name, index) in formatDeptNames(row.deptName)"
|
||||
:key="`${row.id}-${index}-${name}`"
|
||||
class="user-dept-tag"
|
||||
@click.stop="handleDeptTagClick(row, index)"
|
||||
>{{ name }}</el-tag>
|
||||
</template>
|
||||
<span v-else>-</span>
|
||||
@@ -174,8 +175,6 @@
|
||||
<el-col :span="6"><el-form-item label="工号"><el-input v-model="form.code" :disabled="isUserViewMode" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="OA登录名"><el-input v-model="form.oaLoginId" :disabled="isUserViewMode" maxlength="128" placeholder="请输入OA登录名" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="人员类别"><el-select v-model="form.personCategory" :disabled="isUserViewMode"><el-option label="内部员工" :value="1" /><el-option label="承运商" :value="2" /></el-select></el-form-item></el-col>
|
||||
<el-col :span="18"><el-form-item label="数据权限范围"><el-radio-group v-model="form.dataScopeRange" :disabled="isUserViewMode"><el-radio :value="1">仅所属组织</el-radio><el-radio :value="2">全部组织</el-radio><el-radio :value="3">自定义</el-radio></el-radio-group></el-form-item></el-col>
|
||||
<el-col :span="18"><el-form-item label="数据层级范围"><el-radio-group v-model="form.dataLevelRange" :disabled="isUserViewMode"><el-radio :value="1">包含下级</el-radio><el-radio :value="2">仅本级</el-radio></el-radio-group></el-form-item></el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="备注">
|
||||
<el-input
|
||||
@@ -201,13 +200,6 @@
|
||||
<el-table-column v-if="!isUserViewMode" label="操作" width="90"><template #default="{ row }"><el-link type="danger" @click="removeFormRole(row.id)">删除</el-link></template></el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane v-if="form.dataScopeRange === 3" label="数据权限">
|
||||
<el-tree :data="permissionDeptTree" show-checkbox node-key="id" :default-checked-keys="form.dataScopeDeptIds || []" :props="{ label: 'title', children: 'children' }" @check="handleDataScopeCheck" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="客商权限">
|
||||
<div class="permission-toolbar customer-toolbar"><el-checkbox v-model="form.includeNewCustomer" :true-value="1" :false-value="0" :disabled="isUserViewMode">包含新增的客商</el-checkbox></div>
|
||||
<el-table ref="customerTable" :data="customerList" border @selection-change="handleCustomerSelection"><el-table-column type="selection" width="55" :selectable="isCustomerSelectable" /><el-table-column prop="customerCode" label="客商编码" /><el-table-column prop="fullName" label="客商名称" /></el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<template #footer>
|
||||
<span v-if="isUserViewMode" class="dialog-footer">
|
||||
@@ -406,7 +398,6 @@ import {
|
||||
auditRefuse,
|
||||
setLeader,
|
||||
getLeaderList,
|
||||
getCustomerOptions,
|
||||
} from '@/api/system/user';
|
||||
import { exportBlob } from '@/api/common';
|
||||
import { getDeptTree } from '@/api/system/dept';
|
||||
@@ -476,7 +467,6 @@ export default {
|
||||
treeData: [],
|
||||
permissionDeptTree: [],
|
||||
postList: [],
|
||||
customerList: [],
|
||||
treeOption: treeOption,
|
||||
userOption: userOption(this),
|
||||
data: [],
|
||||
@@ -666,6 +656,104 @@ export default {
|
||||
this.page.currentPage = 1;
|
||||
this.onLoad(this.page);
|
||||
},
|
||||
formatDeptIds(deptId) {
|
||||
if (!deptId && deptId !== 0) {
|
||||
return [];
|
||||
}
|
||||
return String(deptId)
|
||||
.split(',')
|
||||
.map(item => item.trim())
|
||||
.filter(Boolean);
|
||||
},
|
||||
handleDeptTagClick(row, index) {
|
||||
const deptIds = this.formatDeptIds(row.deptId);
|
||||
let deptId = deptIds[index];
|
||||
if (!deptId) {
|
||||
const names = this.formatDeptNames(row.deptName);
|
||||
deptId = this.findDeptIdByName(names[index]);
|
||||
}
|
||||
if (!deptId) {
|
||||
this.$message.warning('未找到对应组织');
|
||||
return;
|
||||
}
|
||||
this.selectDeptOnTree(deptId);
|
||||
},
|
||||
findDeptIdByName(name, tree = this.permissionDeptTree) {
|
||||
if (!name) return null;
|
||||
for (const node of tree || []) {
|
||||
if (node.title === name || node.deptName === name || node.label === name) {
|
||||
return node.id;
|
||||
}
|
||||
const matched = this.findDeptIdByName(name, node.children);
|
||||
if (matched != null) return matched;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
/**
|
||||
* 展开左侧懒加载组织树到目标节点并设为选中,同时按该组织筛选用户列表
|
||||
*/
|
||||
async selectDeptOnTree(deptId) {
|
||||
if (deptId == null || deptId === '') return;
|
||||
if (!this.permissionDeptTree?.length) {
|
||||
const tenantId = this.userInfo.tenantId || this.website.tenantId;
|
||||
try {
|
||||
const res = await getDeptTree(tenantId);
|
||||
this.permissionDeptTree = res.data.data || [];
|
||||
} catch (error) {
|
||||
this.$message.error('组织树加载失败');
|
||||
return;
|
||||
}
|
||||
}
|
||||
const path = this.findDeptPath(deptId);
|
||||
if (!path?.length) {
|
||||
this.$message.warning('未在组织树中找到该部门');
|
||||
return;
|
||||
}
|
||||
const tree = this.$refs.deptTree;
|
||||
if (!tree) return;
|
||||
for (let i = 0; i < path.length - 1; i++) {
|
||||
await this.expandLazyTreeNode(path[i]);
|
||||
}
|
||||
await this.$nextTick();
|
||||
const targetId = path[path.length - 1];
|
||||
tree.setCurrentKey?.(targetId);
|
||||
this.$nextTick(() => {
|
||||
const currentEl = tree.$el?.querySelector?.('.el-tree-node.is-current');
|
||||
currentEl?.scrollIntoView?.({ block: 'nearest', behavior: 'smooth' });
|
||||
});
|
||||
if (String(this.treeDeptId) === String(targetId)) {
|
||||
return;
|
||||
}
|
||||
this.treeDeptId = targetId;
|
||||
this.page.currentPage = 1;
|
||||
this.onLoad(this.page);
|
||||
},
|
||||
expandLazyTreeNode(key) {
|
||||
return new Promise(resolve => {
|
||||
const tree = this.$refs.deptTree;
|
||||
if (!tree?.getNode) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
const tryExpand = (retries = 40) => {
|
||||
const node = tree.getNode(key);
|
||||
if (!node) {
|
||||
if (retries <= 0) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
setTimeout(() => tryExpand(retries - 1), 50);
|
||||
return;
|
||||
}
|
||||
if (node.expanded || node.isLeaf) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
node.expand(() => resolve());
|
||||
};
|
||||
tryExpand();
|
||||
});
|
||||
},
|
||||
handleDropdownCommand(command, row, index) {
|
||||
if (command === 'setLeader') {
|
||||
this.handleSetLeader(row);
|
||||
@@ -853,9 +941,6 @@ export default {
|
||||
const column = this.findColumn(this.userOption.group, 'leaderId');
|
||||
column.dicData = res.data.data;
|
||||
});
|
||||
getCustomerOptions().then(res => {
|
||||
this.customerList = res.data.data || [];
|
||||
});
|
||||
},
|
||||
// 由部门 id 反查「根 → 该节点」的 id 路径,供级联回显(找不到返回 null)
|
||||
findDeptPath(id, tree = this.permissionDeptTree, parents = []) {
|
||||
@@ -901,21 +986,6 @@ export default {
|
||||
.split(this.form.roleId || '')
|
||||
.filter(id => String(id) !== String(roleId));
|
||||
},
|
||||
handleDataScopeCheck(_, checked) {
|
||||
if (this.isUserViewMode) {
|
||||
return;
|
||||
}
|
||||
this.form.dataScopeDeptIds = checked.checkedKeys;
|
||||
},
|
||||
handleCustomerSelection(rows) {
|
||||
if (this.isUserViewMode) {
|
||||
return;
|
||||
}
|
||||
this.form.customerIds = rows.map(row => row.id);
|
||||
},
|
||||
isCustomerSelectable() {
|
||||
return !this.isUserViewMode;
|
||||
},
|
||||
normalizeSexDisplay(sex) {
|
||||
if (sex === -1 || sex === '-1' || sex === '' || sex === null || sex === undefined) {
|
||||
return undefined;
|
||||
@@ -966,13 +1036,8 @@ export default {
|
||||
deptId: [],
|
||||
code: '',
|
||||
personCategory: 1,
|
||||
dataScopeRange: 1,
|
||||
dataLevelRange: 1,
|
||||
remark: '',
|
||||
roleId: '',
|
||||
dataScopeDeptIds: [],
|
||||
customerIds: [],
|
||||
includeNewCustomer: 0,
|
||||
};
|
||||
this.userDialog = true;
|
||||
this.$nextTick(() => this.$refs.userFormRef && this.$refs.userFormRef.clearValidate());
|
||||
@@ -986,22 +1051,13 @@ export default {
|
||||
deptId: Array.isArray(deptIds) ? deptIds : [],
|
||||
postId: func.split(user.postId || '')[0] || '',
|
||||
roleId: func.join(user.roleId || ''),
|
||||
dataScopeDeptIds: user.dataScopeDeptIds || [],
|
||||
customerIds: user.customerIds || [],
|
||||
includeNewCustomer: user.includeNewCustomer || 0,
|
||||
personCategory: user.personCategory || 1,
|
||||
dataScopeRange: user.dataScopeRange || 1,
|
||||
dataLevelRange: user.dataLevelRange || 1,
|
||||
sex: this.normalizeSexDisplay(user.sex),
|
||||
};
|
||||
this.sensitiveManager.saveInitialData(this.form);
|
||||
this.userDialog = true;
|
||||
this.$nextTick(() => {
|
||||
this.$refs.userFormRef && this.$refs.userFormRef.clearValidate();
|
||||
(this.form.customerIds || []).forEach(id => {
|
||||
const customer = this.customerList.find(item => String(item.id) === String(id));
|
||||
if (customer) this.$refs.customerTable && this.$refs.customerTable.toggleRowSelection(customer, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
@@ -1537,6 +1593,10 @@ export default {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.user-dept-tag {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.user-dept-tag + .user-dept-tag {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
@@ -60,7 +60,11 @@
|
||||
<span>{{ row.idCardNo || '-' }}</span>
|
||||
</template>
|
||||
<template #organizationName="{ row }">
|
||||
{{ String(row.organizationName || '').replace(/^[\s\u3000]+/, '') }}
|
||||
<span
|
||||
v-if="isBlankOrganizationName(row.organizationName)"
|
||||
class="organization-name--empty"
|
||||
>未分配组织</span>
|
||||
<span v-else>{{ String(row.organizationName || '').replace(/^[\s\u3000]+/, '') }}</span>
|
||||
</template>
|
||||
<template #status="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'primary' : 'info'" class="status-text">
|
||||
@@ -143,7 +147,7 @@
|
||||
maxlength="18"
|
||||
show-word-limit
|
||||
placeholder="请输入"
|
||||
@input="handleIdCardInput"
|
||||
@blur="handleIdCardBlur"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -153,7 +157,7 @@
|
||||
v-model="driverForm.birthday"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="请选择"
|
||||
placeholder="请选择出生年月"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -201,12 +205,16 @@
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="住址" prop="addressRegion">
|
||||
<!-- 只读直接显示库里那串区划名称:级联要先按名称反解 path,区划改名后反解不出来,
|
||||
同一串文字就会在灰字 placeholder 与黑字标签之间摇摆 -->
|
||||
<el-input v-if="readonly" :model-value="driverForm.addressRegion || '—'" readonly />
|
||||
<el-cascader
|
||||
v-else
|
||||
ref="addressRegionCascader"
|
||||
v-model="driverForm.addressRegionPath"
|
||||
:options="regionOptions"
|
||||
:props="regionCascaderProps"
|
||||
:placeholder="driverForm.addressRegion || '请选择'"
|
||||
:placeholder="driverForm.addressRegion || '请选择省/市/区'"
|
||||
clearable
|
||||
filterable
|
||||
@change="handleAddressRegionChange"
|
||||
@@ -219,7 +227,7 @@
|
||||
v-model="driverForm.address"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
placeholder="请输入"
|
||||
placeholder="请输入详细地址(街道、门牌号)"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -592,7 +600,11 @@
|
||||
<span>{{ row.plateNo || '-' }}</span>
|
||||
</template>
|
||||
<template #organizationName="{ row }">
|
||||
{{ String(row.organizationName || '').replace(/^[\s\u3000]+/, '') }}
|
||||
<span
|
||||
v-if="isBlankOrganizationName(row.organizationName)"
|
||||
class="organization-name--empty"
|
||||
>未分配组织</span>
|
||||
<span v-else>{{ String(row.organizationName || '').replace(/^[\s\u3000]+/, '') }}</span>
|
||||
</template>
|
||||
<template #boundDriver="{ row }">
|
||||
<span>{{ row.boundDriver || '-' }}</span>
|
||||
@@ -743,6 +755,8 @@ export default {
|
||||
expired: 0,
|
||||
},
|
||||
driverBox: false,
|
||||
// 搜索栏组织级联追加「未分配组织」一项(哨兵值见 organization-search mixin)
|
||||
enableUnassignedOrgFilter: true,
|
||||
vehicleSelectorVisible: false,
|
||||
vehicleSelectorLoading: false,
|
||||
vehicleSelectorOption,
|
||||
@@ -857,7 +871,7 @@ export default {
|
||||
emergencyContactMobile: [
|
||||
{ required: true, message: '请输入紧急联系人手机号', trigger: 'blur' },
|
||||
],
|
||||
organizationName: [{ required: true, message: '请选择所属组织', trigger: 'change' }],
|
||||
// 后端已允许所属组织为空,这里不做必填校验
|
||||
idCardFront: [{ required: true, message: '请上传身份证正面照', trigger: 'change' }],
|
||||
idCardBack: [{ required: true, message: '请上传身份证反面照', trigger: 'change' }],
|
||||
drivingLicenseFront: [{ required: true, message: '请上传驾驶证主页', trigger: 'change' }],
|
||||
@@ -1122,19 +1136,21 @@ export default {
|
||||
removeDigits(value = '') {
|
||||
return String(value).replace(/\d/g, '');
|
||||
},
|
||||
handleIdCardInput(value) {
|
||||
const idCardNo = String(value || '').toUpperCase();
|
||||
/**
|
||||
* 身份证号失焦后补出生年月与性别,与小程序 person.vue 的 onIdCardNoChange 同口径:
|
||||
* 只在字段还空着时补——用户手改过的值不被他随后修改身份证号的动作冲掉。
|
||||
* 不回写空值:18 位不合法时清空生日会把已选日期当垃圾丢掉。
|
||||
* OCR 识别结果仍直接覆盖(见 applyIdCardRecognition),那是「重新采一次证件」,与手输联动不是一回事。
|
||||
*/
|
||||
handleIdCardBlur() {
|
||||
const idCardNo = String(this.driverForm.idCardNo || '').toUpperCase();
|
||||
this.driverForm.idCardNo = idCardNo;
|
||||
const idCardInfo = this.parseIdCardInfo(idCardNo);
|
||||
if (idCardInfo.birthday) {
|
||||
if (!this.driverForm.birthday && idCardInfo.birthday) {
|
||||
this.driverForm.birthday = idCardInfo.birthday;
|
||||
} else {
|
||||
this.driverForm.birthday = '';
|
||||
}
|
||||
if (idCardInfo.gender) {
|
||||
if (!this.driverForm.gender && idCardInfo.gender) {
|
||||
this.driverForm.gender = idCardInfo.gender;
|
||||
} else {
|
||||
this.driverForm.gender = '';
|
||||
}
|
||||
if (this.submitAttempted) {
|
||||
this.$nextTick(() => {
|
||||
@@ -1952,6 +1968,11 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
// 空组织记录在列表里的占位文案
|
||||
.organization-name--empty {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.driver-page__date-expiring {
|
||||
color: #d40000;
|
||||
font-weight: 600;
|
||||
|
||||
@@ -59,7 +59,22 @@
|
||||
</el-button>
|
||||
</template>
|
||||
<template #organizationName="{ row }">
|
||||
{{ String(row.organizationName || '').replace(/^[\s\u3000]+/, '') }}
|
||||
<el-tooltip
|
||||
:content="formatOrgDisplayName(row.organizationName)"
|
||||
placement="top"
|
||||
:disabled="!formatOrgDisplayName(row.organizationName)"
|
||||
>
|
||||
<span class="org-cell-text">{{ formatOrgDisplayName(row.organizationName) }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template #useDepartment="{ row }">
|
||||
<el-tooltip
|
||||
:content="formatOrgDisplayName(row.useDepartment)"
|
||||
placement="top"
|
||||
:disabled="!formatOrgDisplayName(row.useDepartment)"
|
||||
>
|
||||
<span class="org-cell-text">{{ formatOrgDisplayName(row.useDepartment) }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template #status="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'primary' : 'info'" class="status-text">
|
||||
@@ -315,26 +330,58 @@
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="所有组织" prop="organizationName">
|
||||
<el-cascader
|
||||
v-model="organizationCascaderValue"
|
||||
:options="organizationTreeOptions"
|
||||
:props="organizationCascaderProps"
|
||||
filterable
|
||||
clearable
|
||||
placeholder="请选择"
|
||||
/>
|
||||
<el-tooltip
|
||||
:content="
|
||||
formatOrgCascaderPath(organizationCascaderPath) ||
|
||||
formatOrgDisplayName(vehicleForm.organizationName)
|
||||
"
|
||||
placement="top"
|
||||
:disabled="
|
||||
!(
|
||||
formatOrgCascaderPath(organizationCascaderPath) ||
|
||||
formatOrgDisplayName(vehicleForm.organizationName)
|
||||
)
|
||||
"
|
||||
>
|
||||
<div class="org-cascader-tooltip">
|
||||
<el-cascader
|
||||
v-model="organizationCascaderValue"
|
||||
:options="organizationFormTreeOptions"
|
||||
:props="formOrgCascaderProps"
|
||||
filterable
|
||||
clearable
|
||||
placeholder="请选择"
|
||||
/>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="使用部门" prop="useDepartment">
|
||||
<el-cascader
|
||||
v-model="useDepartmentCascaderValue"
|
||||
:options="organizationTreeOptions"
|
||||
:props="organizationCascaderProps"
|
||||
filterable
|
||||
clearable
|
||||
placeholder="请选择"
|
||||
/>
|
||||
<el-tooltip
|
||||
:content="
|
||||
formatOrgCascaderPath(useDepartmentCascaderPath) ||
|
||||
formatOrgDisplayName(vehicleForm.useDepartment)
|
||||
"
|
||||
placement="top"
|
||||
:disabled="
|
||||
!(
|
||||
formatOrgCascaderPath(useDepartmentCascaderPath) ||
|
||||
formatOrgDisplayName(vehicleForm.useDepartment)
|
||||
)
|
||||
"
|
||||
>
|
||||
<div class="org-cascader-tooltip">
|
||||
<el-cascader
|
||||
v-model="useDepartmentCascaderValue"
|
||||
:options="useDepartmentFormTreeOptions"
|
||||
:props="formOrgCascaderProps"
|
||||
filterable
|
||||
clearable
|
||||
placeholder="请选择"
|
||||
/>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@@ -652,6 +699,17 @@ export default {
|
||||
vehicleCertificateUploads: {},
|
||||
uploadHeaders: getUploadHeaders(),
|
||||
organizationOptions: [],
|
||||
// 表单两个级联各自维护路径,避免名称回写互相覆盖;不要与搜索栏共用同一棵树节点
|
||||
organizationCascaderPath: [],
|
||||
useDepartmentCascaderPath: [],
|
||||
formOrgCascaderProps: {
|
||||
value: 'id',
|
||||
label: 'label',
|
||||
children: 'children',
|
||||
emitPath: true,
|
||||
checkStrictly: true,
|
||||
expandTrigger: 'hover',
|
||||
},
|
||||
vehicleTypeOptions: [
|
||||
{ label: '高栏车', value: '高栏' },
|
||||
{ label: '低栏车', value: '低栏' },
|
||||
@@ -697,22 +755,31 @@ export default {
|
||||
: [{ required: true, message: '请选择行驶证有效期', trigger: 'change' }],
|
||||
};
|
||||
},
|
||||
// 所属组织级联:表单存组织名称,级联控件用 id 路径,两者在此双向转换
|
||||
// 两个级联必须使用克隆后的独立树,否则 checkStrictly 会在节点上互相抢选中态
|
||||
organizationFormTreeOptions() {
|
||||
return this.cloneOrgTree(this.organizationTreeOptions);
|
||||
},
|
||||
useDepartmentFormTreeOptions() {
|
||||
return this.cloneOrgTree(this.organizationTreeOptions);
|
||||
},
|
||||
// 所属组织级联:本地保存 id 路径,名称同步到表单字段供后端使用
|
||||
organizationCascaderValue: {
|
||||
get() {
|
||||
return this.resolveOrgPath(this.vehicleForm.organizationName);
|
||||
return this.organizationCascaderPath;
|
||||
},
|
||||
set(path) {
|
||||
this.vehicleForm.organizationName = this.resolveOrgName(path);
|
||||
this.organizationCascaderPath = Array.isArray(path) ? [...path] : [];
|
||||
this.vehicleForm.organizationName = this.resolveOrgName(this.organizationCascaderPath);
|
||||
},
|
||||
},
|
||||
// 使用部门级联:与所属组织同一套部门树
|
||||
// 使用部门级联:独立路径状态,避免与所属组织共用计算回写导致选不中
|
||||
useDepartmentCascaderValue: {
|
||||
get() {
|
||||
return this.resolveOrgPath(this.vehicleForm.useDepartment);
|
||||
return this.useDepartmentCascaderPath;
|
||||
},
|
||||
set(path) {
|
||||
this.vehicleForm.useDepartment = this.resolveOrgName(path);
|
||||
this.useDepartmentCascaderPath = Array.isArray(path) ? [...path] : [];
|
||||
this.vehicleForm.useDepartment = this.resolveOrgName(this.useDepartmentCascaderPath);
|
||||
},
|
||||
},
|
||||
isAdmin() {
|
||||
@@ -745,10 +812,49 @@ export default {
|
||||
this.initDeptTree();
|
||||
this.loadOrganizationOptions();
|
||||
},
|
||||
watch: {
|
||||
organizationTreeOptions() {
|
||||
if (this.vehicleBox) {
|
||||
this.syncOrgCascaderPaths();
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
hasPermission(code) {
|
||||
return this.isAdmin || this.permission?.[code] === true;
|
||||
},
|
||||
/** 去掉组织名称前导空白,供列表与提示展示 */
|
||||
formatOrgDisplayName(name) {
|
||||
return String(name || '').replace(/^[\s\u3000]+/, '');
|
||||
},
|
||||
/** 级联路径还原完整组织层级文案,悬停展示用 */
|
||||
formatOrgCascaderPath(path) {
|
||||
const ids = Array.isArray(path)
|
||||
? path.filter(item => item !== undefined && item !== null && item !== '')
|
||||
: [];
|
||||
if (!ids.length) return '';
|
||||
const labels = [];
|
||||
let nodes = this.organizationTreeOptions || [];
|
||||
for (const id of ids) {
|
||||
const node = nodes.find(item => String(item.id) === String(id));
|
||||
if (!node) break;
|
||||
labels.push(node.rawLabel || node.label || '');
|
||||
nodes = node.children || [];
|
||||
}
|
||||
return labels.filter(Boolean).join(' / ');
|
||||
},
|
||||
cloneOrgTree(tree = []) {
|
||||
return (tree || []).map(item => ({
|
||||
id: String(item.id),
|
||||
label: item.label,
|
||||
rawLabel: item.rawLabel,
|
||||
children: item.children?.length ? this.cloneOrgTree(item.children) : undefined,
|
||||
}));
|
||||
},
|
||||
syncOrgCascaderPaths() {
|
||||
this.organizationCascaderPath = this.resolveOrgPath(this.vehicleForm.organizationName);
|
||||
this.useDepartmentCascaderPath = this.resolveOrgPath(this.vehicleForm.useDepartment);
|
||||
},
|
||||
initDeptTree() {
|
||||
getDeptTree(this.userInfo.tenantId).then(res => {
|
||||
this.organizationOptions = this.flattenDeptOptions(res.data.data || []);
|
||||
@@ -760,6 +866,9 @@ export default {
|
||||
if (this.vehicleBox && !this.vehicleForm.useDepartment) {
|
||||
this.vehicleForm.useDepartment = this.getCurrentOrganizationName();
|
||||
}
|
||||
if (this.vehicleBox) {
|
||||
this.syncOrgCascaderPaths();
|
||||
}
|
||||
});
|
||||
},
|
||||
flattenDeptOptions(tree = [], level = 0) {
|
||||
@@ -854,6 +963,7 @@ export default {
|
||||
this.vehicleForm = emptyForm();
|
||||
this.vehicleForm.organizationName = this.getCurrentOrganizationName();
|
||||
this.vehicleForm.useDepartment = this.getCurrentOrganizationName();
|
||||
this.syncOrgCascaderPaths();
|
||||
this.syncPlateNo(false);
|
||||
this.vehicleBox = true;
|
||||
return;
|
||||
@@ -867,6 +977,7 @@ export default {
|
||||
if (!this.vehicleForm.useDepartment) {
|
||||
this.vehicleForm.useDepartment = this.getCurrentOrganizationName();
|
||||
}
|
||||
this.syncOrgCascaderPaths();
|
||||
this.splitPlateNo();
|
||||
this.syncPlateNo(false);
|
||||
this.vehicleBox = true;
|
||||
@@ -878,6 +989,8 @@ export default {
|
||||
},
|
||||
resetVehicle() {
|
||||
this.vehicleForm = emptyForm();
|
||||
this.organizationCascaderPath = [];
|
||||
this.useDepartmentCascaderPath = [];
|
||||
this.vehicleCertificateUploads = {};
|
||||
this.readonly = false;
|
||||
this.submitLoading = false;
|
||||
@@ -1530,7 +1643,20 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.org-cell-text {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.vehicle-form {
|
||||
.org-cascader-tooltip {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-date-editor.el-input),
|
||||
:deep(.el-date-editor.el-input__wrapper),
|
||||
:deep(.el-select),
|
||||
|
||||
@@ -101,6 +101,12 @@
|
||||
style="width: 500px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="减分最高扣分">
|
||||
<el-input :model-value="HARDCODED_MAX_MINUS_SCORE" disabled style="width: 500px">
|
||||
<template #append>分</template>
|
||||
</el-input>
|
||||
<div class="score-config__hint">暂定最高扣 15 分,加分合计同样最高按 15 分计</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input
|
||||
v-model="configForm.remark"
|
||||
@@ -489,9 +495,15 @@ const standardNumberFields = [
|
||||
'creditLimitIncrease',
|
||||
];
|
||||
|
||||
/** 暂定:加分/减分合计上限均为 15 分,超出按 15 分计 */
|
||||
const HARDCODED_MAX_PLUS_SCORE = 15;
|
||||
const HARDCODED_MAX_MINUS_SCORE = 15;
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
HARDCODED_MAX_PLUS_SCORE,
|
||||
HARDCODED_MAX_MINUS_SCORE,
|
||||
form: {},
|
||||
query: {},
|
||||
loading: true,
|
||||
@@ -628,6 +640,7 @@ export default {
|
||||
name: '',
|
||||
remark: '',
|
||||
standardDescription: '',
|
||||
maxMinusScore: HARDCODED_MAX_MINUS_SCORE,
|
||||
status: 3,
|
||||
categories: this.defaultCategories(),
|
||||
standards: [],
|
||||
@@ -684,6 +697,8 @@ export default {
|
||||
});
|
||||
return {
|
||||
...detail,
|
||||
// 暂定写死最高扣分,不读取历史配置值
|
||||
maxMinusScore: HARDCODED_MAX_MINUS_SCORE,
|
||||
categories: categories.map(category => ({
|
||||
...(map[category.categoryCode] || category),
|
||||
categoryName: category.categoryName,
|
||||
@@ -816,6 +831,7 @@ export default {
|
||||
},
|
||||
prepareSubmitConfig(config) {
|
||||
const payload = JSON.parse(JSON.stringify(config || {}));
|
||||
payload.maxMinusScore = this.toScoreValue(HARDCODED_MAX_MINUS_SCORE);
|
||||
(payload.categories || []).forEach(category => {
|
||||
(category.items || []).forEach(item => {
|
||||
item.score = this.toScoreValue(item.score);
|
||||
@@ -1072,6 +1088,8 @@ export default {
|
||||
this.$message.warning('请输入评定表名称');
|
||||
return false;
|
||||
}
|
||||
// 减分最高扣分暂定写死为 15,提交前强制回填,避免历史脏数据触发校验
|
||||
this.configForm.maxMinusScore = HARDCODED_MAX_MINUS_SCORE;
|
||||
const basic = this.configForm.categories.find(item => item.categoryCode === 'basic');
|
||||
if (!basic || basic.items.length === 0) {
|
||||
this.$message.warning('至少配置1个基础得分项目');
|
||||
@@ -1554,6 +1572,13 @@ export default {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.score-config__hint {
|
||||
margin-top: 6px;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
:deep(.score-config__name-input) {
|
||||
width: 500px;
|
||||
}
|
||||
|
||||
@@ -398,47 +398,6 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="经营范围" prop="businessScope">
|
||||
<el-select v-model="archiveForm.businessScope" multiple filterable clearable>
|
||||
<el-option
|
||||
v-for="item in businessScopeOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item
|
||||
label="营业期限"
|
||||
prop="businessEndDate"
|
||||
:required="archiveForm.businessTermType === '固定期限'"
|
||||
:rules="businessEndDateRules"
|
||||
>
|
||||
<div class="business-term-field">
|
||||
<el-date-picker
|
||||
v-model="archiveForm.businessEndDate"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
:disabled="archiveForm.businessTermType === '长期'"
|
||||
@change="handleBusinessEndDateChange"
|
||||
/>
|
||||
<el-checkbox
|
||||
:model-value="archiveForm.businessTermType === '长期'"
|
||||
@change="handleNoFixedTermChange"
|
||||
>
|
||||
无固定期限
|
||||
</el-checkbox>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="助记码" prop="mnemonicCode">
|
||||
<el-input v-model="archiveForm.mnemonicCode" maxlength="15" show-word-limit />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="客户简称" prop="shortName">
|
||||
<el-input
|
||||
@@ -499,38 +458,38 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</section-card>
|
||||
|
||||
<section-card title="联系信息">
|
||||
<el-row :gutter="18">
|
||||
<el-col :span="6">
|
||||
<el-form-item label="客商负责人" prop="principal">
|
||||
<el-input v-model="archiveForm.principal" maxlength="10" show-word-limit />
|
||||
<el-col :span="12">
|
||||
<el-form-item
|
||||
label="营业期限"
|
||||
prop="businessEndDate"
|
||||
:required="archiveForm.businessTermType === '固定期限'"
|
||||
:rules="businessEndDateRules"
|
||||
>
|
||||
<div class="business-term-field">
|
||||
<el-date-picker
|
||||
v-model="archiveForm.businessStartDate"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="开始日期"
|
||||
/>
|
||||
<span class="business-term-separator">至</span>
|
||||
<el-date-picker
|
||||
v-model="archiveForm.businessEndDate"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="截止日期"
|
||||
:disabled="archiveForm.businessTermType === '长期'"
|
||||
@change="handleBusinessEndDateChange"
|
||||
/>
|
||||
<el-checkbox
|
||||
:model-value="archiveForm.businessTermType === '长期'"
|
||||
@change="handleNoFixedTermChange"
|
||||
>
|
||||
无固定期限
|
||||
</el-checkbox>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="联系电话" prop="contactPhone">
|
||||
<el-input v-model="archiveForm.contactPhone" maxlength="20" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="所属组织" prop="deptName">
|
||||
<el-cascader
|
||||
v-model="deptCascaderValue"
|
||||
:options="deptTree"
|
||||
:props="deptCascaderProps"
|
||||
disabled
|
||||
placeholder="广西现代物流集团有限公司"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6"></el-col>
|
||||
</el-row>
|
||||
</section-card>
|
||||
|
||||
<section-card title="财务/信用信息">
|
||||
<el-row :gutter="18">
|
||||
<el-col :span="6">
|
||||
<el-form-item label="注册资金(万元)" prop="registeredCapital">
|
||||
<el-input
|
||||
@@ -542,11 +501,12 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="开票税点(%)" prop="invoiceTaxRate">
|
||||
<el-form-item label="实缴资本(万元)" prop="paidInCapital">
|
||||
<el-input
|
||||
v-model="archiveForm.invoiceTaxRate"
|
||||
maxlength="5"
|
||||
@input="value => handleDecimalInput('invoiceTaxRate', value, 13)"
|
||||
v-model="archiveForm.paidInCapital"
|
||||
maxlength="18"
|
||||
:disabled="kingdeeLocked"
|
||||
@input="value => handleDecimalInput('paidInCapital', value)"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -570,6 +530,29 @@
|
||||
<el-input v-model="archiveForm.applyCreditLimit" maxlength="18" disabled />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="已使用额度(万元)" prop="usedCreditLimit">
|
||||
<el-input
|
||||
v-model="archiveForm.usedCreditLimit"
|
||||
maxlength="18"
|
||||
:disabled="readonly || Boolean(archiveForm.id)"
|
||||
placeholder="新建可录历史已使用额度"
|
||||
@input="value => handleDecimalInput('usedCreditLimit', value)"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="经营范围" prop="businessScope" class="archive-form__remark">
|
||||
<el-input
|
||||
v-model="archiveForm.businessScope"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
maxlength="2000"
|
||||
show-word-limit
|
||||
placeholder="可通过营业执照 OCR 自动识别,也可手动填写"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="备注" prop="remark" class="archive-form__remark">
|
||||
<el-input
|
||||
@@ -644,18 +627,27 @@
|
||||
<span>{{ row.reviewStatus }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="160" fixed="right" align="center">
|
||||
<el-table-column label="操作" min-width="220" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="openScore(row.__raw, row.__rawIndex)">
|
||||
详情
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="showScoreDelete"
|
||||
type="danger"
|
||||
@click="deleteScore(row.__rawIndex)"
|
||||
>
|
||||
删除
|
||||
</el-link>
|
||||
<div class="score-list-table__actions">
|
||||
<el-link type="primary" @click="openScore(row.__raw, row.__rawIndex)">
|
||||
详情
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="canReviewScore(row)"
|
||||
type="primary"
|
||||
@click="startReviewScore(row.__raw, row.__rawIndex)"
|
||||
>
|
||||
复评
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="showScoreDelete"
|
||||
type="danger"
|
||||
@click="deleteScore(row.__rawIndex)"
|
||||
>
|
||||
删除
|
||||
</el-link>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -1155,7 +1147,14 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="开户行名称" prop="bankName">
|
||||
<el-input v-model="invoiceForm.bankName" maxlength="100" />
|
||||
<el-autocomplete
|
||||
v-model="invoiceForm.bankName"
|
||||
:fetch-suggestions="queryBankNameSuggestions"
|
||||
clearable
|
||||
maxlength="100"
|
||||
placeholder="请选择或输入开户行名称"
|
||||
class="invoice-bank-autocomplete"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
@@ -1625,6 +1624,9 @@ const customerArchiveFormRoute = '/vehicle/customer-archive/form';
|
||||
const customerArchiveListRefreshKey = 'customer-archive-list-refresh';
|
||||
/** 客商所属组织固定为顶层组织 */
|
||||
const FIXED_OWNER_DEPT_NAME = '广西现代物流集团有限公司';
|
||||
/** 暂定:加分/减分合计上限均为 15 分,超出按 15 分计 */
|
||||
const HARDCODED_MAX_PLUS_SCORE = 15;
|
||||
const HARDCODED_MAX_MINUS_SCORE = 15;
|
||||
|
||||
const createTimeRangeMap = {
|
||||
createTimeRange: ['createTimeStart', 'createTimeEnd'],
|
||||
@@ -1656,8 +1658,10 @@ export default {
|
||||
const nonNegativeLabelMap = {
|
||||
invoiceTaxRate: '开票税点',
|
||||
registeredCapital: '注册资金',
|
||||
paidInCapital: '实缴资本',
|
||||
maxCreditLimit: '最大资金使用额度',
|
||||
applyCreditLimit: '申请总资金使用额度',
|
||||
usedCreditLimit: '已使用额度',
|
||||
};
|
||||
const validateNonNegative = (rule, value, callback) => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
@@ -1668,15 +1672,6 @@ export default {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
const validateInvoiceTaxRate = (rule, value, callback) => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
callback();
|
||||
} else if (Number(value) < 0 || Number(value) > 13) {
|
||||
callback(new Error('开票税点必须在0到13之间'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
const validateUnifiedCreditCode = (rule, value, callback) => {
|
||||
const code = String(value || '')
|
||||
.trim()
|
||||
@@ -1806,7 +1801,7 @@ export default {
|
||||
{ label: '是', value: 1 },
|
||||
{ label: '否', value: 0 },
|
||||
],
|
||||
businessScopeOptions: [],
|
||||
bankListOptions: [],
|
||||
qualificationTypeOptions: [],
|
||||
creditLevelOptions: ['A', 'B', 'C', 'D', 'E', 'F'].map(item => ({
|
||||
label: `${item}级`,
|
||||
@@ -1845,15 +1840,12 @@ export default {
|
||||
],
|
||||
customerType: [{ required: true, message: '请选择客商分类', trigger: 'change' }],
|
||||
legalPerson: [{ required: true, message: '请输入企业法人', trigger: 'blur' }],
|
||||
contactPhone: [
|
||||
{ required: true, message: '请输入联系电话', trigger: 'blur' },
|
||||
{ pattern: /^1\d{10}$/, message: '联系电话必须为11位手机号', trigger: 'blur' },
|
||||
],
|
||||
deptName: [{ required: true, message: '请选择所属组织', trigger: 'change' }],
|
||||
invoiceTaxRate: [{ validator: validateInvoiceTaxRate, trigger: 'blur' }],
|
||||
registeredCapital: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||
paidInCapital: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||
maxCreditLimit: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||
applyCreditLimit: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||
usedCreditLimit: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||
businessScope: [{ max: 2000, message: '经营范围最多2000个字符', trigger: 'blur' }],
|
||||
registeredRegionName: [{ required: true, message: '请选择省市区', trigger: 'change' }],
|
||||
registeredDetailAddress: [
|
||||
{ required: true, message: '请输入详细地址', trigger: 'blur' },
|
||||
@@ -2296,7 +2288,8 @@ export default {
|
||||
showScoreDelete() {
|
||||
if (this.readonly) return false;
|
||||
const status = this.archiveForm.approvalStatus || 'draft';
|
||||
return status === 'draft' || status === 'rejected';
|
||||
// 草稿/驳回可删;审批通过后也允许删除并重建评分表(旧评分逻辑删除保留,新评分审核通过前仍用旧额度)
|
||||
return status === 'draft' || status === 'rejected' || status === 'approved';
|
||||
},
|
||||
isNewScore() {
|
||||
return this.scoreRecordIndex < 0 && !this.reviewMode;
|
||||
@@ -2348,8 +2341,17 @@ export default {
|
||||
];
|
||||
const rows = categoryMap.map((category, index) => {
|
||||
const details = this.getScoreCategoryDetails(category.categoryCode);
|
||||
const selfScore = details.reduce((sum, item) => sum + Number(item.selfScore || 0), 0);
|
||||
const reviewScore = details.reduce((sum, item) => sum + Number(item.reviewScore || 0), 0);
|
||||
let selfScore = details.reduce((sum, item) => sum + Number(item.selfScore || 0), 0);
|
||||
let reviewScore = details.reduce((sum, item) => sum + Number(item.reviewScore || 0), 0);
|
||||
// 暂定:加分/减分合计最高按 15 分计
|
||||
if (category.categoryCode === 'plus') {
|
||||
selfScore = Math.min(selfScore, HARDCODED_MAX_PLUS_SCORE);
|
||||
reviewScore = Math.min(reviewScore, HARDCODED_MAX_PLUS_SCORE);
|
||||
}
|
||||
if (category.categoryCode === 'minus') {
|
||||
selfScore = Math.min(selfScore, HARDCODED_MAX_MINUS_SCORE);
|
||||
reviewScore = Math.min(reviewScore, HARDCODED_MAX_MINUS_SCORE);
|
||||
}
|
||||
return {
|
||||
...category,
|
||||
index: index + 1,
|
||||
@@ -2406,7 +2408,6 @@ export default {
|
||||
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)
|
||||
@@ -2464,9 +2465,11 @@ export default {
|
||||
form.fullName = '';
|
||||
form.legalPerson = '';
|
||||
form.registeredCapital = '';
|
||||
form.paidInCapital = '';
|
||||
form.registeredDetailAddress = '';
|
||||
form.businessScope = [];
|
||||
form.businessScope = '';
|
||||
form.businessTermType = '固定期限';
|
||||
form.businessStartDate = '';
|
||||
form.businessEndDate = '';
|
||||
},
|
||||
// 新增态下统一社会信用代码录入完成后自动查询金蝶(A902-1),防抖 500ms;
|
||||
@@ -2525,14 +2528,7 @@ export default {
|
||||
}
|
||||
if (customer.businessterm) this.applyBusinessTermRecognition(customer.businessterm);
|
||||
if (customer.businessscope) {
|
||||
const matchedScopes = this.businessScopeOptions
|
||||
.filter(
|
||||
item =>
|
||||
customer.businessscope.includes(item.label) ||
|
||||
customer.businessscope.includes(item.value)
|
||||
)
|
||||
.map(item => item.value);
|
||||
if (matchedScopes.length) form.businessScope = matchedScopes;
|
||||
form.businessScope = String(customer.businessscope).trim();
|
||||
}
|
||||
if (!form.shortName && customer.simplename) form.shortName = customer.simplename;
|
||||
this.$nextTick(() => {
|
||||
@@ -2541,6 +2537,8 @@ export default {
|
||||
'fullName',
|
||||
'legalPerson',
|
||||
'registeredCapital',
|
||||
'paidInCapital',
|
||||
'businessStartDate',
|
||||
'businessEndDate',
|
||||
'businessScope',
|
||||
'registeredDetailAddress',
|
||||
@@ -2563,8 +2561,13 @@ export default {
|
||||
deptIds: '',
|
||||
deptName: '',
|
||||
customerType: [],
|
||||
businessScope: [],
|
||||
businessScope: '',
|
||||
businessTermType: '固定期限',
|
||||
businessStartDate: '',
|
||||
businessEndDate: '',
|
||||
registeredCapital: '',
|
||||
paidInCapital: '',
|
||||
usedCreditLimit: '',
|
||||
networkFreightPlatform: null,
|
||||
guangxiTop100: null,
|
||||
registeredRegionPath: [],
|
||||
@@ -2699,6 +2702,12 @@ export default {
|
||||
approvedTime: '审核通过时间',
|
||||
status: '状态',
|
||||
businessTermType: '营业期限类型',
|
||||
businessStartDate: '营业期限开始日',
|
||||
businessEndDate: '营业期限截止日',
|
||||
registeredCapital: '注册资金(万元)',
|
||||
paidInCapital: '实缴资本(万元)',
|
||||
usedCreditLimit: '已使用额度(万元)',
|
||||
businessScope: '经营范围',
|
||||
networkFreightPlatform: '网络货运平台',
|
||||
registeredRegionName: '注册地址',
|
||||
registeredDetailAddress: '详细地址',
|
||||
@@ -3147,11 +3156,21 @@ export default {
|
||||
const customerTypeColumn = this.findColumn(this.option.column, 'customerType');
|
||||
customerTypeColumn.dicData = [{ label: '全部', value: '' }, ...options];
|
||||
});
|
||||
this.loadDictOptions('scope_of_business', 'businessScopeOptions');
|
||||
this.loadDictOptions('bank_list', 'bankListOptions');
|
||||
this.loadDictOptions('customer_attachment_types', 'qualificationTypeOptions', [], () => {
|
||||
this.sortQualificationFiles();
|
||||
});
|
||||
},
|
||||
queryBankNameSuggestions(queryString, callback) {
|
||||
const query = String(queryString || '')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const options = Array.isArray(this.bankListOptions) ? this.bankListOptions : [];
|
||||
const matched = query
|
||||
? options.filter(item => String(item.value || '').toLowerCase().includes(query))
|
||||
: options;
|
||||
callback(matched.map(item => ({ value: item.value })));
|
||||
},
|
||||
initMkEnabled() {
|
||||
this._mkEnabledResolved = false;
|
||||
loadMkEnabled()
|
||||
@@ -3405,8 +3424,15 @@ export default {
|
||||
});
|
||||
},
|
||||
formatBusinessLicenseValidPeriod(row = {}) {
|
||||
if (row.businessTermType === '长期') return '无固定期限';
|
||||
return row.businessEndDate || '';
|
||||
if (row.businessTermType === '长期') {
|
||||
return row.businessStartDate
|
||||
? `${row.businessStartDate} 至 无固定期限`
|
||||
: '无固定期限';
|
||||
}
|
||||
if (row.businessStartDate && row.businessEndDate) {
|
||||
return `${row.businessStartDate} 至 ${row.businessEndDate}`;
|
||||
}
|
||||
return row.businessEndDate || row.businessStartDate || '';
|
||||
},
|
||||
formatFundUseRate(row = {}) {
|
||||
if (row.fundUseRate === undefined || row.fundUseRate === null || row.fundUseRate === '') {
|
||||
@@ -3939,8 +3965,13 @@ export default {
|
||||
const legalPerson = this.getBusinessLicenseOcrValue(result, ['法定代表人', '法人']);
|
||||
const address = this.getBusinessLicenseOcrValue(result, ['地址', '住所']);
|
||||
const capital = this.getBusinessLicenseOcrValue(result, ['注册资本']);
|
||||
const paidInCapital = this.getBusinessLicenseOcrValue(result, ['实缴资本', '实收资本']);
|
||||
const term = this.getBusinessLicenseOcrValue(result, ['营业期限', '有效期']);
|
||||
const termStartDate = this.getBusinessLicenseOcrValue(result, ['有效期起始日期']);
|
||||
const termStartDate = this.getBusinessLicenseOcrValue(result, [
|
||||
'有效期起始日期',
|
||||
'成立日期',
|
||||
'核准日期',
|
||||
]);
|
||||
const scope = this.getBusinessLicenseOcrValue(result, ['经营范围']);
|
||||
if (code) this.archiveForm.unifiedCreditCode = code.replace(/\s/g, '').toUpperCase();
|
||||
if (name) this.archiveForm.fullName = name;
|
||||
@@ -3955,15 +3986,16 @@ export default {
|
||||
const normalizedCapital = capital.replace(/,/g, '').match(/\d+(?:\.\d+)?/);
|
||||
if (normalizedCapital) this.archiveForm.registeredCapital = normalizedCapital[0];
|
||||
}
|
||||
this.applyBusinessTermRecognition(term);
|
||||
if (paidInCapital) {
|
||||
const normalizedPaidIn = paidInCapital.replace(/,/g, '').match(/\d+(?:\.\d+)?/);
|
||||
if (normalizedPaidIn) this.archiveForm.paidInCapital = normalizedPaidIn[0];
|
||||
}
|
||||
this.applyBusinessTermRecognition(term, termStartDate);
|
||||
if (this.isNoFixedBusinessTerm(term) || this.isNoFixedBusinessTerm(termStartDate)) {
|
||||
this.handleNoFixedTermChange(true);
|
||||
}
|
||||
if (scope) {
|
||||
const matchedScopes = this.businessScopeOptions
|
||||
.filter(item => scope.includes(item.label) || scope.includes(item.value))
|
||||
.map(item => item.value);
|
||||
if (matchedScopes.length) this.archiveForm.businessScope = matchedScopes;
|
||||
this.archiveForm.businessScope = scope;
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
[
|
||||
@@ -3971,27 +4003,49 @@ export default {
|
||||
'fullName',
|
||||
'legalPerson',
|
||||
'registeredCapital',
|
||||
'paidInCapital',
|
||||
'businessStartDate',
|
||||
'businessEndDate',
|
||||
'businessScope',
|
||||
'registeredDetailAddress',
|
||||
].forEach(prop => this.$refs.archiveForm?.validateField(prop));
|
||||
});
|
||||
},
|
||||
applyBusinessTermRecognition(term) {
|
||||
normalizeOcrDate(value) {
|
||||
if (!value) return '';
|
||||
const match = String(value).match(/(\d{4})[年./-](\d{1,2})[月./-](\d{1,2})/);
|
||||
if (!match) return '';
|
||||
const month = String(match[2]).padStart(2, '0');
|
||||
const day = String(match[3]).padStart(2, '0');
|
||||
return `${match[1]}-${month}-${day}`;
|
||||
},
|
||||
applyBusinessTermRecognition(term, termStartDate = '') {
|
||||
const startFromField = this.normalizeOcrDate(termStartDate);
|
||||
if (startFromField) {
|
||||
this.archiveForm.businessStartDate = startFromField;
|
||||
}
|
||||
if (!term) return;
|
||||
if (/长期|永久/.test(term)) {
|
||||
if (/长期|永久|无固定/.test(term) || this.isNoFixedBusinessTerm(term)) {
|
||||
this.archiveForm.businessTermType = '长期';
|
||||
this.archiveForm.businessEndDate = '';
|
||||
return;
|
||||
}
|
||||
const match = term.match(/(\d{4}[年./-]\d{1,2}[月./-]\d{1,2})/);
|
||||
if (!match) return;
|
||||
const date = match[1]
|
||||
.replace(/[年./]/g, '-')
|
||||
.replace('月', '-')
|
||||
.replace('日', '');
|
||||
this.archiveForm.businessTermType = '固定期限';
|
||||
this.archiveForm.businessEndDate = date;
|
||||
const dateMatches = String(term).match(/\d{4}[年./-]\d{1,2}[月./-]\d{1,2}/g) || [];
|
||||
const startDate = this.normalizeOcrDate(dateMatches[0]) || startFromField;
|
||||
const endDate = this.normalizeOcrDate(dateMatches[1]);
|
||||
if (startDate) {
|
||||
this.archiveForm.businessStartDate = startDate;
|
||||
}
|
||||
if (endDate) {
|
||||
this.archiveForm.businessTermType = '固定期限';
|
||||
this.archiveForm.businessEndDate = endDate;
|
||||
return;
|
||||
}
|
||||
if (startDate && dateMatches.length === 1 && !/至|到|-|—|~/.test(term)) {
|
||||
// 仅识别到一个日期且无区间分隔时,按截止日处理(兼容旧 OCR 结果)
|
||||
this.archiveForm.businessTermType = '固定期限';
|
||||
this.archiveForm.businessEndDate = startDate;
|
||||
}
|
||||
},
|
||||
getQualificationFileUrl(file = {}) {
|
||||
return file.url || file.link || file.domain || '';
|
||||
@@ -4159,7 +4213,6 @@ export default {
|
||||
return value;
|
||||
},
|
||||
normalizeDetail(detail) {
|
||||
const businessScope = this.splitValue(detail.businessScope);
|
||||
const registeredDetailAddress =
|
||||
detail.registeredDetailAddress ||
|
||||
(detail.registeredRegionName ? '' : detail.registeredAddress) ||
|
||||
@@ -4172,10 +4225,14 @@ export default {
|
||||
customerKind: detail.customerKind || 'external',
|
||||
invoiceTaxRate: this.normalizeOptionalAmount(detail.invoiceTaxRate),
|
||||
registeredCapital: this.normalizeOptionalAmount(detail.registeredCapital),
|
||||
paidInCapital: this.normalizeOptionalAmount(detail.paidInCapital),
|
||||
maxCreditLimit: this.normalizeOptionalAmount(detail.maxCreditLimit),
|
||||
applyCreditLimit: this.normalizeOptionalAmount(detail.applyCreditLimit),
|
||||
businessScope,
|
||||
usedCreditLimit: this.normalizeOptionalAmount(detail.usedCreditLimit),
|
||||
businessScope: detail.businessScope || '',
|
||||
businessTermType: detail.businessTermType || '固定期限',
|
||||
businessStartDate: detail.businessStartDate || '',
|
||||
businessEndDate: detail.businessEndDate || '',
|
||||
networkFreightPlatform:
|
||||
detail.networkFreightPlatform === 0 || detail.networkFreightPlatform === 1
|
||||
? detail.networkFreightPlatform
|
||||
@@ -4211,6 +4268,8 @@ export default {
|
||||
scoreDate: this.$dayjs().format('YYYY-MM-DD'),
|
||||
applyCreditLimit: this.archiveForm.applyCreditLimit || '',
|
||||
maxCreditLimit: '',
|
||||
maxMinusScore: HARDCODED_MAX_MINUS_SCORE,
|
||||
maxPlusScore: HARDCODED_MAX_PLUS_SCORE,
|
||||
creditLevel: '',
|
||||
standards: [],
|
||||
tempApplyCreditLimit: '',
|
||||
@@ -4230,6 +4289,8 @@ export default {
|
||||
: score.applyCreditLimit
|
||||
),
|
||||
maxCreditLimit: this.normalizeOptionalAmount(score.maxCreditLimit),
|
||||
maxMinusScore: HARDCODED_MAX_MINUS_SCORE,
|
||||
maxPlusScore: HARDCODED_MAX_PLUS_SCORE,
|
||||
tempApplyCreditLimit: this.normalizeOptionalAmount(score.tempApplyCreditLimit),
|
||||
attachments: score.attachments || this.parseAttachments(score.proofAttachments),
|
||||
details: (score.details || []).map(detail => ({
|
||||
@@ -4268,7 +4329,14 @@ export default {
|
||||
String(item.score.creditLevel || '').trim()
|
||||
)[0]?.score;
|
||||
},
|
||||
/** 曾审核通过后:删除/重建评分在再次审核通过前,主表等级与额度保持旧值暂时生效 */
|
||||
shouldFreezeEffectiveCredit(archive = this.archiveForm) {
|
||||
return Boolean(archive?.approvedTime);
|
||||
},
|
||||
applyArchiveCreditFromScores(archive) {
|
||||
if (this.shouldFreezeEffectiveCredit(archive)) {
|
||||
return archive;
|
||||
}
|
||||
const latestScore = this.getLatestCreditScore(archive.scores || []);
|
||||
archive.customerLevel = latestScore?.creditLevel || archive.customerLevel || '';
|
||||
archive.maxCreditLimit = latestScore?.maxCreditLimit || archive.maxCreditLimit || '';
|
||||
@@ -4397,16 +4465,21 @@ export default {
|
||||
if (archive.businessTermType === '长期') {
|
||||
archive.businessEndDate = null;
|
||||
}
|
||||
if (!archive.businessStartDate) {
|
||||
archive.businessStartDate = null;
|
||||
}
|
||||
const invoiceTaxRate = this.normalizeOptionalAmount(archive.invoiceTaxRate);
|
||||
const registeredCapital = this.normalizeOptionalAmount(archive.registeredCapital);
|
||||
const paidInCapital = this.normalizeOptionalAmount(archive.paidInCapital);
|
||||
const usedCreditLimit = this.normalizeOptionalAmount(archive.usedCreditLimit);
|
||||
archive.invoiceTaxRate = invoiceTaxRate === '' ? null : invoiceTaxRate;
|
||||
archive.registeredCapital = registeredCapital === '' ? null : registeredCapital;
|
||||
archive.paidInCapital = paidInCapital === '' ? null : paidInCapital;
|
||||
archive.usedCreditLimit = usedCreditLimit === '' ? null : usedCreditLimit;
|
||||
archive.customerType = Array.isArray(archive.customerType)
|
||||
? archive.customerType.join(',')
|
||||
: archive.customerType;
|
||||
archive.businessScope = Array.isArray(archive.businessScope)
|
||||
? archive.businessScope.join(',')
|
||||
: archive.businessScope;
|
||||
archive.businessScope = String(archive.businessScope || '').trim();
|
||||
archive.networkFreightPlatform =
|
||||
archive.networkFreightPlatform === 0 || archive.networkFreightPlatform === 1
|
||||
? archive.networkFreightPlatform
|
||||
@@ -4437,6 +4510,11 @@ export default {
|
||||
return contact;
|
||||
})
|
||||
.filter(item => item.contactName || item.contactPhone);
|
||||
// 主表联系电话改由联系人维护,取第一条有效手机号回填
|
||||
const firstContactPhone = (archive.contacts || [])
|
||||
.map(item => String(item.contactPhone || '').trim())
|
||||
.find(Boolean);
|
||||
archive.contactPhone = firstContactPhone || null;
|
||||
archive.receiptAccounts = (archive.receiptAccounts || [])
|
||||
.map(item => this.normalizeReceipt(item))
|
||||
.filter(item => item.accountName || item.accountHolderName || item.bankAccount || item.cnapsCode);
|
||||
@@ -4475,16 +4553,29 @@ export default {
|
||||
archive.applyCreditLimit = applyCreditLimit === '' ? null : applyCreditLimit;
|
||||
return archive;
|
||||
},
|
||||
validateContactsRequired(archive = this.archiveForm) {
|
||||
const contacts = (archive.contacts || []).filter(
|
||||
item => String(item.contactName || '').trim() || String(item.contactPhone || '').trim()
|
||||
);
|
||||
if (!contacts.length) {
|
||||
this.$message.warning('请至少维护一条联系人');
|
||||
this.activeTab = 'contacts';
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
saveArchive() {
|
||||
if (this.kingdeeBlocked) {
|
||||
this.$message.warning('金蝶审核中,请稍后重新添加');
|
||||
return;
|
||||
}
|
||||
this.applyFixedOwnerDept();
|
||||
this.$refs.archiveForm.validate(valid => {
|
||||
if (!valid) {
|
||||
this.$message.warning('请先完整填写必填项');
|
||||
return;
|
||||
}
|
||||
if (!this.validateContactsRequired()) return;
|
||||
submit(this.normalizeArchive()).then(() => {
|
||||
this.closeArchive();
|
||||
this.onLoad(this.page);
|
||||
@@ -4497,11 +4588,13 @@ export default {
|
||||
this.$message.warning('金蝶审核中,请稍后重新添加');
|
||||
return;
|
||||
}
|
||||
this.applyFixedOwnerDept();
|
||||
this.$refs.archiveForm.validate(valid => {
|
||||
if (!valid) {
|
||||
this.$message.warning('请先完整填写必填项');
|
||||
return;
|
||||
}
|
||||
if (!this.validateContactsRequired()) return;
|
||||
const archive = this.normalizeArchive();
|
||||
// 仅提交时落变更记录;保存不记录
|
||||
archive.recordChange = true;
|
||||
@@ -4577,9 +4670,31 @@ export default {
|
||||
}
|
||||
this.$nextTick(() => this.openScore(scores[0], 0));
|
||||
},
|
||||
/** 提交后详情页评分表:审批中且未完成复评时可复评 */
|
||||
canReviewScore(row = {}) {
|
||||
return (
|
||||
this.hasPermission('customer_type_re_cert') &&
|
||||
this.archiveForm.approvalStatus === 'reviewing' &&
|
||||
row.reviewStatus !== '已完成'
|
||||
);
|
||||
},
|
||||
startReviewScore(row, index = 0) {
|
||||
if (!row) {
|
||||
this.$message.warning('暂无评分记录,无法复评');
|
||||
return;
|
||||
}
|
||||
// 详情只读进入时也要能改复评得分并确认
|
||||
this.reviewMode = true;
|
||||
this.openScore(row, index);
|
||||
},
|
||||
addScore() {
|
||||
if ((this.archiveForm.scores || []).length) {
|
||||
this.$message.warning('请勿重复添加');
|
||||
const existingScores = this.archiveForm.scores || [];
|
||||
if (existingScores.length) {
|
||||
this.$message.warning(
|
||||
this.shouldFreezeEffectiveCredit()
|
||||
? '请先删除原评分记录后再添加新评分表'
|
||||
: '请勿重复添加'
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.scoreRecordIndex = -1;
|
||||
@@ -4600,9 +4715,9 @@ export default {
|
||||
const loadOptions = this.scoreQuantificationOptions.length
|
||||
? Promise.resolve()
|
||||
: this.initScoreQuantificationOptions();
|
||||
loadOptions.then(() =>
|
||||
this.ensureScoreQuantificationOption(this.currentScore.quantificationId)
|
||||
);
|
||||
loadOptions
|
||||
.then(() => this.ensureScoreQuantificationOption(this.currentScore.quantificationId))
|
||||
.then(() => this.applyMaxMinusScoreFromQuantification(this.currentScore.quantificationId));
|
||||
},
|
||||
resetScoreDialog() {
|
||||
this.scoreRecordIndex = -1;
|
||||
@@ -4637,6 +4752,8 @@ export default {
|
||||
applyCreditLimit: this.currentScore.applyCreditLimit,
|
||||
maxCreditLimit: '',
|
||||
creditLevel: '',
|
||||
maxMinusScore: HARDCODED_MAX_MINUS_SCORE,
|
||||
maxPlusScore: HARDCODED_MAX_PLUS_SCORE,
|
||||
tempApplyCreditLimit: this.currentScore.tempApplyCreditLimit,
|
||||
tempCreditStartDate: this.currentScore.tempCreditStartDate,
|
||||
tempCreditEndDate: this.currentScore.tempCreditEndDate,
|
||||
@@ -4653,6 +4770,14 @@ export default {
|
||||
this.calculateScore(this.currentScore);
|
||||
});
|
||||
},
|
||||
applyMaxMinusScoreFromQuantification(quantificationId) {
|
||||
if (!quantificationId) return Promise.resolve();
|
||||
// 暂定写死加分/减分上限,不再依赖量化表配置值
|
||||
this.currentScore.maxMinusScore = HARDCODED_MAX_MINUS_SCORE;
|
||||
this.currentScore.maxPlusScore = HARDCODED_MAX_PLUS_SCORE;
|
||||
this.calculateScore(this.currentScore);
|
||||
return Promise.resolve();
|
||||
},
|
||||
normalizeScoreTemplateDetails(template = {}) {
|
||||
const categoryDetails = (template.categories || []).flatMap(category =>
|
||||
(category.items || []).map(item => ({
|
||||
@@ -4797,13 +4922,24 @@ export default {
|
||||
if (!valid) return null;
|
||||
|
||||
const distance = increase ? input - base : base - input;
|
||||
const changeValue = Number(rule.changeValue);
|
||||
const stepSize = Number.isFinite(changeValue) && changeValue > 0 ? changeValue : 1;
|
||||
const steps = Math.floor(distance / stepSize + 1e-9);
|
||||
const stepScore = Math.abs(Number(rule.score || 0));
|
||||
if (!Number.isFinite(stepScore)) return null;
|
||||
if (!Number.isFinite(stepScore) || steps < 0) return null;
|
||||
|
||||
const projectScore = this.getScoreItemFullScore(detail);
|
||||
// 减分项:按阶梯累计扣分,存正数,总分计算时再减;不做项目满分截断
|
||||
if (this.isMinusScoreDetail(detail)) {
|
||||
return Number((steps * stepScore).toFixed(2));
|
||||
}
|
||||
|
||||
// 基础/加分项:从项目分值起步,按阶梯加减,限制在 [0, 项目满分]
|
||||
const baseScore = Number(detail.score || 0);
|
||||
const signedStep = rule.scoreType === 'subtract' ? -stepScore : stepScore;
|
||||
const calculatedScore = Number(detail.score || 0) + distance * signedStep;
|
||||
return Number(Math.min(Math.max(calculatedScore, 0), projectScore).toFixed(2));
|
||||
const calculatedScore = baseScore + steps * signedStep;
|
||||
const projectScore = this.getScoreItemFullScore(detail);
|
||||
const upper = Math.max(projectScore, baseScore, 0);
|
||||
return Number(Math.min(Math.max(calculatedScore, 0), upper).toFixed(2));
|
||||
},
|
||||
handleScoreValueInput(detail, value) {
|
||||
const raw = String(value ?? '')
|
||||
@@ -4984,14 +5120,17 @@ export default {
|
||||
const plusDetails = this.getScoreCategoryDetailsByList(details, 'plus');
|
||||
const minusDetails = this.getScoreCategoryDetailsByList(details, 'minus');
|
||||
const sumScore = (list, prop) => list.reduce((sum, item) => sum + Number(item[prop] || 0), 0);
|
||||
const selfScore =
|
||||
sumScore(basicDetails, 'selfScore') +
|
||||
sumScore(plusDetails, 'selfScore') -
|
||||
sumScore(minusDetails, 'selfScore');
|
||||
const reviewScore =
|
||||
sumScore(basicDetails, 'reviewScore') +
|
||||
sumScore(plusDetails, 'reviewScore') -
|
||||
sumScore(minusDetails, 'reviewScore');
|
||||
const rawPlusSelf = sumScore(plusDetails, 'selfScore');
|
||||
const rawPlusReview = sumScore(plusDetails, 'reviewScore');
|
||||
const rawMinusSelf = sumScore(minusDetails, 'selfScore');
|
||||
const rawMinusReview = sumScore(minusDetails, 'reviewScore');
|
||||
// 暂定:加分/减分合计最高按 15 分计
|
||||
const plusSelf = Math.min(rawPlusSelf, HARDCODED_MAX_PLUS_SCORE);
|
||||
const plusReview = Math.min(rawPlusReview, HARDCODED_MAX_PLUS_SCORE);
|
||||
const minusSelf = Math.min(rawMinusSelf, HARDCODED_MAX_MINUS_SCORE);
|
||||
const minusReview = Math.min(rawMinusReview, HARDCODED_MAX_MINUS_SCORE);
|
||||
const selfScore = sumScore(basicDetails, 'selfScore') + plusSelf - minusSelf;
|
||||
const reviewScore = sumScore(basicDetails, 'reviewScore') + plusReview - minusReview;
|
||||
const hasReviewScore = details.some(
|
||||
item => !this.isBlankScore(item.reviewScore)
|
||||
);
|
||||
@@ -5065,7 +5204,11 @@ export default {
|
||||
return pending;
|
||||
},
|
||||
deleteScore(index) {
|
||||
this.$confirm('确定删除该评分记录?', '提示', {
|
||||
const freezeCredit = this.shouldFreezeEffectiveCredit();
|
||||
const confirmText = freezeCredit
|
||||
? '确定删除该评分记录?删除后可重新添加评分表;原评分数据会保留,新评分再次审核通过前,客商等级与额度仍按原评分暂时生效。'
|
||||
: '确定删除该评分记录?';
|
||||
this.$confirm(confirmText, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
@@ -5078,16 +5221,23 @@ export default {
|
||||
} else if (this.scoreRecordIndex > index) {
|
||||
this.scoreRecordIndex -= 1;
|
||||
}
|
||||
const latestScore = this.getLatestCreditScore(this.archiveForm.scores);
|
||||
if (latestScore) {
|
||||
this.syncArchiveCreditFromScores();
|
||||
} else {
|
||||
this.archiveForm.customerLevel = '';
|
||||
this.archiveForm.maxCreditLimit = '';
|
||||
this.archiveForm.applyCreditLimit = '';
|
||||
if (!freezeCredit) {
|
||||
const latestScore = this.getLatestCreditScore(this.archiveForm.scores);
|
||||
if (latestScore) {
|
||||
this.syncArchiveCreditFromScores();
|
||||
} else {
|
||||
this.archiveForm.customerLevel = '';
|
||||
this.archiveForm.maxCreditLimit = '';
|
||||
this.archiveForm.applyCreditLimit = '';
|
||||
}
|
||||
}
|
||||
this.handleScoreCurrentChange(this.scorePage.currentPage);
|
||||
this.saveScoreArchive('评分记录已删除', '评分记录已删除,请保存客商档案后生效');
|
||||
this.saveScoreArchive(
|
||||
freezeCredit
|
||||
? '评分记录已删除,原等级与额度暂时继续生效'
|
||||
: '评分记录已删除',
|
||||
'评分记录已删除,请保存客商档案后生效'
|
||||
);
|
||||
})
|
||||
.catch(() => {});
|
||||
},
|
||||
@@ -5223,6 +5373,7 @@ export default {
|
||||
getDetail(row.id).then(res => {
|
||||
const archive = this.normalizeDetail(res.data.data);
|
||||
archive.qualificationAttachments = archive.qualificationAttachments || '';
|
||||
if (!this.validateContactsRequired(archive)) return;
|
||||
if (!this.validateScoreForApproval(archive)) return;
|
||||
this.$confirm('是否提交客商准入审批?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
@@ -5604,7 +5755,8 @@ export default {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: nowrap;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
|
||||
:deep(.el-date-editor) {
|
||||
flex: 0 0 140px;
|
||||
@@ -5612,6 +5764,11 @@ export default {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.business-term-separator {
|
||||
flex: 0 0 auto;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox) {
|
||||
flex: 0 0 auto;
|
||||
margin-right: 0;
|
||||
@@ -5619,6 +5776,10 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.invoice-bank-autocomplete {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.archive-form__address {
|
||||
:deep(.el-cascader) {
|
||||
flex: 0 0 240px;
|
||||
@@ -5827,6 +5988,14 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.score-list-table__actions {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.contact-form,
|
||||
.receipt-form,
|
||||
.invoice-form {
|
||||
@@ -5846,11 +6015,13 @@ export default {
|
||||
|
||||
:deep(.el-input),
|
||||
:deep(.el-cascader),
|
||||
:deep(.el-select) {
|
||||
:deep(.el-select),
|
||||
:deep(.el-autocomplete) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-cascader .el-input) {
|
||||
:deep(.el-cascader .el-input),
|
||||
:deep(.el-autocomplete .el-input) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
+15
-15
@@ -49,28 +49,28 @@ export default ({ mode, command }) => {
|
||||
__VUE_I18N_LEGACY_API__: true,
|
||||
__INTLIFY_PROD_DEVTOOLS__: false,
|
||||
},
|
||||
// server: {
|
||||
// port: 2888,
|
||||
// proxy: {
|
||||
// '/api': {
|
||||
// // 本地网关默认 80 端口(blade-gateway bootstrap.yml);开发时需先启动网关及 blade-auth/blade-system 等服务
|
||||
// // 注意:本机 8080 一般是 Nacos,不是业务网关
|
||||
// target: 'http://localhost',
|
||||
// // target: 'http://172.16.203.228:8000', // 仅联调远程网关时启用(且不要 rewrite)
|
||||
// changeOrigin: true,
|
||||
// rewrite: path => path.replace(/^\/api/, ''),
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
server: {
|
||||
port: 2889,
|
||||
port: 2888,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://172.16.203.228:8000',
|
||||
// 本地网关默认 80 端口(blade-gateway bootstrap.yml);开发时需先启动网关及 blade-auth/blade-system 等服务
|
||||
// 注意:本机 8080 一般是 Nacos,不是业务网关
|
||||
target: 'http://localhost',
|
||||
// target: 'http://172.16.203.228:8000', // 仅联调远程网关时启用(且不要 rewrite)
|
||||
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, './'),
|
||||
|
||||
@@ -214,6 +214,7 @@
|
||||
- 到场、装货、发货、在途、到货、卸货、签收节点的操作要求包含是否打卡、打卡定位;装货、发货、到货、卸货、签收还包含上传货量和上传货量类型;附件要求包含上传凭证和凭证类型。
|
||||
- 在途节点额外包含打卡频次和打卡时段,打卡频次使用普通输入框,打卡时段使用时间选择器。
|
||||
- 回单节点的操作要求包含是否确认、确认完成,是否确认提供“是 / 无需确认完成”单选,确认完成使用“内部人员”复选框。
|
||||
- 回单节点的“内部人员”复选框只读,不接受手工勾选:是否确认为“是”时自动勾选(即回单需内部人员确认),为“无需确认完成”时自动取消勾选。
|
||||
- 各节点中是否打卡、上传货量、上传凭证均使用单选或复选输入,控件类型与截图保持一致。
|
||||
|
||||
- 异常与边界:
|
||||
@@ -233,6 +234,7 @@
|
||||
- 不允许非在途节点配置打卡频次和打卡时段。
|
||||
- 不允许是否打卡为否时仍强制填写打卡定位或打卡时段。
|
||||
- 不允许上传凭证关闭后仍提交凭证类型。
|
||||
- 不允许手工勾选或取消回单节点的“内部人员”复选框。
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user