Compare commits

..

40 Commits

Author SHA1 Message Date
b2894lxlx 74988c0b5e Merge pull request '项目+合同对接合同系统' (#8) from feature/hcs-api-p0-web into master
Reviewed-on: #8
2026-10-27 14:47:14 +08:00
b2894lxlx 4c117d87a5 ✨ 角色数据权限配置并移除用户侧数据权限
角色权限弹窗支持按菜单批量设置数据范围;用户管理去掉组织/客商权限配置;角色别名改为角色标识。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-27 04:05:02 +08:00
b2894lxlx d6bcb12c20 Merge remote-tracking branch 'websoft/dev'
# Conflicts:
#	src/views/transportCapacity/vehicle.vue
2026-10-26 10:51:35 +08:00
b2894lxlx e1d87f5788 ✨ 结算汇率反算、开票合计填报、配载承运商交集与车辆组织悬停
外币结算按本位币/汇率反算;开票改填合计金额并反算不含税单价;配载展示承运商合同并按乙方交集限选;评分加减分封顶15分;车辆所有组织/使用部门悬停显示完整路径。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-26 10:50:58 +08:00
weicw 56b8f68449 ✨ 过程配置回单「内部人员」改为只读并按是否确认自动勾选 2026-10-25 23:10:24 +08:00
weicw 0ac17dea0b 🐛 fix(运单管理): 打卡记录里程回显按正整数归一
后端若仍下发 "200.00",编辑打卡记录时原样保存会被正整数校验拒掉。
2026-10-25 21:57:33 +08:00
weicw 39e3115fbe 🔀 合并「所属组织不是小程序注册与司机建档门槛」 2026-10-25 20:24:41 +08:00
weicw 9c0f7cb3f4 📝 所属组织可为空的注释改为事实句 2026-10-25 20:21:16 +08:00
weicw 7b478bdbf2 📝 修正未分配组织筛选的注释与接入步骤 2026-10-25 20:21:16 +08:00
weicw 87d6ff97a3 🔀 合并票 06 前端未分配组织筛选 2026-10-25 19:50:08 +08:00
weicw e3bd44e8fc 🔀 合并票 05 前端取消所属组织必填 2026-10-25 19:49:54 +08:00
weicw 324daf9d4d ✨ 司机与车辆列表支持按未分配组织筛选 2026-10-25 19:48:08 +08:00
weicw 37ac683fb4 🐛 前端取消所属组织必填 2026-10-25 19:46:42 +08:00
weicw c402d68aea 🔥 管理端打卡照片去掉主图角标
主图角标按分组下标标在第一张上,既没有独立字段也不能切换,与司机端一致删除;v-for 不再需要下标。
2026-10-24 22:19:23 +08:00
weicw e8f6311477 ✨ 编辑打卡记录支持里程采集与结算后冻结 2026-10-24 21:29:21 +08:00
b2894lxlx fa78a68df6 🐛 过程配置项目下拉兼容变更已通过状态
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-24 14:56:10 +08:00
b2894lxlx 320422856c 🔀 合并 websoft/dev,统一待回单确认与公路配载校验
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-24 14:55:04 +08:00
weicw-dshsa 0add4c5cdc ♻️ 派单候选司机不再按承运商组织过滤
- 新增 fetchDriverCandidates:按租户与岗位取司机,不再解析承运商联系人组织
- driverOptionText 只做展示(姓名+手机尾号后四位),写回表单的仍是姓名
- 删除 fetchDriversByCarrierOrganizations 与无调用者的 clearCarrierOrganizationCache
- 运单管理、总单派车、运输计划、装卸管理四页同步;去掉「请先选择承运商」前置
2026-10-23 20:03:49 +08:00
weicw-dshsa 372e8826a2 🔀 合并待回单确认功能 2026-10-22 16:51:06 +08:00
weicw-dshsa 92a8d4aab1 🔀 合并待回单确认审查修整 2026-10-22 14:53:04 +08:00
weicw-dshsa 59a1ca8a0d ♻️ 删除待回单确认标签色死分支并订正注释 2026-10-22 14:39:50 +08:00
weicw-dshsa 5544cfb3ca 🔀 合并待回单确认禁止补录打卡入口 2026-10-22 13:48:56 +08:00
weicw-dshsa fcd5ef7df9 🔀 合并待回单确认主分支 2026-10-22 13:44:19 +08:00
weicw-dshsa 489b406214 🔀 合并待回单确认管理端动作门禁 2026-10-22 13:44:10 +08:00
weicw-dshsa 353148eb7f 🐛 待回单确认下拒绝补录打卡 2026-10-22 13:44:06 +08:00
weicw-dshsa 293b4fc6ce 🔀 合并待回单确认前端状态名称 2026-10-22 13:44:03 +08:00
weicw-dshsa 53b588063e 🐛 待回单确认运单支持管理端完成与取消 2026-10-22 13:42:43 +08:00
weicw-dshsa 0889f29c59 🐛 待回单确认补齐前端状态名称与标签色 2026-10-22 13:40:46 +08:00
weicw-dshsa f1c004813e 🔀 合并待回单确认 web 状态与确认按钮 2026-10-22 13:36:32 +08:00
weicw-dshsa d67cf55491 ✨ 运单列表支持待回单确认状态 2026-10-22 13:32:35 +08:00
weicw-dshsa 6384e80758 🐛 司机档案住址与出生年月口径对齐小程序
- 住址只读态直接显示区划名称串,不再用级联反解(改名后会退化成灰字 placeholder)
- 身份证号联动改为失焦触发,且只在出生年月/性别为空时补,不覆盖手填值
- 认证审核页标签改为「住址」「详细地址」
- 详细地址 placeholder 补明采集范围,空住址提示改为「请选择省/市/区」
2026-10-22 00:17:55 +08:00
weicw-dshsa d3af455374 📝 修正配载候选运单组织范围注释 2026-10-20 11:54:49 +08:00
weicw-dshsa 1622015724 🔀 合并 origin/master 到 dev
# Conflicts:
#	src/views/business/components/business-crud-page.vue
2026-10-19 19:06:34 +08:00
weicw-dshsa f7d8fbf16b 🐛 配载候选列表不再按运输方式过滤并订正可配载注释 2026-10-18 15:49:38 +08:00
weicw-dshsa 847f09b14d ♻️ 运单管理公路配载改用可配载查询回查并清理自造判据 2026-10-18 14:51:33 +08:00
weicw-dshsa 2ae817ec38 ✨ 运单管理新增「确认回单」行操作
回单节点配成「需业务员确认」时,运单会一直停在进行中等人确认,此前后台没有这个入口。
在与「完成」并列的位置加一个「确认回单」行操作:只在进行中的运单上出现,沿用「完成」的
权限码(它比完成更严——后端还要校验司机打卡节点齐全且无未了结异常,故不会多开出任何权限)。
确认后不可撤销,提示里写明。
2026-10-18 12:15:07 +08:00
weicw-dshsa 6d80ffde6c ✨ 认证审核对比补充出生年月与住址字段
审核对比的字段元数据加上 birthday / addressRegion / address:司机端本次新提交
的这几项若不登记,审核页既不展示也 diff 不出改动,审核员等于在不知情下通过。
2026-10-17 12:38:17 +08:00
weicw-dshsa 44809ad303 Merge remote-tracking branch 'origin/master' into dev 2026-10-17 01:12:09 +08:00
weicw-dshsa 91cb3ee8c2 🐛 修复运输计划调度确认生成仍强制校验司机 2026-10-16 12:57:14 +08:00
weicw-dshsa b2a6d73bac 🐛 修复导入运单明细重载后实际/预计时间列为空
明细接口返回运单实体字段 startDate/endDate/estimatedStartTime/estimatedEndTime,
明细表与编辑回填按导入列字段名 actualStartDate 等读取,重载后统一在 normalizeDetailRow 对齐。
2026-10-16 12:10:47 +08:00
34 changed files with 1021 additions and 17088 deletions
+7 -2
View File
@@ -24,11 +24,16 @@ export const getDetail = id =>
}, },
}); });
export const getCarrierContracts = projectIds => export const getCarrierContracts = (projectIds, waybillIds = []) =>
request({ request({
url: `${baseUrl}/carrier-contracts`, url: `${baseUrl}/carrier-contracts`,
method: 'get', method: 'get',
params: { projectIds: (projectIds || []).join(',') }, params: {
projectIds: (projectIds || []).join(','),
...(waybillIds || []).filter(Boolean).length
? { waybillIds: (waybillIds || []).filter(Boolean).join(',') }
: {},
},
}); });
export const saveDraft = row => export const saveDraft = row =>
+1 -1
View File
@@ -14,7 +14,7 @@ export const cancel = api.cancel;
export const reassign = api.reassign; export const reassign = api.reassign;
export const complete = api.complete; export const complete = api.complete;
export const batchComplete = api.batchComplete; export const batchComplete = api.batchComplete;
/** 确认回单:待确认回单运单完成后转为已完成 */ /** 确认回单:回单节点配成「需业务员确认」时用;后端校验司机打卡节点齐全且无未了结异常后才置完成 */
export const confirmReturn = id => export const confirmReturn = id =>
request({ request({
url: `${baseUrl}/confirm-return`, url: `${baseUrl}/confirm-return`,
+19
View File
@@ -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,
});
};
+27 -2
View File
@@ -12,6 +12,8 @@ import { h } from 'vue';
* 1. 混入本 mixin:mixins: [organizationSearch] * 1. 混入本 mixin:mixins: [organizationSearch]
* 2. option 中「所属组织」列加上 searchslot: true(prop 为 organizationName 或 deptName 均可) * 2. option 中「所属组织」列加上 searchslot: true(prop 为 organizationName 或 deptName 均可)
* 3. 在 created / 初始化时调用 this.loadOrganizationOptions() * 3. 在 created / 初始化时调用 this.loadOrganizationOptions()
* 4. 需要「未分配组织」筛选项的页面在 data 里置 enableUnassignedOrgFilter: true;
* 该项只追加进搜索树 organizationSearchTreeOptions,表单级联 organizationTreeOptions 不含哨兵
* *
* 说明: * 说明:
* - 自动识别 organizationName / deptName 两种 prop,并挂载 column.renderSearch * - 自动识别 organizationName / deptName 两种 prop,并挂载 column.renderSearch
@@ -26,6 +28,12 @@ import { h } from 'vue';
*/ */
const ORG_PROPS = ['organizationName', 'deptName']; const ORG_PROPS = ['organizationName', 'deptName'];
// 「未分配组织」筛选项的哨兵值:不是真实组织名,选中后提交给后端的就是这串字面量。
// 后端 TransportBusiConstant.UNASSIGNED_ORG_FILTER、DriverMapper.xml / TransportVehicleMapper.xml
// 的 organizationName 分支以及司机、车辆导出查询都按它改判为 organization_name = '';
// 后端换成空串会让整段组织条件不生效,筛选静默失效。前端与后端这几处必须同步修改。
const UNASSIGNED_ORG_FILTER = '__UNASSIGNED_ORG__';
export default { export default {
data() { data() {
return { return {
@@ -34,9 +42,18 @@ export default {
organizationTreeOptions: [], organizationTreeOptions: [],
organizationTreeFlatOptions: [], organizationTreeFlatOptions: [],
organizationLoading: false, organizationLoading: false,
// 组织筛选是否追加「未分配组织」一项;页面在 data 里覆盖为 true 才生效
enableUnassignedOrgFilter: false,
}; };
}, },
computed: { computed: {
// 搜索栏级联的选项:开关打开时在真实组织最前面追加「未分配组织」。
// 不能直接改 organizationTreeOptions——司机/车辆页的编辑表单也用这棵树选组织。
organizationSearchTreeOptions() {
return this.enableUnassignedOrgFilter
? [this.unassignedOrgOption(), ...this.organizationTreeOptions]
: this.organizationTreeOptions;
},
// 所属组织级联配置:checkStrictly 允许选中任意层级,不必选到末级 // 所属组织级联配置:checkStrictly 允许选中任意层级,不必选到末级
organizationCascaderProps() { organizationCascaderProps() {
return { return {
@@ -61,7 +78,7 @@ export default {
? this.excludeExternalOrganization(source) ? this.excludeExternalOrganization(source)
: source; : source;
this.organizationTreeOptions = this.normalizeOrgTree(tree); this.organizationTreeOptions = this.normalizeOrgTree(tree);
this.organizationTreeFlatOptions = this.flattenOrgTree(this.organizationTreeOptions); this.organizationTreeFlatOptions = this.flattenOrgTree(this.organizationSearchTreeOptions);
const option = this.tableOption || this.option; const option = this.tableOption || this.option;
if (!option) return; if (!option) return;
ORG_PROPS.forEach(prop => { 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 = []) { flattenOrgTree(tree = []) {
return (tree || []).flatMap(item => [ return (tree || []).flatMap(item => [
{ id: String(item.id), label: item.label, rawLabel: item.rawLabel }, { id: String(item.id), label: item.label, rawLabel: item.rawLabel },
...this.flattenOrgTree(item.children || []), ...this.flattenOrgTree(item.children || []),
]); ]);
}, },
// 组织列渲染用:空串或纯空白即「未分配组织」(组织列 NOT NULL,无组织存空串)
isBlankOrganizationName(value) {
return String(value ?? '').trim() === '';
},
// 兼容两种入参:级联路径数组(取末级)或单个部门 id // 兼容两种入参:级联路径数组(取末级)或单个部门 id
findOrgById(id) { findOrgById(id) {
const value = Array.isArray(id) ? id.at(-1) : id; const value = Array.isArray(id) ? id.at(-1) : id;
@@ -155,7 +180,7 @@ export default {
// 级联单选:modelValue 必须是 id 路径数组,否则控件内部状态会错乱导致无法再次选中 // 级联单选:modelValue 必须是 id 路径数组,否则控件内部状态会错乱导致无法再次选中
if (scope.row) scope.row[prop] = Array.isArray(value) ? value : []; if (scope.row) scope.row[prop] = Array.isArray(value) ? value : [];
}, },
options: this.organizationTreeOptions, options: this.organizationSearchTreeOptions,
props: { props: {
label: 'label', label: 'label',
value: 'id', value: 'id',
+1 -1
View File
@@ -42,7 +42,7 @@ export const waybillStatusOptions = [
{ label: '草稿', value: 'draft' }, { label: '草稿', value: 'draft' },
{ label: '待执行', value: 'pending' }, { label: '待执行', value: 'pending' },
{ label: '进行中', value: 'running' }, { label: '进行中', value: 'running' },
{ label: '待确认回单', value: 'pending_return' }, { label: '待回单确认', value: 'pending_return_confirm' },
{ label: '已完成', value: 'completed' }, { label: '已完成', value: 'completed' },
{ label: '已取消', value: 'cancelled' }, { label: '已取消', value: 'cancelled' },
]; ];
+6 -18
View File
@@ -471,26 +471,20 @@ export const config = {
endDateRange: ['endDateStart', 'endDateEnd'], endDateRange: ['endDateStart', 'endDateEnd'],
createTimeRange: ['createTimeStart', 'createTimeEnd'], createTimeRange: ['createTimeStart', 'createTimeEnd'],
}, },
actions: ['copy', 'cancel', 'reassign', 'complete', 'batchComplete'], actions: ['copy', 'cancel', 'reassign', 'complete', 'batchComplete', 'confirmReturn'],
detailButton: true, detailButton: true,
statusProp: 'businessStatus', statusProp: 'businessStatus',
statusTextProp: 'businessStatusName', statusTextProp: 'businessStatusName',
formatStatus(row, prop, defaultText) { formatStatus(row, prop, defaultText) {
if (prop !== 'businessStatus') return defaultText; if (prop !== 'businessStatus') return defaultText;
const status = String(row.businessStatus || ''); const status = String(row.businessStatus || '');
const terminalStatuses = [ const terminalStatuses = ['draft', 'completed', 'cancelled', 'waiting_dispatch', 'dispatching'];
'draft',
'completed',
'cancelled',
'waiting_dispatch',
'dispatching',
'pending_return',
];
if (status === 'pending_return') return '待确认回单';
const needAccept = const needAccept =
row.requireAccept === true || row.requireAccept === true ||
row.requireAccept === 1 || row.requireAccept === 1 ||
requiresDriverAcceptConfirmation(row); requiresDriverAcceptConfirmation(row);
// 待回单确认由末位打卡产生,司机必然已接单
if (status === 'pending_return_confirm') return '待回单确认';
// 需司机接单且尚未接单 → 待执行(含库中仍为 running 的历史数据展示校正) // 需司机接单且尚未接单 → 待执行(含库中仍为 running 的历史数据展示校正)
if (needAccept && !hasDriverAcceptRecord(row) && !terminalStatuses.includes(status)) { if (needAccept && !hasDriverAcceptRecord(row) && !terminalStatuses.includes(status)) {
return '待执行'; return '待执行';
@@ -505,20 +499,14 @@ export const config = {
getDriverRejectReason, getDriverRejectReason,
isDriverRejectedWaybill, isDriverRejectedWaybill,
canEdit(row) { canEdit(row) {
// 进行中、待确认回单不允许编辑 // 进行中的运单不允许编辑
if (isInProgressStatus(row, row.businessStatusName)) { if (isInProgressStatus(row, row.businessStatusName)) {
return false; return false;
} }
if (String(row.businessStatus || '') === 'pending_return') {
return false;
}
return !isDriverRejectedWaybill(row); return !isDriverRejectedWaybill(row);
}, },
canCancel(row) { canCancel(row) {
// 进行中、待确认回单不允许取消 // 进行中的运单不允许取消;待回单确认仍可取消(与页面 canCancel 一致)
if (String(row.businessStatus || '') === 'pending_return') {
return false;
}
return !isInProgressStatus(row, row.businessStatusName); return !isInProgressStatus(row, row.businessStatusName);
}, },
deleteStatus: ['draft'], deleteStatus: ['draft'],
+1 -1
View File
@@ -133,7 +133,7 @@ export default {
hide: true, hide: true,
span: 24, span: 24,
order: 55, order: 55,
rules: [{ required: true, message: '请选择所属组织', trigger: 'change' }], // 所属组织可为空:为空表示未分配组织,账号的数据权限为空集
}, },
{ {
label: '所属组织', label: '所属组织',
-23
View File
@@ -315,29 +315,6 @@ export const userOption = safe => {
{ label: '承运商', value: 2 }, { 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: '备注', label: '备注',
prop: 'remark', prop: 'remark',
@@ -48,6 +48,7 @@ export const option = {
searchOrder: 1, searchOrder: 1,
searchslot: true, searchslot: true,
slot: true, slot: true,
overHidden: true,
minWidth: 150, minWidth: 150,
}, },
{ {
@@ -55,6 +56,8 @@ export const option = {
prop: 'useDepartment', prop: 'useDepartment',
search: true, search: true,
searchOrder: 3, searchOrder: 3,
slot: true,
overHidden: true,
minWidth: 140, minWidth: 140,
}, },
{ {
+1 -1
View File
@@ -622,7 +622,7 @@
background: #fff; background: #fff;
} }
// 合同管理新增/编辑弹窗(business-crud-page 封装 → avue-crud,弹窗带 avue-crud__dialog class) // 合同管理新增/编辑弹窗(avue-crud,弹窗带 avue-crud__dialog class)
// 复刻 waybill-manage 方案:接近全屏、压过库内置 .avue-dialog 的 calc(100% - 200px), // 复刻 waybill-manage 方案:接近全屏、压过库内置 .avue-dialog 的 calc(100% - 200px),
// 并解开 body 的 200px 上限、抵消全局 footer 的 -30px 上移 // 并解开 body 的 200px 上限、抵消全局 footer 的 -30px 上移
.contract-manage-dialog.el-dialog { .contract-manage-dialog.el-dialog {
+30 -33
View File
@@ -26,6 +26,35 @@ const uniqueOrgNames = (values = []) => {
return result; 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),无联系人时回退客商所属组织。 * 解析承运商客商档案上联系人的所属组织(branchName),无联系人时回退客商所属组织。
*/ */
@@ -72,13 +101,6 @@ export const resolveCarrierOrganizationNames = async ({ carrierId, carrierName }
return orgNames; 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 = []) => { export const matchOrganizationName = (value, orgNames = []) => {
const text = normalizeOrgName(value); const text = normalizeOrgName(value);
if (!text || !orgNames.length) return false; if (!text || !orgNames.length) return false;
@@ -90,34 +112,9 @@ const filterByOrganizations = (records = [], orgNames = []) => {
return records.filter(item => matchOrganizationName(item.organizationName, 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 ( export const fetchVehiclesByCarrierOrganizations = async (
query = {}, query = {},
+3
View File
@@ -106,6 +106,9 @@ const CERT_SNAPSHOT_GROUPS = {
fields: [ fields: [
{ key: 'realName', label: '真实姓名' }, { key: 'realName', label: '真实姓名' },
{ key: 'idCardNo', label: '身份证号' }, { key: 'idCardNo', label: '身份证号' },
{ key: 'birthday', label: '出生年月', type: TYPE_DATE },
{ key: 'addressRegion', label: '住址' },
{ key: 'address', label: '详细地址' },
{ key: 'idCardFrontUrl', label: '身份证正面照', type: TYPE_IMAGE }, { key: 'idCardFrontUrl', label: '身份证正面照', type: TYPE_IMAGE },
{ key: 'idCardBackUrl', label: '身份证反面照', type: TYPE_IMAGE }, { key: 'idCardBackUrl', label: '身份证反面照', type: TYPE_IMAGE },
{ key: 'idCardValidStart', label: '身份证有效期起', type: TYPE_DATE }, { key: 'idCardValidStart', label: '身份证有效期起', type: TYPE_DATE },
+190 -100
View File
@@ -50,7 +50,7 @@
title="角色权限配置" title="角色权限配置"
append-to-body append-to-body
v-model="box" v-model="box"
width="345px" width="960px"
@close="handleDialogClose" @close="handleDialogClose"
> >
<el-tabs type="border-card"> <el-tabs type="border-card">
@@ -100,41 +100,26 @@
align="middle" align="middle"
style="margin-bottom: 12px; background: #f5f7fa; padding: 6px 10px; border-radius: 4px" style="margin-bottom: 12px; background: #f5f7fa; padding: 6px 10px; border-radius: 4px"
> >
<span style="display: inline-flex; align-items: center"> <span style="color: #606266; font-size: 13px"
<el-switch >勾选多个菜单后点击「设置」,可批量配置数据权限;设置后菜单旁展示当前范围</span
v-model="dataScopeLinked" >
active-text="节点联动" <el-button type="primary" size="small" @click="openDataScopeSetting">设置</el-button>
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>
</el-row> </el-row>
<el-tree <el-tree
:data="dataScopeGrantList" :data="roleDataScopeMenuTree"
show-checkbox show-checkbox
:check-strictly="!dataScopeLinked"
node-key="id" node-key="id"
ref="treeDataScope" ref="treeRoleDataScope"
:default-checked-keys="dataScopeTreeObj"
:props="props" :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-tree>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="接口权限"> <el-tab-pane label="接口权限">
@@ -188,6 +173,45 @@
</span> </span>
</template> </template>
</el-dialog> </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> </basic-container>
</template> </template>
@@ -196,37 +220,44 @@ import {
add, add,
getList, getList,
getRole, getRole,
getRoleAlias,
getRoleTreeById, getRoleTreeById,
grant, grant,
grantTree, grantTree,
remove, remove,
update, update,
} from '@/api/system/role'; } from '@/api/system/role';
import { getRoleMenuDataScopeTree, submitRoleMenuDataScope } from '@/api/system/roleDataScope';
import { getDeptTree } from '@/api/system/dept';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import website from '@/config/website'; import website from '@/config/website';
import { validatenull } from '@/utils/validate';
export default { export default {
data() { data() {
return { return {
form: {}, form: {},
box: false, box: false,
dataScopeSettingBox: false,
props: { props: {
label: 'title', label: 'title',
value: 'key', value: 'key',
}, },
menuGrantList: [], menuGrantList: [],
dataScopeGrantList: [],
apiScopeGrantList: [], apiScopeGrantList: [],
apiGrantList: [], apiGrantList: [],
menuTreeObj: [], menuTreeObj: [],
dataScopeTreeObj: [],
apiScopeTreeObj: [], apiScopeTreeObj: [],
menuLinked: false, menuLinked: false,
dataScopeLinked: false,
apiScopeLinked: false, apiScopeLinked: false,
currentRoleIds: [], currentRoleIds: [],
currentRoleName: '',
roleDataScopeMenuTree: [],
roleDataScopeMap: {},
deptTree: [],
dataScopeSetting: {
roleName: '',
scopeType: 2,
menuIds: [],
},
selectionList: [], selectionList: [],
query: {}, query: {},
loading: true, loading: true,
@@ -309,32 +340,19 @@ export default {
], ],
}, },
{ {
label: '角色别名', label: '角色标识',
labelTip: '用于权限校验与标注的属性,可手动录入也可点击现有别名快速录入', labelTip: '用于权限校验与标注的属性',
prop: 'roleAlias', prop: 'roleAlias',
search: true, search: true,
span: 24, span: 24,
rules: [ rules: [
{ {
required: true, required: true,
message: '请输入角色别名', message: '请输入角色标识',
trigger: 'blur', trigger: 'blur',
}, },
], ],
}, },
{
label: '现有别名',
labelTip: '当前系统已存在的角色别名,点击可快速录入',
prop: 'currentAlias',
type: 'radio',
dicData: [],
props: {
label: 'roleName',
value: 'roleAlias',
},
hide: true,
span: 24,
},
{ {
label: '角色排序', label: '角色排序',
prop: 'sort', prop: 'sort',
@@ -378,14 +396,6 @@ export default {
return ids; return ids;
}, },
}, },
watch: {
'form.currentAlias'() {
const alias = this.form.currentAlias;
if (!validatenull(alias)) {
this.form.roleAlias = alias;
}
},
},
methods: { methods: {
initData(roleId) { initData(roleId) {
getRoleTreeById(roleId).then(res => { getRoleTreeById(roleId).then(res => {
@@ -452,9 +462,9 @@ export default {
}, },
submit() { submit() {
const menuList = this.$refs.treeMenu.getCheckedKeys(); const menuList = this.$refs.treeMenu.getCheckedKeys();
const dataScopeList = this.$refs.treeDataScope.getCheckedKeys();
const apiScopeList = this.$refs.treeApiScope.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.box = false;
this.$message({ this.$message({
type: 'success', type: 'success',
@@ -465,6 +475,84 @@ export default {
this.onLoad(this.page); 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) { rowSave(row, done, loading) {
add(this.withTenantId(row)).then( add(this.withTenantId(row)).then(
() => { () => {
@@ -547,10 +635,6 @@ export default {
this.form.parentId = ''; this.form.parentId = '';
} }
} }
getRoleAlias().then(res => {
const column = this.findColumn(this.option.column, 'currentAlias');
column.dicData = res.data.data;
});
done(); done();
}, },
handleRole() { handleRole() {
@@ -563,58 +647,52 @@ export default {
return; return;
} }
this.menuTreeObj = []; this.menuTreeObj = [];
this.dataScopeTreeObj = [];
this.apiScopeTreeObj = []; this.apiScopeTreeObj = [];
// 保存当前要配置权限的角色ID
this.currentRoleIds = this.idsArray; this.currentRoleIds = this.idsArray;
grantTree().then(res => { this.currentRoleName = (this.selectionList[0] && this.selectionList[0].roleName) || '';
this.menuGrantList = res.data.data.menu; const roleId = this.currentRoleIds[0];
this.dataScopeGrantList = res.data.data.dataScope; Promise.all([grantTree(), getRole(this.ids), this.loadRoleDataScopeTree(roleId)]).then(results => {
this.apiScopeGrantList = res.data.data.apiScope; const grantRes = results[0];
getRole(this.ids).then(res => { const roleRes = results[1];
this.menuTreeObj = res.data.data.menu; this.menuGrantList = grantRes.data.data.menu;
this.dataScopeTreeObj = res.data.data.dataScope; this.apiScopeGrantList = grantRes.data.data.apiScope;
this.apiScopeTreeObj = res.data.data.apiScope; this.menuTreeObj = roleRes.data.data.menu;
this.box = true; this.apiScopeTreeObj = roleRes.data.data.apiScope;
this.$nextTick(() => { this.box = true;
this.applyCheckedKeys('treeMenu', this.menuTreeObj); this.$nextTick(() => {
this.applyCheckedKeys('treeDataScope', this.dataScopeTreeObj); this.applyCheckedKeys('treeMenu', this.menuTreeObj);
this.applyCheckedKeys('treeApiScope', this.apiScopeTreeObj); this.applyCheckedKeys('treeApiScope', this.apiScopeTreeObj);
});
}); });
}); });
}, },
handleRowRole(row) { handleRowRole(row) {
this.menuTreeObj = []; this.menuTreeObj = [];
this.dataScopeTreeObj = [];
this.apiScopeTreeObj = []; this.apiScopeTreeObj = [];
// 保存当前要配置权限的角色ID(单个行操作)
this.currentRoleIds = [row.id]; this.currentRoleIds = [row.id];
grantTree().then(res => { this.currentRoleName = row.roleName || '';
this.menuGrantList = res.data.data.menu; Promise.all([grantTree(), getRole(row.id), this.loadRoleDataScopeTree(row.id)]).then(results => {
this.dataScopeGrantList = res.data.data.dataScope; const grantRes = results[0];
this.apiScopeGrantList = res.data.data.apiScope; const roleRes = results[1];
getRole(row.id).then(res => { this.menuGrantList = grantRes.data.data.menu;
this.menuTreeObj = res.data.data.menu; this.apiScopeGrantList = grantRes.data.data.apiScope;
this.dataScopeTreeObj = res.data.data.dataScope; this.menuTreeObj = roleRes.data.data.menu;
this.apiScopeTreeObj = res.data.data.apiScope; this.apiScopeTreeObj = roleRes.data.data.apiScope;
this.box = true; this.box = true;
this.$nextTick(() => { this.$nextTick(() => {
this.applyCheckedKeys('treeMenu', this.menuTreeObj); this.applyCheckedKeys('treeMenu', this.menuTreeObj);
this.applyCheckedKeys('treeDataScope', this.dataScopeTreeObj); this.applyCheckedKeys('treeApiScope', this.apiScopeTreeObj);
this.applyCheckedKeys('treeApiScope', this.apiScopeTreeObj);
});
}); });
}); });
}, },
handleDialogClose() { handleDialogClose() {
// 对话框关闭时清空选择状态和权限树数据
this.menuTreeObj = []; this.menuTreeObj = [];
this.dataScopeTreeObj = [];
this.apiScopeTreeObj = []; this.apiScopeTreeObj = [];
this.menuLinked = false; this.menuLinked = false;
this.dataScopeLinked = false;
this.apiScopeLinked = false; this.apiScopeLinked = false;
this.roleDataScopeMenuTree = [];
this.roleDataScopeMap = {};
this.currentRoleName = '';
this.dataScopeSettingBox = false;
this.selectionClear(); this.selectionClear();
}, },
handleDelete() { handleDelete() {
@@ -659,3 +737,15 @@ export default {
}, },
}; };
</script> </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' }, query: { id: String(customerId), name: '查看客商档案', view: '1' },
}); });
}, },
waybillStatusName(value) { return ({ pending: '待执行', waiting: '待执行', running: '进行中', processing: '进行中', completed: '已完成', cancelled: '已取消' })[value] || value || '-'; }, 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', completed: 'success', cancelled: 'danger' })[value] || 'info'; }, 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 || '-'; }, statusName(value) { return ({ waiting_dispatch: '待调度', dispatching: '调度中', completed: '调度完成', closed: '调度关闭' })[value] || value || '-'; },
statusType(value) { return ({ waiting_dispatch: 'warning', dispatching: 'success', completed: 'primary', closed: 'danger' })[value] || 'info'; }, statusType(value) { return ({ waiting_dispatch: 'warning', dispatching: 'success', completed: 'primary', closed: 'danger' })[value] || 'info'; },
}, },
@@ -210,7 +210,11 @@
@mousedown="() => handleDriverFocus(route)" @mousedown="() => handleDriverFocus(route)"
@focus="() => handleDriverFocus(route)" @focus="() => handleDriverFocus(route)"
@select="handleDriverSuggestionSelect" @select="handleDriverSuggestionSelect"
/></el-form-item >
<template #default="{ item }">
<span>{{ driverOptionText(item) }}</span>
</template>
</el-autocomplete></el-form-item
></el-col> ></el-col>
<el-col :span="6" <el-col :span="6"
><el-form-item ><el-form-item
@@ -239,7 +243,11 @@
@mousedown="() => handleDriverFocus(route)" @mousedown="() => handleDriverFocus(route)"
@focus="() => handleDriverFocus(route)" @focus="() => handleDriverFocus(route)"
@select="handleEscortSuggestionSelect" @select="handleEscortSuggestionSelect"
/></el-form-item >
<template #default="{ item }">
<span>{{ driverOptionText(item) }}</span>
</template>
</el-autocomplete></el-form-item
></el-col> ></el-col>
<el-col v-if="route.carrierType !== '承运商'" :span="6" <el-col v-if="route.carrierType !== '承运商'" :span="6"
><el-form-item label="押运人手机号" ><el-form-item label="押运人手机号"
@@ -316,7 +324,7 @@
<script> <script>
import * as api from '@/api/business/master-order'; 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 { formatContractOptionLabel } from '@/utils/contract';
import { import {
currencyUnit, currencyUnit,
@@ -403,6 +411,7 @@ export default {
this.applySelfOperatedCarrierDefaults(); this.applySelfOperatedCarrierDefaults();
}, },
methods: { methods: {
driverOptionText,
loadQuantityUnitOptions() { loadQuantityUnitOptions() {
if (this.quantityUnitLoading) return this.quantityUnitLoadPromise; if (this.quantityUnitLoading) return this.quantityUnitLoadPromise;
this.quantityUnitLoading = true; this.quantityUnitLoading = true;
@@ -944,12 +953,6 @@ export default {
route.escortPhone = ''; route.escortPhone = '';
this.driverOptions = []; this.driverOptions = [];
}, },
getRouteCarrierFilter(route = {}) {
return {
carrierId: route.carrierId || '',
carrierName: route.carrierName || '',
};
},
carrierOptionValue(item = {}) { carrierOptionValue(item = {}) {
return item.contractId || item.id || item.value || item.carrierName || ''; return item.contractId || item.id || item.value || item.carrierName || '';
}, },
@@ -1026,36 +1029,22 @@ export default {
this.activeSuggestRoute = route; this.activeSuggestRoute = route;
}, },
async loadDriverOptions(route = {}, keyword = '') { async loadDriverOptions(route = {}, keyword = '') {
const carrier = this.getRouteCarrierFilter(route);
if (!carrier.carrierId && !carrier.carrierName) {
this.driverOptions = [];
return [];
}
this.driverLoading = true; this.driverLoading = true;
try { try {
this.driverOptions = await fetchDriversByCarrierOrganizations( this.driverOptions = await fetchDriverCandidates({
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' }, size: 20,
carrier ...(keyword ? { driverName: keyword } : {}),
); posts: '司机',
});
return this.driverOptions; return this.driverOptions;
} finally { } finally {
this.driverLoading = false; this.driverLoading = false;
} }
}, },
fetchDriverSuggestions(queryString, callback) { fetchDriverSuggestions(queryString, callback) {
const route = this.activeSuggestRoute || {};
const carrier = this.getRouteCarrierFilter(route);
const keyword = String(queryString || '').trim(); const keyword = String(queryString || '').trim();
if (!carrier.carrierId && !carrier.carrierName) {
callback([]);
if (!keyword) this.$message.warning('请先选择承运商');
return;
}
this.driverLoading = true; this.driverLoading = true;
fetchDriversByCarrierOrganizations( fetchDriverCandidates({ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' })
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' },
carrier
)
.then(records => { .then(records => {
this.driverOptions = records; this.driverOptions = records;
callback( callback(
@@ -1080,19 +1069,13 @@ export default {
if (drivingVehicle) route.vehicleNo = drivingVehicle; if (drivingVehicle) route.vehicleNo = drivingVehicle;
}, },
fetchEscortSuggestions(queryString, callback) { fetchEscortSuggestions(queryString, callback) {
const route = this.activeSuggestRoute || {};
const keyword = String(queryString || '').trim(); 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; this.escortLoading = true;
fetchDriversByCarrierOrganizations( fetchDriverCandidates({
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '押运员' }, size: 20,
carrier ...(keyword ? { driverName: keyword } : {}),
) posts: '押运员',
})
.then(records => { .then(records => {
callback( callback(
records.map(item => ({ records.map(item => ({
@@ -10,7 +10,7 @@
:class="{ 'punch-record-photos__grid--edit': editable }" :class="{ 'punch-record-photos__grid--edit': editable }"
> >
<div <div
v-for="(item, groupIndex) in group.items" v-for="item in group.items"
:key="`${item.photo.url}-${item.index}`" :key="`${item.photo.url}-${item.index}`"
class="punch-record-photos__item" class="punch-record-photos__item"
@click="handlePreview(item.index)" @click="handlePreview(item.index)"
@@ -23,8 +23,6 @@
:lazy="lazy" :lazy="lazy"
fit="cover" fit="cover"
/> />
<!-- 主图 = 该类型的第一张,按上传顺序取;没有独立字段,也不可切换 -->
<div v-if="groupIndex === 0" class="punch-record-photos__badge">主图</div>
<el-link <el-link
v-if="editable" v-if="editable"
class="punch-record-photos__remove" class="punch-record-photos__remove"
@@ -59,7 +57,7 @@
import { computed } from 'vue'; import { computed } from 'vue';
// 打卡照片是每个打卡节点必有的一个照片类型,不由过程配置的凭证类型决定; // 打卡照片是每个打卡节点必有的一个照片类型,不由过程配置的凭证类型决定;
// 它与各凭证类型各自最多 9 张,同一类型下第一张即界面上的「主图」。 // 它与各凭证类型各自最多 9 张。
const PUNCH_PHOTO_TYPE = '打卡照片'; const PUNCH_PHOTO_TYPE = '打卡照片';
// 照片没带类型时的兜底名称,与后端落库时的兜底口径一致 // 照片没带类型时的兜底名称,与后端落库时的兜底口径一致
const PHOTO_TYPE_FALLBACK = '凭证'; 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 { &__remove {
align-self: center; align-self: center;
font-size: 12px; font-size: 12px;
@@ -1870,7 +1870,11 @@
:fetch-suggestions="fetchTaskDriverSuggestions" :fetch-suggestions="fetchTaskDriverSuggestions"
:loading="taskDriverLoading" :loading="taskDriverLoading"
@select="item => handleTaskDriverSelect('dispatch', item)" @select="item => handleTaskDriverSelect('dispatch', item)"
/> >
<template #default="{ item }">
<span>{{ driverOptionText(item) }}</span>
</template>
</el-autocomplete>
</el-form-item> </el-form-item>
<el-form-item <el-form-item
:label="dispatchItemForm.carrierType === '承运商' ? '手机号' : '司机手机号'" :label="dispatchItemForm.carrierType === '承运商' ? '手机号' : '司机手机号'"
@@ -1893,7 +1897,11 @@
:fetch-suggestions="fetchTaskEscortSuggestions" :fetch-suggestions="fetchTaskEscortSuggestions"
:loading="taskEscortLoading" :loading="taskEscortLoading"
@select="handleTaskEscortSelect" @select="handleTaskEscortSelect"
/> >
<template #default="{ item }">
<span>{{ driverOptionText(item) }}</span>
</template>
</el-autocomplete>
</el-form-item> </el-form-item>
<el-form-item v-if="dispatchItemForm.carrierType !== '承运商'" label="押运人手机号"> <el-form-item v-if="dispatchItemForm.carrierType !== '承运商'" label="押运人手机号">
<el-input v-model="dispatchItemForm.escortPhone" placeholder="请输入" /> <el-input v-model="dispatchItemForm.escortPhone" placeholder="请输入" />
@@ -2760,7 +2768,7 @@ import {
getList as getProjectList, getList as getProjectList,
} from '@/api/business/project-apply'; } from '@/api/business/project-apply';
import { getList as getCommonRouteList } from '@/api/business/common-route'; 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 } from '@/api/system/dictbiz';
import { getDictionary as getSystemDictionary } from '@/api/system/dict'; import { getDictionary as getSystemDictionary } from '@/api/system/dict';
import { addressTypeOptions } from '@/option/base/common-address'; import { addressTypeOptions } from '@/option/base/common-address';
@@ -3809,6 +3817,7 @@ export default {
}, },
methods: { methods: {
formatContractOptionLabel, formatContractOptionLabel,
driverOptionText,
initStandaloneFormPage() { initStandaloneFormPage() {
if (!this.isStandaloneFormPage) return; if (!this.isStandaloneFormPage) return;
const mode = this.$route.query.mode === 'edit' ? 'edit' : 'add'; const mode = this.$route.query.mode === 'edit' ? 'edit' : 'add';
@@ -4178,6 +4187,7 @@ export default {
waiting: ['待执行', 'is-pending'], waiting: ['待执行', 'is-pending'],
processing: ['进行中', 'is-processing'], processing: ['进行中', 'is-processing'],
running: ['进行中', 'is-processing'], running: ['进行中', 'is-processing'],
pending_return_confirm: ['待回单确认', 'is-pending'],
completed: ['已完成', 'is-completed'], completed: ['已完成', 'is-completed'],
cancelled: ['已取消', 'is-cancelled'], cancelled: ['已取消', 'is-cancelled'],
}; };
@@ -4988,16 +4998,8 @@ export default {
}, },
fetchTaskDriverSuggestions(queryString, callback) { fetchTaskDriverSuggestions(queryString, callback) {
const keyword = String(queryString || '').trim(); const keyword = String(queryString || '').trim();
const carrier = this.getDispatchCarrierFilter();
if (!carrier.carrierId && !carrier.carrierName) {
callback([]);
return;
}
this.taskDriverLoading = true; this.taskDriverLoading = true;
fetchDriversByCarrierOrganizations( fetchDriverCandidates({ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' })
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' },
carrier
)
.then(records => { .then(records => {
this.taskDriverOptions = records; this.taskDriverOptions = records;
callback( callback(
@@ -5017,16 +5019,12 @@ export default {
}, },
fetchTaskEscortSuggestions(queryString, callback) { fetchTaskEscortSuggestions(queryString, callback) {
const keyword = String(queryString || '').trim(); const keyword = String(queryString || '').trim();
const carrier = this.getDispatchCarrierFilter();
if (!carrier.carrierId && !carrier.carrierName) {
callback([]);
return;
}
this.taskEscortLoading = true; this.taskEscortLoading = true;
fetchDriversByCarrierOrganizations( fetchDriverCandidates({
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '押运员' }, size: 20,
carrier ...(keyword ? { driverName: keyword } : {}),
) posts: '押运员',
})
.then(records => { .then(records => {
this.taskEscortOptions = records; this.taskEscortOptions = records;
callback(records.map(item => ({ ...item, value: item.driverName || item.name || '' }))); callback(records.map(item => ({ ...item, value: item.driverName || item.name || '' })));
@@ -8044,12 +8042,6 @@ export default {
this.taskDriverOptions = []; this.taskDriverOptions = [];
this.taskEscortOptions = []; this.taskEscortOptions = [];
}, },
getDispatchCarrierFilter() {
return {
carrierId: this.dispatchItemForm.carrierId || '',
carrierName: this.dispatchItemForm.carrierName || '',
};
},
handleDispatchCargoQuantityInput(row, value) { handleDispatchCargoQuantityInput(row, value) {
const text = String(value || '').replace(/[^\d.]/g, ''); const text = String(value || '').replace(/[^\d.]/g, '');
const parts = text.split('.'); const parts = text.split('.');
@@ -1381,11 +1381,23 @@ const formatOptionalNumber = value => {
const normalized = normalizeOptionalImportNumber(value); const normalized = normalizeOptionalImportNumber(value);
return normalized === null || normalized === undefined ? '' : normalized; 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 normalizeDetailRow = row => {
const next = { ...(row || {}) }; const next = { ...(row || {}) };
OPTIONAL_NUMBER_PROPS.forEach(prop => { OPTIONAL_NUMBER_PROPS.forEach(prop => {
next[prop] = formatOptionalNumber(next[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; return next;
}; };
const normalizeImportRow = row => ({ const normalizeImportRow = row => ({
@@ -1200,7 +1200,11 @@
:loading="taskDriverLoading" :loading="taskDriverLoading"
:disabled="dialogReadonly" :disabled="dialogReadonly"
@select="handleTaskDriverSelect" @select="handleTaskDriverSelect"
/> >
<template #default="{ item }">
<span>{{ driverOptionText(item) }}</span>
</template>
</el-autocomplete>
</el-form-item> </el-form-item>
<el-form-item <el-form-item
v-if="isRoadTransport" v-if="isRoadTransport"
@@ -1279,7 +1283,11 @@
:loading="taskEscortLoading" :loading="taskEscortLoading"
:disabled="dialogReadonly" :disabled="dialogReadonly"
@select="handleTaskEscortSelect" @select="handleTaskEscortSelect"
/> >
<template #default="{ item }">
<span>{{ driverOptionText(item) }}</span>
</template>
</el-autocomplete>
</el-form-item> </el-form-item>
<el-form-item v-if="showRoadEscortFields" label="押运人手机号"> <el-form-item v-if="showRoadEscortFields" label="押运人手机号">
<el-input <el-input
@@ -1591,8 +1599,15 @@
<el-link type="primary" v-if="canCancel(row)" @click="handleAction('cancel', row)"> <el-link type="primary" v-if="canCancel(row)" @click="handleAction('cancel', row)">
取消 取消
</el-link> </el-link>
<el-link type="primary" v-if="canComplete(row)" @click="handleCompleteAction(row)"> <el-link type="primary" v-if="canComplete(row)" @click="handleAction('complete', row)">
{{ completeActionLabelFor(row) }} {{ completeActionLabel }}
</el-link>
<el-link
type="primary"
v-if="canConfirmReturn(row)"
@click="handleAction('confirmReturn', row)"
>
确认回单
</el-link> </el-link>
<el-link type="primary" v-if="canMaintainMileage(row)" @click="openMileageDialog(row)"> <el-link type="primary" v-if="canMaintainMileage(row)" @click="openMileageDialog(row)">
维护里程 维护里程
@@ -1653,7 +1668,11 @@
style="width: 100%" style="width: 100%"
@select="handleReassignDriverSelect" @select="handleReassignDriverSelect"
@change="handleReassignDriverChange" @change="handleReassignDriverChange"
/> >
<template #default="{ item }">
<span>{{ driverOptionText(item) }}</span>
</template>
</el-autocomplete>
</el-form-item> </el-form-item>
<el-form-item <el-form-item
label="手机号" label="手机号"
@@ -2252,6 +2271,13 @@
<span v-if="record.volume">体积 {{ record.volume }}方</span> <span v-if="record.volume">体积 {{ record.volume }}方</span>
<span v-if="record.quantity">数量 {{ record.quantity }}</span> <span v-if="record.quantity">数量 {{ record.quantity }}</span>
</div> </div>
<!-- 里程只挂在末位节点上(needMileage 由后端下发),显示的是运单里程 -->
<div
v-if="record.needMileage && Number(record.mileage) > 0"
class="waybill-manage-page__punch-record-line"
>
里程 {{ record.mileage }} 公里
</div>
<div <div
v-if="record.remark" v-if="record.remark"
class="waybill-manage-page__punch-record-line" class="waybill-manage-page__punch-record-line"
@@ -2848,6 +2874,20 @@
/> />
</el-form-item> </el-form-item>
</el-col> </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-col :span="24">
<el-form-item label="备注"> <el-form-item label="备注">
<el-input <el-input
@@ -3683,6 +3723,7 @@ import {
getVoucherImages as getProcessConfigVoucherImages, getVoucherImages as getProcessConfigVoucherImages,
} from '@/api/business/process-config'; } from '@/api/business/process-config';
import { import {
getList as getWaybillList,
getPunchRecords as getWaybillPunchRecords, getPunchRecords as getWaybillPunchRecords,
locateVehicle, locateVehicle,
savePunchRecord, savePunchRecord,
@@ -3692,9 +3733,7 @@ import VoucherExecutionDetail from './voucher-execution-detail.vue';
import PunchRecordPhotos from './punch-record-photos.vue'; import PunchRecordPhotos from './punch-record-photos.vue';
import AddressMapPicker from '@/components/address-map-picker/main.vue'; import AddressMapPicker from '@/components/address-map-picker/main.vue';
import { getList as getDriverList } from '@/api/transportCapacity/driver'; import { getList as getDriverList } from '@/api/transportCapacity/driver';
import { import { fetchDriverCandidates, driverOptionText } from '@/utils/carrier-org-resource';
fetchDriversByCarrierOrganizations,
} from '@/utils/carrier-org-resource';
import { getDictionary } from '@/api/system/dictbiz'; import { getDictionary } from '@/api/system/dictbiz';
import { getDictionary as getSystemDictionary } from '@/api/system/dict'; import { getDictionary as getSystemDictionary } from '@/api/system/dict';
import { addressTypeOptions } from '@/option/base/common-address'; import { addressTypeOptions } from '@/option/base/common-address';
@@ -4011,6 +4050,7 @@ export default {
weight: '', weight: '',
volume: '', volume: '',
quantity: '', quantity: '',
mileage: '',
remark: '', remark: '',
photos: [], photos: [],
}, },
@@ -4370,9 +4410,15 @@ export default {
addBtn: this.canCreate, addBtn: this.canCreate,
}; };
}, },
// 打卡记录编辑入口与运单编辑同权限:能改运单的人才能订正它的打卡流水 // 打卡记录编辑入口与运单编辑同权限:能改运单的人才能订正它的打卡流水。
// 时间轴的「编辑打卡 / 补卡」链接是本页唯一写入入口,提交固定 punched=true。
// 取消打卡不在本页;待回单确认的运单不显示该链接。
canEditWaybillPunchRecord() { 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() { waybillPunchEditNode() {
return this.findWaybillPunchEditNodeConfig(this.waybillPunchEditRecord); return this.findWaybillPunchEditNodeConfig(this.waybillPunchEditRecord);
@@ -4407,6 +4453,15 @@ export default {
waybillPunchEditPunchDisabled() { waybillPunchEditPunchDisabled() {
return this.waybillPunchEditNode?.punch === false; 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() { waybillPunchEditRules() {
return { punchTime: [{ required: true, message: '请选择打卡时间', trigger: 'change' }] }; return { punchTime: [{ required: true, message: '请选择打卡时间', trigger: 'change' }] };
}, },
@@ -4800,6 +4855,7 @@ export default {
}, },
methods: { methods: {
formatContractOptionLabel, formatContractOptionLabel,
driverOptionText,
parseRouteChangeContent, parseRouteChangeContent,
// 页面被 keep-alive 缓存/卸载时调用,关闭本页所有 append-to-body 的弹窗, // 页面被 keep-alive 缓存/卸载时调用,关闭本页所有 append-to-body 的弹窗,
// 避免 Teleport 出去的 DOM 残留在 body 上盖住后续页面(详见 data 里的说明)。 // 避免 Teleport 出去的 DOM 残留在 body 上盖住后续页面(详见 data 里的说明)。
@@ -5099,7 +5155,8 @@ export default {
return false; return false;
} }
const status = this.statusValue(row); const status = this.statusValue(row);
return status === 'pending'; // 待回单确认可取消,取消不生成应收应付
return status === 'pending' || status === 'pending_return_confirm';
}, },
canComplete(row) { canComplete(row) {
if ( if (
@@ -5111,29 +5168,17 @@ export default {
return false; return false;
} }
const status = this.statusValue(row); const status = this.statusValue(row);
return status === 'processing' || status === 'running' || status === 'pending_return'; // 待回单确认也可直接完成,不经确认回单
return status === 'processing' || status === 'running' || status === 'pending_return_confirm';
}, },
completeActionLabelFor(row) { canConfirmReturn(row) {
return this.statusValue(row) === 'pending_return' ? '确认回单' : this.completeActionLabel; return (
}, this.hasAction('confirmReturn') &&
handleCompleteAction(row) { this.hasPermission(`${this.config.permission}_complete`) &&
const isPendingReturn = this.statusValue(row) === 'pending_return'; !this.readonlyScope &&
const actionName = this.completeActionLabelFor(row); !row.readonly &&
this.$confirm(`确定${actionName}该${this.config.title}?`, '提示', { this.statusValue(row) === 'pending_return_confirm'
confirmButtonText: '确定', );
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
if (isPendingReturn && typeof this.api.confirmReturn === 'function') {
return this.api.confirmReturn(row.id);
}
return this.api.complete(row.id);
})
.then(() => {
this.$message.success(`${actionName}成功`);
this.onLoad(this.page, this.query);
});
}, },
canMaintainMileage(row) { canMaintainMileage(row) {
return ( return (
@@ -5948,6 +5993,8 @@ export default {
weight: record.weight || '', weight: record.weight || '',
volume: record.volume || '', volume: record.volume || '',
quantity: record.quantity || '', quantity: record.quantity || '',
// 里程只有正整数口径,后端若仍下发 "200.00" 会过不了提交时的正整数校验
mileage: Number(record.mileage) > 0 ? String(Number(record.mileage)) : '',
remark: record.remark || '', remark: record.remark || '',
photos: (record.photos || []) photos: (record.photos || [])
.filter(photo => photo?.url) .filter(photo => photo?.url)
@@ -6023,10 +6070,22 @@ export default {
photos.splice(index, 1); photos.splice(index, 1);
this.waybillPunchEditForm.photos = photos; this.waybillPunchEditForm.photos = photos;
}, },
// 里程只收整数:非数字直接丢掉,长度与「维护里程」一致(≤10 位)
handleWaybillPunchMileageInput(value) {
this.waybillPunchEditForm.mileage = String(value || '')
.replace(/\D/g, '')
.slice(0, 10);
},
async submitWaybillPunchRecordEdit() { async submitWaybillPunchRecordEdit() {
await this.$refs.waybillPunchRecordEditFormRef.validate(); await this.$refs.waybillPunchRecordEditFormRef.validate();
const record = this.waybillPunchEditRecord; const record = this.waybillPunchEditRecord;
const form = this.waybillPunchEditForm; 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 = const submit =
typeof this.api?.savePunchRecord === 'function' ? this.api.savePunchRecord : savePunchRecord; typeof this.api?.savePunchRecord === 'function' ? this.api.savePunchRecord : savePunchRecord;
this.waybillPunchRecordEditDialog.submitting = true; this.waybillPunchRecordEditDialog.submitting = true;
@@ -6045,6 +6104,8 @@ export default {
weight: form.weight, weight: form.weight,
volume: form.volume, volume: form.volume,
quantity: form.quantity, quantity: form.quantity,
// 不传代表不改;后端按 null 处理
mileage: mileageText ? Number(mileageText) : undefined,
remark: form.remark, remark: form.remark,
photos: form.photos.map(photo => ({ url: photo.url, type: photo.type })), photos: form.photos.map(photo => ({ url: photo.url, type: photo.type })),
}); });
@@ -6565,7 +6626,7 @@ export default {
if (status === 'completed') { if (status === 'completed') {
return 'primary'; return 'primary';
} }
if (status === 'pending_return') { if (status === 'pending_return_confirm') {
return 'warning'; return 'warning';
} }
return 'warning'; return 'warning';
@@ -7967,19 +8028,10 @@ export default {
this.taskDriverOptions = []; this.taskDriverOptions = [];
this.taskEscortOptions = []; this.taskEscortOptions = [];
}, },
getTaskCarrierFilter() {
return {
carrierId: this.form.carrierId || '',
carrierName: this.form.carrierName || '',
};
},
loadTaskDriverOptions() { loadTaskDriverOptions() {
if (!this.taskInfoFormEnabled || this.taskDriverLoading) return Promise.resolve([]); if (!this.taskInfoFormEnabled || this.taskDriverLoading) return Promise.resolve([]);
this.taskDriverLoading = true; this.taskDriverLoading = true;
return fetchDriversByCarrierOrganizations( return fetchDriverCandidates({ size: 9999, posts: '司机' })
{ size: 9999, posts: '司机' },
this.getTaskCarrierFilter()
)
.then(records => { .then(records => {
this.taskDriverOptions = records; this.taskDriverOptions = records;
return this.taskDriverOptions; return this.taskDriverOptions;
@@ -7990,16 +8042,8 @@ export default {
}, },
fetchTaskDriverSuggestions(queryString, callback) { fetchTaskDriverSuggestions(queryString, callback) {
const keyword = String(queryString || '').trim(); const keyword = String(queryString || '').trim();
const carrier = this.getTaskCarrierFilter();
if (!carrier.carrierId && !carrier.carrierName) {
callback([]);
return;
}
this.taskDriverLoading = true; this.taskDriverLoading = true;
fetchDriversByCarrierOrganizations( fetchDriverCandidates({ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' })
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' },
carrier
)
.then(records => { .then(records => {
this.taskDriverOptions = records; this.taskDriverOptions = records;
callback( callback(
@@ -8019,16 +8063,12 @@ export default {
}, },
fetchTaskEscortSuggestions(queryString, callback) { fetchTaskEscortSuggestions(queryString, callback) {
const keyword = String(queryString || '').trim(); const keyword = String(queryString || '').trim();
const carrier = this.getTaskCarrierFilter();
if (!carrier.carrierId && !carrier.carrierName) {
callback([]);
return;
}
this.taskEscortLoading = true; this.taskEscortLoading = true;
fetchDriversByCarrierOrganizations( fetchDriverCandidates({
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '押运员' }, size: 20,
carrier ...(keyword ? { driverName: keyword } : {}),
) posts: '押运员',
})
.then(records => { .then(records => {
this.taskEscortOptions = records; this.taskEscortOptions = records;
callback(records.map(item => ({ ...item, value: item.driverName || item.name || '' }))); callback(records.map(item => ({ ...item, value: item.driverName || item.name || '' })));
@@ -10175,8 +10215,14 @@ export default {
cancel: '取消', cancel: '取消',
complete: this.completeActionLabel, complete: this.completeActionLabel,
reassign: '重新派单', reassign: '重新派单',
confirmReturn: '确认回单',
}[action]; }[action];
this.$confirm(`确定${actionName}该${this.config.title}?`, '提示', { // 确认回单是终态动作(本轮不做撤销),把后果写在提示里,别让人以为还能退回
const confirmText =
action === 'confirmReturn'
? '确认回单后该运单将置为已完成,且不可撤销。是否继续?'
: `确定${actionName}该${this.config.title}?`;
this.$confirm(confirmText, '提示', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning', type: 'warning',
@@ -10229,20 +10275,8 @@ export default {
}, },
fetchReassignDriverSuggestions(queryString, callback) { fetchReassignDriverSuggestions(queryString, callback) {
const keyword = String(queryString || '').trim(); 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; this.reassignDrawer.driverLoading = true;
fetchDriversByCarrierOrganizations( fetchDriverCandidates({ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' })
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' },
carrier
)
.then(records => { .then(records => {
this.reassignDriverOptions = records; this.reassignDriverOptions = records;
callback( callback(
@@ -10382,11 +10416,11 @@ export default {
return; return;
} }
const pendingReturnCount = this.selectionList.filter( const pendingReturnCount = this.selectionList.filter(
row => this.statusValue(row) === 'pending_return' row => this.statusValue(row) === 'pending_return_confirm'
).length; ).length;
const tip = const tip =
pendingReturnCount > 0 pendingReturnCount > 0
? `确定完成选中的运单?其中 ${pendingReturnCount} 条为待确认回单,确认后将转为已完成。` ? `确定完成选中的运单?其中 ${pendingReturnCount} 条为待回单确认,确认后将转为已完成。`
: '确定完成选中的运单?'; : '确定完成选中的运单?';
this.$confirm(tip, '提示', { this.$confirm(tip, '提示', {
confirmButtonText: '确定', confirmButtonText: '确定',
@@ -10402,25 +10436,38 @@ export default {
this.onLoad(this.page, this.query); this.onLoad(this.page, this.query);
}); });
}, },
handleRoadLoading() { async handleRoadLoading() {
if (!this.selectionList.length) { if (!this.selectionList.length) {
this.$message.warning('请选择至少一条数据'); this.$message.warning('请选择至少一条数据');
return; return;
} }
const nonRoadWaybills = this.selectionList.filter(row => !this.waybillIsRoadTransport(row)); const selectedIds = this.selectionList.map(row => row.id).filter(Boolean);
if (nonRoadWaybills.length) { if (!selectedIds.length) {
this.$message.warning('公路配载仅支持运输方式为“公路运输”的运单'); this.$message.warning('请选择至少一条数据');
return; return;
} }
const unavailableWaybills = this.selectionList.filter( // 可配载与否只看后端查询结果
row => let loadableIds;
!['pending', 'processing', 'running'].includes(this.statusValue(row)) || try {
row.loadingId || const res = await getWaybillList(1, selectedIds.length, {
row.loadingManageId || loadableOnly: 1,
row.loadingNo ids: selectedIds.join(','),
); });
if (unavailableWaybills.length) { loadableIds = new Set(extractRecords(res).map(row => String(row.id)));
this.$message.warning('公路配载仅支持待执行/进行中且未关联有效配载单的运单'); } 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; return;
} }
try { try {
@@ -10443,6 +10490,13 @@ export default {
}, },
}); });
}, },
// 只给后端判定为不可配载的运单生成可读原因,本身不参与准入判定
roadLoadingRejectReasonText(row = {}) {
if (!this.waybillIsRoadTransport(row)) return '非公路运输';
if (String(row.businessStatus || '') !== 'pending') return '状态不是待执行';
if (row.loadingNo) return '已关联配载单';
return '运单数据可能已变化,请刷新后重试';
},
}, },
}; };
</script> </script>
+36 -2
View File
@@ -27,8 +27,14 @@
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="合同编号"> <el-form-item label="合同编号" prop="contractNo">
<el-input v-model="form.contractNo" disabled /> <el-input
v-model="form.contractNo"
maxlength="100"
clearable
placeholder="请输入合同编号"
:disabled="isSettlementRuleChange"
/>
</el-form-item> </el-form-item>
<el-form-item label="合同名称" prop="contractName"> <el-form-item label="合同名称" prop="contractName">
<el-input <el-input
@@ -667,6 +673,24 @@ export default {
'按数量', '按数量',
], ],
rules: { 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' }], contractName: [{ required: true, message: '请输入合同名称', trigger: 'blur' }],
contractAmount: [ contractAmount: [
{ required: true, message: '请输入合同金额', trigger: 'blur' }, { required: true, message: '请输入合同金额', trigger: 'blur' },
@@ -972,6 +996,16 @@ export default {
} }
await this.$refs.formRef.validate(); await this.$refs.formRef.validate();
if (!this.isSettlementRuleChange) { 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); const total = this.paymentRatioRows.reduce((sum, row) => sum + Number(row.ratioLimit || 0), 0);
if (this.paymentRatioRows.length && Math.abs(total - 100) > 0.0001) { if (this.paymentRatioRows.length && Math.abs(total - 100) > 0.0001) {
this.$message.warning('付款比例上限合计必须等于100%'); this.$message.warning('付款比例上限合计必须等于100%');
+106 -69
View File
@@ -901,7 +901,7 @@
<div class="loading-manage-dialog__admission-tip"> <div class="loading-manage-dialog__admission-tip">
<div class="loading-manage-dialog__admission-title">可配载条件</div> <div class="loading-manage-dialog__admission-title">可配载条件</div>
<div class="loading-manage-dialog__admission-text"> <div class="loading-manage-dialog__admission-text">
同时满足以下条件的运单可被配载:运输方式为「公路」;运单状态为「待执行」或「进行中」;未关联其他有效配载单。 同时满足以下条件的运单可被配载:运输方式为「公路」;运单状态为「待执行」或「进行中」;未关联其他有效配载单;同一配载单的子运单须属同一项目,且承运商合同乙方须存在共同可选值(按各子单客户合同甲方匹配后取交集)。
</div> </div>
<div class="loading-manage-dialog__admission-title">配载后状态说明</div> <div class="loading-manage-dialog__admission-title">配载后状态说明</div>
<div class="loading-manage-dialog__admission-text"> <div class="loading-manage-dialog__admission-text">
@@ -1079,31 +1079,12 @@
<el-option <el-option
v-for="item in driverOptions" v-for="item in driverOptions"
:key="item.id || item.driverName || item.name" :key="item.id || item.driverName || item.name"
:label="item.driverName || item.name" :label="driverOptionText(item)"
:value="item.driverName || item.name" :value="item.driverName || item.name"
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> </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> </template>
<el-col <el-col
:span="candidateSearchVisible ? 24 : 6" :span="candidateSearchVisible ? 24 : 6"
@@ -1212,6 +1193,16 @@
min-width="160" min-width="160"
show-overflow-tooltip 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 <el-table-column
prop="planName" prop="planName"
label="计划名称" label="计划名称"
@@ -1511,7 +1502,11 @@
placeholder="请输入" placeholder="请输入"
:disabled="dialogReadonly || isRunningSelfDriverLocked" :disabled="dialogReadonly || isRunningSelfDriverLocked"
@select="item => handleDriverSuggestionSelect('dialog', item)" @select="item => handleDriverSuggestionSelect('dialog', item)"
/> >
<template #default="{ item }">
<span>{{ driverOptionText(item) }}</span>
</template>
</el-autocomplete>
</el-form-item> </el-form-item>
<el-form-item <el-form-item
label="手机号" label="手机号"
@@ -1550,7 +1545,11 @@
placeholder="请输入" placeholder="请输入"
:disabled="dialogReadonly" :disabled="dialogReadonly"
@select="item => handleEscortSuggestionSelect('dialog', item)" @select="item => handleEscortSuggestionSelect('dialog', item)"
/> >
<template #default="{ item }">
<span>{{ driverOptionText(item) }}</span>
</template>
</el-autocomplete>
</el-form-item> </el-form-item>
<el-form-item v-if="dialogForm.carrierType !== '承运商'" label="押运人手机号"> <el-form-item v-if="dialogForm.carrierType !== '承运商'" label="押运人手机号">
<el-input <el-input
@@ -1880,7 +1879,7 @@ import { getList as getContractList } from '@/api/business/contract-manage';
import { getParentOptions as getCargoTypeOptions } from '@/api/base/cargo-type'; import { getParentOptions as getCargoTypeOptions } from '@/api/base/cargo-type';
import { getList as getCustomerList } from '@/api/vehicle/customer-archive'; import { getList as getCustomerList } from '@/api/vehicle/customer-archive';
import { getList as getDriverList } from '@/api/transportCapacity/driver'; 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 { formatContractOptionLabel } from '@/utils/contract';
import { import {
getList as getProcessConfigList, getList as getProcessConfigList,
@@ -1905,7 +1904,6 @@ import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-sear
import { parseRouteChangeContent } from '@/utils/route-change'; import { parseRouteChangeContent } from '@/utils/route-change';
const defaultTransportType = ''; const defaultTransportType = '';
const defaultCandidateTransportType = '';
const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315'; const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a'; const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
const loadingCreateWaybillsStorageKey = 'loading-manage-create-waybills'; const loadingCreateWaybillsStorageKey = 'loading-manage-create-waybills';
@@ -1946,7 +1944,6 @@ const createCandidateSearchForm = () => ({
vehicleNo: '', vehicleNo: '',
carrierName: '', carrierName: '',
driverName: '', driverName: '',
transportType: defaultCandidateTransportType,
}); });
const createDialogForm = () => ({ const createDialogForm = () => ({
@@ -2401,6 +2398,7 @@ export default {
}, },
methods: { methods: {
parseRouteChangeContent, parseRouteChangeContent,
driverOptionText,
formatTaskCarrierContractLabel(item = {}) { formatTaskCarrierContractLabel(item = {}) {
const carrierName = item.carrierName || item.partyB || ''; const carrierName = item.carrierName || item.partyB || '';
const contractLabel = formatContractOptionLabel(item); const contractLabel = formatContractOptionLabel(item);
@@ -3104,7 +3102,6 @@ export default {
this.dialogMode = mode; this.dialogMode = mode;
this.dialogLoading = true; this.dialogLoading = true;
this.resetDialogData(); this.resetDialogData();
if (mode === 'add') this.candidateQuery.transportType = 'road';
try { try {
if (row?.id) { if (row?.id) {
const res = await loadingApi.getDetail(row.id); const res = await loadingApi.getDetail(row.id);
@@ -3131,7 +3128,6 @@ export default {
} else { } else {
this.dialogForm = createDialogForm(); this.dialogForm = createDialogForm();
this.candidateQuery = createCandidateSearchForm(); this.candidateQuery = createCandidateSearchForm();
this.candidateQuery.transportType = 'road';
await this.loadCandidateWaybills(); await this.loadCandidateWaybills();
} }
} finally { } finally {
@@ -3910,18 +3906,24 @@ export default {
async loadCandidateWaybills() { async loadCandidateWaybills() {
this.candidateLoading = true; this.candidateLoading = true;
try { try {
const selectedProjectId = this.selectedWaybillRows.map(row => row.projectId).find(Boolean);
const query = { const query = {
...this.buildQueryParams(this.candidateQuery), ...this.buildQueryParams(this.candidateQuery),
// 与后端 LoadableAdmissionSupport 一致:公路 + 待执行/进行中 + 未关联有效配载单 // 公路、待执行、未配载的判定都在后端可配载开关内
// 该开关已把候选限定为公路,再按精确运输方式取值筛会漏掉「公路运输」这类历史写法
loadableOnly: 1, loadableOnly: 1,
// 仅查当前账户所属组织及子组织下的运单 // allDept=0 表示不指定组织:超管与「数据权限范围=全部组织」的账户后端不过滤组织,
// 其余账户限定本组织(不展开下级)
allDept: 0, allDept: 0,
}; };
const transportType = this.getCandidateTransportTypeQuery(query.transportType); // 已选运单后,候选列表优先限定同一项目,便于再按承运商合同乙方交集筛选
if (selectedProjectId && !query.projectId && !query.projectName) {
query.projectId = selectedProjectId;
}
const res = await getWaybillList( const res = await getWaybillList(
this.candidatePage.currentPage, this.candidatePage.currentPage,
this.candidatePage.pageSize, this.candidatePage.pageSize,
{ ...query, transportType } query
); );
const page = unwrapPage(res); const page = unwrapPage(res);
this.candidateRows = page.records; this.candidateRows = page.records;
@@ -3930,20 +3932,12 @@ export default {
this.candidateLoading = false; this.candidateLoading = false;
} }
}, },
getCandidateTransportTypeQuery(value) {
const normalized = String(value || '')
.trim()
.toLowerCase();
if (['road', '公路运输'].includes(normalized)) return 'road,公路运输';
return value || '';
},
handleCandidateSearch() { handleCandidateSearch() {
this.candidatePage.currentPage = 1; this.candidatePage.currentPage = 1;
this.loadCandidateWaybills(); this.loadCandidateWaybills();
}, },
handleCandidateReset() { handleCandidateReset() {
this.candidateQuery = createCandidateSearchForm(); this.candidateQuery = createCandidateSearchForm();
if (this.dialogMode === 'add') this.candidateQuery.transportType = 'road';
this.candidatePage.currentPage = 1; this.candidatePage.currentPage = 1;
this.loadCandidateWaybills(); this.loadCandidateWaybills();
}, },
@@ -3987,7 +3981,10 @@ export default {
]; ];
if (projectIds.length) { if (projectIds.length) {
try { try {
const response = await loadingApi.getCarrierContracts(projectIds); 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 contracts = response?.data?.data || response?.data || [];
const stillAllowed = (Array.isArray(contracts) ? contracts : []).some( const stillAllowed = (Array.isArray(contracts) ? contracts : []).some(
item => String(item.id) === String(contractId) item => String(item.id) === String(contractId)
@@ -4005,14 +4002,50 @@ export default {
const exists = new Set( const exists = new Set(
this.selectedWaybillRows.map(row => String(row.id || row.waybillNo)) 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]; const nextRows = [...this.selectedWaybillRows];
selectedRows.forEach(row => { selectedRows.forEach(row => {
if (mismatchedRows.includes(row)) return;
const key = String(row.id || row.waybillNo); const key = String(row.id || row.waybillNo);
if (!exists.has(key)) { if (!exists.has(key)) {
nextRows.push(row); nextRows.push(row);
exists.add(key); 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]; const prevRouteNodes = [...this.routeNodes];
this.selectedWaybillRows = nextRows; this.selectedWaybillRows = nextRows;
if (this.isRunningEdit) { if (this.isRunningEdit) {
@@ -4026,9 +4059,24 @@ export default {
this.candidateAdding = false; 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() { async addSelectedWaybills() {
await this.addWaybillRows(this.candidateSelection); await this.addWaybillRows(this.candidateSelection);
}, },
waybillProjectKey(row = {}) {
return String(row.projectId || row.projectName || '').trim();
},
removeSelectedWaybill(index) { removeSelectedWaybill(index) {
const row = this.selectedWaybillRows[index]; const row = this.selectedWaybillRows[index];
if (this.isSelectedWaybillRemoveLocked(row)) { if (this.isSelectedWaybillRemoveLocked(row)) {
@@ -4551,12 +4599,6 @@ export default {
this.dialogForm.escortPhone = ''; this.dialogForm.escortPhone = '';
this.driverOptions = []; this.driverOptions = [];
}, },
getDialogCarrierFilter() {
return {
carrierId: this.dialogForm.carrierId || '',
carrierName: this.dialogForm.carrierName || '',
};
},
handleDriverChange(value) { handleDriverChange(value) {
const driver = this.driverOptions.find(item => (item.driverName || item.name) === value); const driver = this.driverOptions.find(item => (item.driverName || item.name) === value);
if (driver && !this.dialogForm.driverPhone) { if (driver && !this.dialogForm.driverPhone) {
@@ -4590,16 +4632,12 @@ export default {
}, },
fetchEscortSuggestions(queryString, callback) { fetchEscortSuggestions(queryString, callback) {
const keyword = String(queryString || '').trim(); const keyword = String(queryString || '').trim();
const carrier = this.getDialogCarrierFilter();
if (!carrier.carrierId && !carrier.carrierName) {
callback([]);
return;
}
this.escortLoading = true; this.escortLoading = true;
fetchDriversByCarrierOrganizations( fetchDriverCandidates({
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '押运员' }, size: 20,
carrier ...(keyword ? { driverName: keyword } : {}),
) posts: '押运员',
})
.then(records => { .then(records => {
callback( callback(
records.map(item => ({ records.map(item => ({
@@ -4679,17 +4717,13 @@ export default {
} }
}, },
async loadDialogDriverOptions(keyword = '') { async loadDialogDriverOptions(keyword = '') {
const carrier = this.getDialogCarrierFilter();
if (!carrier.carrierId && !carrier.carrierName) {
this.driverOptions = [];
return [];
}
this.driverLoading = true; this.driverLoading = true;
try { try {
this.driverOptions = await fetchDriversByCarrierOrganizations( this.driverOptions = await fetchDriverCandidates({
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' }, size: 20,
carrier ...(keyword ? { driverName: keyword } : {}),
); posts: '司机',
});
return this.driverOptions; return this.driverOptions;
} finally { } finally {
this.driverLoading = false; this.driverLoading = false;
@@ -4720,10 +4754,13 @@ export default {
} }
this.taskCarrierLoading = true; this.taskCarrierLoading = true;
try { try {
const projectIds = [ // 配载单只有一个项目;传入子运单后按客户合同甲方匹配并取承运商合同乙方交集
...new Set(this.selectedWaybillRows.map(row => row.projectId).filter(Boolean)), 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(projectIds); const response = await loadingApi.getCarrierContracts(
projectId ? [projectId] : [],
waybillIds
);
if (requestId !== this.taskCarrierRequestId) return []; if (requestId !== this.taskCarrierRequestId) return [];
const contracts = response.data?.data || response.data || []; const contracts = response.data?.data || response.data || [];
if (this.dialogForm.carrierType !== '自运') { if (this.dialogForm.carrierType !== '自运') {
+14 -11
View File
@@ -245,7 +245,7 @@
<el-radio-group <el-radio-group
v-model="row.confirmMode" v-model="row.confirmMode"
:disabled="dialogReadonly || !row.enabled" :disabled="dialogReadonly || !row.enabled"
@change="handleReturnConfirmChange(row)" @change="syncNodeFields"
> >
<el-radio label="yes">是</el-radio> <el-radio label="yes">是</el-radio>
<el-radio label="no_confirm_complete">无需确认完成</el-radio> <el-radio label="no_confirm_complete">无需确认完成</el-radio>
@@ -253,10 +253,7 @@
</div> </div>
<div class="process-config-form__require-line"> <div class="process-config-form__require-line">
<span class="process-config-form__require-label">确认完成</span> <span class="process-config-form__require-label">确认完成</span>
<el-checkbox <el-checkbox v-model="row.confirmInternal" :disabled="true"
v-model="row.confirmInternal"
:disabled="dialogReadonly || !row.enabled || row.confirmMode !== 'yes'"
@change="syncNodeFields"
>内部人员</el-checkbox >内部人员</el-checkbox
> >
</div> </div>
@@ -549,15 +546,18 @@ export default {
row.confirmDriver = row.confirmMode === 'yes'; row.confirmDriver = row.confirmMode === 'yes';
this.syncNodeFields(); this.syncNodeFields();
}, },
handleReturnConfirmChange(row) {
row.confirmInternal = row.confirmMode === 'yes';
this.syncNodeFields();
},
enforceAcceptDriver(node) { enforceAcceptDriver(node) {
if (!node || (node.key !== 'accept' && node.name !== '接单')) return node; if (!node || (node.key !== 'accept' && node.name !== '接单')) return node;
node.confirmDriver = node.confirmMode === 'yes'; node.confirmDriver = node.confirmMode === 'yes';
return node; return node;
}, },
// 回单「确认完成」只有内部人员一种角色,是否确认为「是」时即由内部人员确认,
// 界面上只读展示,值始终由 confirmMode 推导
enforceReturnInternal(node) {
if (!node || (node.key !== 'return' && node.name !== '回单')) return node;
node.confirmInternal = node.confirmMode === 'yes';
return node;
},
initDeptOptions() { initDeptOptions() {
getDeptTree(this.userInfo.tenantId).then(res => { getDeptTree(this.userInfo.tenantId).then(res => {
const deptColumn = this.findColumn(this.option.column, 'searchDeptId'); const deptColumn = this.findColumn(this.option.column, 'searchDeptId');
@@ -724,7 +724,7 @@ export default {
this.projectLoading = true; this.projectLoading = true;
return getProjectList(1, 50, { return getProjectList(1, 50, {
projectName: keyword, projectName: keyword,
approvalStatus: 'approved', approvalStatuses: 'approved,change_approved',
allDept: this.isAdmin ? 1 : 0, allDept: this.isAdmin ? 1 : 0,
}) })
.then(res => { .then(res => {
@@ -808,7 +808,10 @@ export default {
this.syncNodeFields(); this.syncNodeFields();
}, },
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); const enabledNodes = this.nodeRows.filter(item => item.enabled);
this.form.includedNodes = enabledNodes.map(item => item.name).join(','); this.form.includedNodes = enabledNodes.map(item => item.name).join(',');
this.form.nodeConfigJson = JSON.stringify( this.form.nodeConfigJson = JSON.stringify(
@@ -577,10 +577,7 @@ export default {
this.$message.warning(`第${index + 1}条调度明细缺少承运商信息`); this.$message.warning(`第${index + 1}条调度明细缺少承运商信息`);
return; return;
} }
if (!row.driverName) { // 司机是否必填由调度明细弹窗按承运类型判定(承运商承运时司机选填),此处不再重复校验
this.$message.warning(`第${index + 1}条调度明细缺少司机信息`);
return;
}
if (!row.vehicleNo) { if (!row.vehicleNo) {
this.$message.warning(`第${index + 1}条调度明细缺少车牌号`); this.$message.warning(`第${index + 1}条调度明细缺少车牌号`);
return; return;
+35 -15
View File
@@ -251,10 +251,10 @@
/> />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="不含税单价" min-width="130"> <el-table-column label="合计金额" min-width="140">
<template #default="{ row }"> <template #default="{ row }">
<el-input-number <el-input-number
v-model="row.unitPriceNoTax" v-model="row.totalAmount"
:disabled="readonly" :disabled="readonly"
:min="0" :min="0"
:precision="2" :precision="2"
@@ -263,9 +263,6 @@
/> />
</template> </template>
</el-table-column> </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"> <el-table-column label="税率" min-width="110">
<template #default="{ row }"> <template #default="{ row }">
<el-select <el-select
@@ -282,11 +279,14 @@
</el-select> </el-select>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="税额" min-width="120"> <el-table-column label="不含税单价" min-width="140" align="right">
<template #default="{ row }">{{ formatMoney(row.taxAmount) }}</template> <template #default="{ row }">{{ formatUnitPrice(row.unitPriceNoTax) }}</template>
</el-table-column> </el-table-column>
<el-table-column label="合计" min-width="130" align="right"> <el-table-column label="不含税金额" min-width="130" align="right">
<template #default="{ row }">{{ formatMoney(row.totalAmount) }}</template> <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>
<el-table-column label="备注" min-width="170"> <el-table-column label="备注" min-width="170">
<template #default="{ row }"> <template #default="{ row }">
@@ -1108,15 +1108,30 @@ export default {
addLine(sheet) { addLine(sheet) {
sheet.lines.push(emptyLine()); sheet.lines.push(emptyLine());
}, },
/**
* 填写合计金额 + 税率后反算:
* 合计金额 = 数量 × 不含税单价 × (1 + 税率)
* 不含税单价 = 合计金额 / 数量 / (1 + 税率),保留 6 位小数
* 不含税金额 = 数量 × 不含税单价,保留 2 位小数
*/
recalculateLine(row) { recalculateLine(row) {
const quantity = Number(row.quantity || 0); const quantity = Number(row.quantity || 0);
const unitPrice = Number(row.unitPriceNoTax || 0); const totalAmount = Number(Number(row.totalAmount || 0).toFixed(2));
const amountNoTax = Number((quantity * unitPrice).toFixed(2));
const rate = Number(row.taxRate || 0) / 100; 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.amountNoTax = amountNoTax;
row.taxAmount = rate ? Number((amountNoTax * rate).toFixed(2)) : 0; row.taxAmount = Number((totalAmount - amountNoTax).toFixed(2));
row.totalAmount = Number((amountNoTax + row.taxAmount).toFixed(2));
row.amountWithTax = row.totalAmount;
}, },
validateBusiness() { validateBusiness() {
if (!this.selectedDetailIds.length) throw new Error('请至少选择一条开票明细'); if (!this.selectedDetailIds.length) throw new Error('请至少选择一条开票明细');
@@ -1129,12 +1144,14 @@ export default {
for (const line of sheet.lines) { for (const line of sheet.lines) {
if (!line.goodsCategory || !line.goodsName) throw new Error('请完整填写商品和服务信息'); if (!line.goodsCategory || !line.goodsName) throw new Error('请完整填写商品和服务信息');
if ( if (
[line.quantity, line.unitPriceNoTax, line.amountNoTax, line.taxRate].some( [line.quantity, line.totalAmount, line.unitPriceNoTax, line.amountNoTax, line.taxRate].some(
Number.isNaN Number.isNaN
) )
) { ) {
throw new Error('开票商品行金额格式不正确'); 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) { formatMoney(value) {
return Number(value || 0).toFixed(2); return Number(value || 0).toFixed(2);
}, },
formatUnitPrice(value) {
return Number(value || 0).toFixed(6);
},
}, },
}; };
</script> </script>
@@ -1388,16 +1388,12 @@ export default {
}, },
immediate: true, immediate: true,
}, },
summaryTotal(value) { summaryTotal() {
this.form.settlementAmount = Number(value || 0).toFixed(2); this.syncSettlementAmountsFromSummary();
this.form.localSettlementAmount = (
Number(value || 0) * Number(this.form.exchangeRate || 1)
).toFixed(2);
}, },
'form.exchangeRate'(value) { 'form.exchangeRate'() {
this.form.localSettlementAmount = ( if (this._skipExchangeRateRebuild) return;
Number(this.summaryTotal || 0) * Number(value || 1) this.buildSummaryFeesFromDetails();
).toFixed(2);
}, },
}, },
methods: { methods: {
@@ -1683,8 +1679,9 @@ export default {
current.settlementAmount = current.originalAmount; current.settlementAmount = current.originalAmount;
summaryMap.set(key, current); summaryMap.set(key, current);
}); });
// 先保留预结算手工费用行,再按明细本位币 + 结算汇率重算系统归集行
this.summaryFees = [...summaryMap.values()]; this.summaryFees = [...summaryMap.values()];
if (!this.summaryFees.length) await this.refreshSummaryFees(); await this.refreshSummaryFees();
}, },
async loadAllContracts() { async loadAllContracts() {
const response = await getContractOptions(''); const response = await getContractOptions('');
@@ -1786,13 +1783,19 @@ export default {
}, },
async fetchBfiExchangeRate() { async fetchBfiExchangeRate() {
const currency = this.form.currency; const currency = this.form.currency;
if (!currency || currency === 'RMB') return; if (!currency || currency === 'RMB') {
this.buildSummaryFeesFromDetails();
return;
}
if (!this.form.exchangeRateDate) return; if (!this.form.exchangeRateDate) return;
try { try {
const { data } = await getBfiExchangeRate(currency, this.form.exchangeRateDate); const { data } = await getBfiExchangeRate(currency, this.form.exchangeRateDate);
const rateData = data?.data; const rateData = data?.data;
if (rateData?.excval != null) { if (rateData?.excval != null) {
this._skipExchangeRateRebuild = true;
this.form.exchangeRate = Number(rateData.excval); this.form.exchangeRate = Number(rateData.excval);
this._skipExchangeRateRebuild = false;
this.buildSummaryFeesFromDetails();
} else { } else {
this.$message.warning(`未查询到 ${currency} 的BFI汇率数据`); this.$message.warning(`未查询到 ${currency} 的BFI汇率数据`);
} }
@@ -1800,6 +1803,37 @@ export default {
this.$message.warning('查询BFI汇率失败,请手动输入结算汇率'); 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() { openCandidateDialog() {
this.candidate.visible = true; this.candidate.visible = true;
this.candidate.page.current = 1; this.candidate.page.current = 1;
@@ -2021,8 +2055,9 @@ export default {
.map(row => [`${row.feeType}\u0000${row.feeItem}`, row]) .map(row => [`${row.feeType}\u0000${row.feeItem}`, row])
); );
const append = (feeItem, originalValue, settlementValue, fallbackFeeType = '') => { 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)) { if (!feeItem || !Number.isFinite(originalAmount) || !Number.isFinite(settlementAmount)) {
return; return;
} }
@@ -2060,6 +2095,7 @@ export default {
}); });
const manualRows = this.summaryFees.filter(row => Number(row.manualFlag) === 1); const manualRows = this.summaryFees.filter(row => Number(row.manualFlag) === 1);
this.summaryFees = [...generatedMap.values(), ...manualRows]; this.summaryFees = [...generatedMap.values(), ...manualRows];
this.syncSettlementAmountsFromSummary();
}, },
// 每条明细对应的费用行:[调整前基准行, 调整后当前行],缺少费用快照时退化为明细自身的汇总行。 // 每条明细对应的费用行:[调整前基准行, 调整后当前行],缺少费用快照时退化为明细自身的汇总行。
resolveDetailFeePairs(detail) { resolveDetailFeePairs(detail) {
@@ -1332,9 +1332,12 @@ export default {
if (value) this.initialize(); if (value) this.initialize();
}, },
}, },
summaryTotal(value) { summaryTotal() {
this.form.settlementAmount = Number(value || 0).toFixed(2); this.syncSettlementAmountsFromSummary();
this.recalculateLocalAmount(); },
'form.exchangeRate'() {
if (this._skipExchangeRateRebuild) return;
this.buildSummaryFeesFromDetails();
}, },
filteredDetails: { filteredDetails: {
handler(list) { handler(list) {
@@ -1890,8 +1893,9 @@ export default {
.map(row => [`${row.feeType}\u0000${row.feeItem}`, row]) .map(row => [`${row.feeType}\u0000${row.feeItem}`, row])
); );
const appendSummary = (feeItem, originalValue, settlementValue, fallbackFeeType = '') => { 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)) { if (!feeItem || !Number.isFinite(originalAmount) || !Number.isFinite(settlementAmount)) {
return; return;
} }
@@ -1946,7 +1950,7 @@ export default {
}); });
nextFeeMap.forEach(row => orderedFees.push(row)); nextFeeMap.forEach(row => orderedFees.push(row));
this.summaryFees = orderedFees; this.summaryFees = orderedFees;
this.recalculateLocalAmount(); this.syncSettlementAmountsFromSummary();
}, },
// 每条明细对应的费用行:[调整前基准行, 调整后当前行],缺少来源费用快照时退化为明细自身的汇总行。 // 每条明细对应的费用行:[调整前基准行, 调整后当前行],缺少来源费用快照时退化为明细自身的汇总行。
resolveDetailFeePairs(detail) { resolveDetailFeePairs(detail) {
@@ -2034,17 +2038,21 @@ export default {
}, },
handleExchangeRateDateChange() { handleExchangeRateDateChange() {
this.fetchBfiExchangeRate(); this.fetchBfiExchangeRate();
this.recalculateLocalAmount();
}, },
async fetchBfiExchangeRate() { 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; if (!this.form.exchangeRateDate) return;
try { try {
const { data } = await getBfiExchangeRate(this.form.currency, this.form.exchangeRateDate); const { data } = await getBfiExchangeRate(this.form.currency, this.form.exchangeRateDate);
const rateData = data?.data; const rateData = data?.data;
if (rateData?.excval != null) { if (rateData?.excval != null) {
this._skipExchangeRateRebuild = true;
this.form.exchangeRate = Number(rateData.excval); this.form.exchangeRate = Number(rateData.excval);
this.recalculateLocalAmount(); this._skipExchangeRateRebuild = false;
this.buildSummaryFeesFromDetails();
} else { } else {
this.$message.warning(`未查询到 ${this.form.currency} 的BFI汇率数据`); this.$message.warning(`未查询到 ${this.form.currency} 的BFI汇率数据`);
} }
@@ -2052,9 +2060,39 @@ export default {
this.$message.warning('查询BFI汇率失败,请手动输入结算汇率'); 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() { recalculateLocalAmount() {
const rate = this.form.currency === 'RMB' ? 1 : Number(this.form.exchangeRate || 0); this.buildSummaryFeesFromDetails();
this.form.localSettlementAmount = (Number(this.form.settlementAmount || 0) * rate).toFixed(2);
}, },
openCandidateDialog() { openCandidateDialog() {
if (!this.form.contractId) { if (!this.form.contractId) {
-10
View File
@@ -315,11 +315,6 @@ export default {
}, },
rowSave(row, done, loading) { rowSave(row, done, loading) {
const payload = this.normalizeSubmitRow(row, false); const payload = this.normalizeSubmitRow(row, false);
if (!payload.deptId) {
this.$message.warning('请选择所属组织');
loading();
return;
}
if (payload.dataScopeRange === 3 && (!payload.dataScopeDeptIds || !payload.dataScopeDeptIds.length)) { if (payload.dataScopeRange === 3 && (!payload.dataScopeDeptIds || !payload.dataScopeDeptIds.length)) {
this.$message.warning('请选择自定义数据权限组织'); this.$message.warning('请选择自定义数据权限组织');
loading(); loading();
@@ -336,11 +331,6 @@ export default {
}, },
rowUpdate(row, index, done, loading) { rowUpdate(row, index, done, loading) {
const payload = this.normalizeSubmitRow(row, true); const payload = this.normalizeSubmitRow(row, true);
if (!payload.deptId) {
this.$message.warning('请选择所属组织');
loading();
return;
}
if (payload.dataScopeRange === 3 && (!payload.dataScopeDeptIds || !payload.dataScopeDeptIds.length)) { if (payload.dataScopeRange === 3 && (!payload.dataScopeDeptIds || !payload.dataScopeDeptIds.length)) {
this.$message.warning('请选择自定义数据权限组织'); this.$message.warning('请选择自定义数据权限组织');
loading(); loading();
-43
View File
@@ -175,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="工号"><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="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="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-col :span="24">
<el-form-item label="备注"> <el-form-item label="备注">
<el-input <el-input
@@ -202,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-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-table>
</el-tab-pane> </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> </el-tabs>
<template #footer> <template #footer>
<span v-if="isUserViewMode" class="dialog-footer"> <span v-if="isUserViewMode" class="dialog-footer">
@@ -407,7 +398,6 @@ import {
auditRefuse, auditRefuse,
setLeader, setLeader,
getLeaderList, getLeaderList,
getCustomerOptions,
} from '@/api/system/user'; } from '@/api/system/user';
import { exportBlob } from '@/api/common'; import { exportBlob } from '@/api/common';
import { getDeptTree } from '@/api/system/dept'; import { getDeptTree } from '@/api/system/dept';
@@ -477,7 +467,6 @@ export default {
treeData: [], treeData: [],
permissionDeptTree: [], permissionDeptTree: [],
postList: [], postList: [],
customerList: [],
treeOption: treeOption, treeOption: treeOption,
userOption: userOption(this), userOption: userOption(this),
data: [], data: [],
@@ -952,9 +941,6 @@ export default {
const column = this.findColumn(this.userOption.group, 'leaderId'); const column = this.findColumn(this.userOption.group, 'leaderId');
column.dicData = res.data.data; column.dicData = res.data.data;
}); });
getCustomerOptions().then(res => {
this.customerList = res.data.data || [];
});
}, },
// 由部门 id 反查「根 → 该节点」的 id 路径,供级联回显(找不到返回 null) // 由部门 id 反查「根 → 该节点」的 id 路径,供级联回显(找不到返回 null)
findDeptPath(id, tree = this.permissionDeptTree, parents = []) { findDeptPath(id, tree = this.permissionDeptTree, parents = []) {
@@ -1000,21 +986,6 @@ export default {
.split(this.form.roleId || '') .split(this.form.roleId || '')
.filter(id => String(id) !== String(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) { normalizeSexDisplay(sex) {
if (sex === -1 || sex === '-1' || sex === '' || sex === null || sex === undefined) { if (sex === -1 || sex === '-1' || sex === '' || sex === null || sex === undefined) {
return undefined; return undefined;
@@ -1065,13 +1036,8 @@ export default {
deptId: [], deptId: [],
code: '', code: '',
personCategory: 1, personCategory: 1,
dataScopeRange: 1,
dataLevelRange: 1,
remark: '', remark: '',
roleId: '', roleId: '',
dataScopeDeptIds: [],
customerIds: [],
includeNewCustomer: 0,
}; };
this.userDialog = true; this.userDialog = true;
this.$nextTick(() => this.$refs.userFormRef && this.$refs.userFormRef.clearValidate()); this.$nextTick(() => this.$refs.userFormRef && this.$refs.userFormRef.clearValidate());
@@ -1085,22 +1051,13 @@ export default {
deptId: Array.isArray(deptIds) ? deptIds : [], deptId: Array.isArray(deptIds) ? deptIds : [],
postId: func.split(user.postId || '')[0] || '', postId: func.split(user.postId || '')[0] || '',
roleId: func.join(user.roleId || ''), roleId: func.join(user.roleId || ''),
dataScopeDeptIds: user.dataScopeDeptIds || [],
customerIds: user.customerIds || [],
includeNewCustomer: user.includeNewCustomer || 0,
personCategory: user.personCategory || 1, personCategory: user.personCategory || 1,
dataScopeRange: user.dataScopeRange || 1,
dataLevelRange: user.dataLevelRange || 1,
sex: this.normalizeSexDisplay(user.sex), sex: this.normalizeSexDisplay(user.sex),
}; };
this.sensitiveManager.saveInitialData(this.form); this.sensitiveManager.saveInitialData(this.form);
this.userDialog = true; this.userDialog = true;
this.$nextTick(() => { this.$nextTick(() => {
this.$refs.userFormRef && this.$refs.userFormRef.clearValidate(); 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);
});
}); });
}); });
}, },
+36 -15
View File
@@ -60,7 +60,11 @@
<span>{{ row.idCardNo || '-' }}</span> <span>{{ row.idCardNo || '-' }}</span>
</template> </template>
<template #organizationName="{ row }"> <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>
<template #status="{ row }"> <template #status="{ row }">
<el-tag :type="row.status === 1 ? 'primary' : 'info'" class="status-text"> <el-tag :type="row.status === 1 ? 'primary' : 'info'" class="status-text">
@@ -143,7 +147,7 @@
maxlength="18" maxlength="18"
show-word-limit show-word-limit
placeholder="请输入" placeholder="请输入"
@input="handleIdCardInput" @blur="handleIdCardBlur"
/> />
</el-form-item> </el-form-item>
</el-col> </el-col>
@@ -153,7 +157,7 @@
v-model="driverForm.birthday" v-model="driverForm.birthday"
type="date" type="date"
value-format="YYYY-MM-DD" value-format="YYYY-MM-DD"
placeholder="请选择" placeholder="请选择出生年月"
/> />
</el-form-item> </el-form-item>
</el-col> </el-col>
@@ -201,12 +205,16 @@
</el-col> </el-col>
<el-col :span="6"> <el-col :span="6">
<el-form-item label="住址" prop="addressRegion"> <el-form-item label="住址" prop="addressRegion">
<!-- 只读直接显示库里那串区划名称:级联要先按名称反解 path,区划改名后反解不出来,
同一串文字就会在灰字 placeholder 与黑字标签之间摇摆 -->
<el-input v-if="readonly" :model-value="driverForm.addressRegion || '—'" readonly />
<el-cascader <el-cascader
v-else
ref="addressRegionCascader" ref="addressRegionCascader"
v-model="driverForm.addressRegionPath" v-model="driverForm.addressRegionPath"
:options="regionOptions" :options="regionOptions"
:props="regionCascaderProps" :props="regionCascaderProps"
:placeholder="driverForm.addressRegion || '请选择'" :placeholder="driverForm.addressRegion || '请选择省/市/区'"
clearable clearable
filterable filterable
@change="handleAddressRegionChange" @change="handleAddressRegionChange"
@@ -219,7 +227,7 @@
v-model="driverForm.address" v-model="driverForm.address"
maxlength="200" maxlength="200"
show-word-limit show-word-limit
placeholder="请输入" placeholder="请输入详细地址(街道、门牌号)"
/> />
</el-form-item> </el-form-item>
</el-col> </el-col>
@@ -592,7 +600,11 @@
<span>{{ row.plateNo || '-' }}</span> <span>{{ row.plateNo || '-' }}</span>
</template> </template>
<template #organizationName="{ row }"> <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>
<template #boundDriver="{ row }"> <template #boundDriver="{ row }">
<span>{{ row.boundDriver || '-' }}</span> <span>{{ row.boundDriver || '-' }}</span>
@@ -743,6 +755,8 @@ export default {
expired: 0, expired: 0,
}, },
driverBox: false, driverBox: false,
// 搜索栏组织级联追加「未分配组织」一项(哨兵值见 organization-search mixin)
enableUnassignedOrgFilter: true,
vehicleSelectorVisible: false, vehicleSelectorVisible: false,
vehicleSelectorLoading: false, vehicleSelectorLoading: false,
vehicleSelectorOption, vehicleSelectorOption,
@@ -857,7 +871,7 @@ export default {
emergencyContactMobile: [ emergencyContactMobile: [
{ required: true, message: '请输入紧急联系人手机号', trigger: 'blur' }, { required: true, message: '请输入紧急联系人手机号', trigger: 'blur' },
], ],
organizationName: [{ required: true, message: '请选择所属组织', trigger: 'change' }], // 后端已允许所属组织为空,这里不做必填校验
idCardFront: [{ required: true, message: '请上传身份证正面照', trigger: 'change' }], idCardFront: [{ required: true, message: '请上传身份证正面照', trigger: 'change' }],
idCardBack: [{ required: true, message: '请上传身份证反面照', trigger: 'change' }], idCardBack: [{ required: true, message: '请上传身份证反面照', trigger: 'change' }],
drivingLicenseFront: [{ required: true, message: '请上传驾驶证主页', trigger: 'change' }], drivingLicenseFront: [{ required: true, message: '请上传驾驶证主页', trigger: 'change' }],
@@ -1122,19 +1136,21 @@ export default {
removeDigits(value = '') { removeDigits(value = '') {
return String(value).replace(/\d/g, ''); 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; this.driverForm.idCardNo = idCardNo;
const idCardInfo = this.parseIdCardInfo(idCardNo); const idCardInfo = this.parseIdCardInfo(idCardNo);
if (idCardInfo.birthday) { if (!this.driverForm.birthday && idCardInfo.birthday) {
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; this.driverForm.gender = idCardInfo.gender;
} else {
this.driverForm.gender = '';
} }
if (this.submitAttempted) { if (this.submitAttempted) {
this.$nextTick(() => { this.$nextTick(() => {
@@ -1952,6 +1968,11 @@ export default {
} }
} }
// 空组织记录在列表里的占位文案
.organization-name--empty {
color: #909399;
}
.driver-page__date-expiring { .driver-page__date-expiring {
color: #d40000; color: #d40000;
font-weight: 600; font-weight: 600;
+149 -23
View File
@@ -59,7 +59,22 @@
</el-button> </el-button>
</template> </template>
<template #organizationName="{ row }"> <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>
<template #status="{ row }"> <template #status="{ row }">
<el-tag :type="row.status === 1 ? 'primary' : 'info'" class="status-text"> <el-tag :type="row.status === 1 ? 'primary' : 'info'" class="status-text">
@@ -315,26 +330,58 @@
</el-col> </el-col>
<el-col :span="6"> <el-col :span="6">
<el-form-item label="所有组织" prop="organizationName"> <el-form-item label="所有组织" prop="organizationName">
<el-cascader <el-tooltip
v-model="organizationCascaderValue" :content="
:options="organizationTreeOptions" formatOrgCascaderPath(organizationCascaderPath) ||
:props="organizationCascaderProps" formatOrgDisplayName(vehicleForm.organizationName)
filterable "
clearable placement="top"
placeholder="请选择" :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-form-item>
</el-col> </el-col>
<el-col :span="6"> <el-col :span="6">
<el-form-item label="使用部门" prop="useDepartment"> <el-form-item label="使用部门" prop="useDepartment">
<el-cascader <el-tooltip
v-model="useDepartmentCascaderValue" :content="
:options="organizationTreeOptions" formatOrgCascaderPath(useDepartmentCascaderPath) ||
:props="organizationCascaderProps" formatOrgDisplayName(vehicleForm.useDepartment)
filterable "
clearable placement="top"
placeholder="请选择" :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-form-item>
</el-col> </el-col>
</el-row> </el-row>
@@ -652,6 +699,17 @@ export default {
vehicleCertificateUploads: {}, vehicleCertificateUploads: {},
uploadHeaders: getUploadHeaders(), uploadHeaders: getUploadHeaders(),
organizationOptions: [], organizationOptions: [],
// 表单两个级联各自维护路径,避免名称回写互相覆盖;不要与搜索栏共用同一棵树节点
organizationCascaderPath: [],
useDepartmentCascaderPath: [],
formOrgCascaderProps: {
value: 'id',
label: 'label',
children: 'children',
emitPath: true,
checkStrictly: true,
expandTrigger: 'hover',
},
vehicleTypeOptions: [ vehicleTypeOptions: [
{ label: '高栏车', value: '高栏' }, { label: '高栏车', value: '高栏' },
{ label: '低栏车', value: '低栏' }, { label: '低栏车', value: '低栏' },
@@ -697,22 +755,31 @@ export default {
: [{ required: true, message: '请选择行驶证有效期', trigger: 'change' }], : [{ required: true, message: '请选择行驶证有效期', trigger: 'change' }],
}; };
}, },
// 所属组织级联:表单存组织名称,级联控件用 id 路径,两者在此双向转换 // 两个级联必须使用克隆后的独立树,否则 checkStrictly 会在节点上互相抢选中态
organizationFormTreeOptions() {
return this.cloneOrgTree(this.organizationTreeOptions);
},
useDepartmentFormTreeOptions() {
return this.cloneOrgTree(this.organizationTreeOptions);
},
// 所属组织级联:本地保存 id 路径,名称同步到表单字段供后端使用
organizationCascaderValue: { organizationCascaderValue: {
get() { get() {
return this.resolveOrgPath(this.vehicleForm.organizationName); return this.organizationCascaderPath;
}, },
set(path) { set(path) {
this.vehicleForm.organizationName = this.resolveOrgName(path); this.organizationCascaderPath = Array.isArray(path) ? [...path] : [];
this.vehicleForm.organizationName = this.resolveOrgName(this.organizationCascaderPath);
}, },
}, },
// 使用部门级联:与所属组织同一套部门树 // 使用部门级联:独立路径状态,避免与所属组织共用计算回写导致选不中
useDepartmentCascaderValue: { useDepartmentCascaderValue: {
get() { get() {
return this.resolveOrgPath(this.vehicleForm.useDepartment); return this.useDepartmentCascaderPath;
}, },
set(path) { set(path) {
this.vehicleForm.useDepartment = this.resolveOrgName(path); this.useDepartmentCascaderPath = Array.isArray(path) ? [...path] : [];
this.vehicleForm.useDepartment = this.resolveOrgName(this.useDepartmentCascaderPath);
}, },
}, },
isAdmin() { isAdmin() {
@@ -745,10 +812,49 @@ export default {
this.initDeptTree(); this.initDeptTree();
this.loadOrganizationOptions(); this.loadOrganizationOptions();
}, },
watch: {
organizationTreeOptions() {
if (this.vehicleBox) {
this.syncOrgCascaderPaths();
}
},
},
methods: { methods: {
hasPermission(code) { hasPermission(code) {
return this.isAdmin || this.permission?.[code] === true; 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() { initDeptTree() {
getDeptTree(this.userInfo.tenantId).then(res => { getDeptTree(this.userInfo.tenantId).then(res => {
this.organizationOptions = this.flattenDeptOptions(res.data.data || []); this.organizationOptions = this.flattenDeptOptions(res.data.data || []);
@@ -760,6 +866,9 @@ export default {
if (this.vehicleBox && !this.vehicleForm.useDepartment) { if (this.vehicleBox && !this.vehicleForm.useDepartment) {
this.vehicleForm.useDepartment = this.getCurrentOrganizationName(); this.vehicleForm.useDepartment = this.getCurrentOrganizationName();
} }
if (this.vehicleBox) {
this.syncOrgCascaderPaths();
}
}); });
}, },
flattenDeptOptions(tree = [], level = 0) { flattenDeptOptions(tree = [], level = 0) {
@@ -854,6 +963,7 @@ export default {
this.vehicleForm = emptyForm(); this.vehicleForm = emptyForm();
this.vehicleForm.organizationName = this.getCurrentOrganizationName(); this.vehicleForm.organizationName = this.getCurrentOrganizationName();
this.vehicleForm.useDepartment = this.getCurrentOrganizationName(); this.vehicleForm.useDepartment = this.getCurrentOrganizationName();
this.syncOrgCascaderPaths();
this.syncPlateNo(false); this.syncPlateNo(false);
this.vehicleBox = true; this.vehicleBox = true;
return; return;
@@ -867,6 +977,7 @@ export default {
if (!this.vehicleForm.useDepartment) { if (!this.vehicleForm.useDepartment) {
this.vehicleForm.useDepartment = this.getCurrentOrganizationName(); this.vehicleForm.useDepartment = this.getCurrentOrganizationName();
} }
this.syncOrgCascaderPaths();
this.splitPlateNo(); this.splitPlateNo();
this.syncPlateNo(false); this.syncPlateNo(false);
this.vehicleBox = true; this.vehicleBox = true;
@@ -878,6 +989,8 @@ export default {
}, },
resetVehicle() { resetVehicle() {
this.vehicleForm = emptyForm(); this.vehicleForm = emptyForm();
this.organizationCascaderPath = [];
this.useDepartmentCascaderPath = [];
this.vehicleCertificateUploads = {}; this.vehicleCertificateUploads = {};
this.readonly = false; this.readonly = false;
this.submitLoading = 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 { .vehicle-form {
.org-cascader-tooltip {
width: 100%;
}
:deep(.el-date-editor.el-input), :deep(.el-date-editor.el-input),
:deep(.el-date-editor.el-input__wrapper), :deep(.el-date-editor.el-input__wrapper),
:deep(.el-select), :deep(.el-select),
@@ -102,15 +102,10 @@
/> />
</el-form-item> </el-form-item>
<el-form-item label="减分最高扣分"> <el-form-item label="减分最高扣分">
<el-input <el-input :model-value="HARDCODED_MAX_MINUS_SCORE" disabled style="width: 500px">
:model-value="configForm.maxMinusScore"
placeholder="所有减分项合计不得超过该分值"
clearable
style="width: 500px"
@input="handleMaxMinusScoreInput"
>
<template #append>分</template> <template #append>分</template>
</el-input> </el-input>
<div class="score-config__hint">暂定最高扣 15 分,加分合计同样最高按 15 分计</div>
</el-form-item> </el-form-item>
<el-form-item label="备注"> <el-form-item label="备注">
<el-input <el-input
@@ -500,9 +495,15 @@ const standardNumberFields = [
'creditLimitIncrease', 'creditLimitIncrease',
]; ];
/** 暂定:加分/减分合计上限均为 15 分,超出按 15 分计 */
const HARDCODED_MAX_PLUS_SCORE = 15;
const HARDCODED_MAX_MINUS_SCORE = 15;
export default { export default {
data() { data() {
return { return {
HARDCODED_MAX_PLUS_SCORE,
HARDCODED_MAX_MINUS_SCORE,
form: {}, form: {},
query: {}, query: {},
loading: true, loading: true,
@@ -639,7 +640,7 @@ export default {
name: '', name: '',
remark: '', remark: '',
standardDescription: '', standardDescription: '',
maxMinusScore: '', maxMinusScore: HARDCODED_MAX_MINUS_SCORE,
status: 3, status: 3,
categories: this.defaultCategories(), categories: this.defaultCategories(),
standards: [], standards: [],
@@ -696,7 +697,8 @@ export default {
}); });
return { return {
...detail, ...detail,
maxMinusScore: this.normalizePlainDecimal(detail.maxMinusScore), // 暂定写死最高扣分,不读取历史配置值
maxMinusScore: HARDCODED_MAX_MINUS_SCORE,
categories: categories.map(category => ({ categories: categories.map(category => ({
...(map[category.categoryCode] || category), ...(map[category.categoryCode] || category),
categoryName: category.categoryName, categoryName: category.categoryName,
@@ -704,14 +706,6 @@ export default {
standards: (detail.standards || []).map(standard => this.normalizeStandard(standard)), standards: (detail.standards || []).map(standard => this.normalizeStandard(standard)),
}; };
}, },
handleMaxMinusScoreInput(value) {
const raw = String(value ?? '')
.replace(/[^\d.]/g, '')
.replace(/^\./, '')
.replace(/(\..*)\./g, '$1')
.replace(/^(\d+)(\.\d{0,2})?.*$/, '$1$2');
this.configForm.maxMinusScore = raw;
},
openConfig(row, readonly = false) { openConfig(row, readonly = false) {
this.readonly = readonly; this.readonly = readonly;
if (row && row.id) { if (row && row.id) {
@@ -837,9 +831,7 @@ export default {
}, },
prepareSubmitConfig(config) { prepareSubmitConfig(config) {
const payload = JSON.parse(JSON.stringify(config || {})); const payload = JSON.parse(JSON.stringify(config || {}));
const maxMinus = this.normalizePlainDecimal(payload.maxMinusScore); payload.maxMinusScore = this.toScoreValue(HARDCODED_MAX_MINUS_SCORE);
payload.maxMinusScore =
maxMinus === '' || maxMinus == null ? null : this.toScoreValue(maxMinus);
(payload.categories || []).forEach(category => { (payload.categories || []).forEach(category => {
(category.items || []).forEach(item => { (category.items || []).forEach(item => {
item.score = this.toScoreValue(item.score); item.score = this.toScoreValue(item.score);
@@ -1096,14 +1088,8 @@ export default {
this.$message.warning('请输入评定表名称'); this.$message.warning('请输入评定表名称');
return false; return false;
} }
if ( // 减分最高扣分暂定写死为 15,提交前强制回填,避免历史脏数据触发校验
this.configForm.maxMinusScore !== '' && this.configForm.maxMinusScore = HARDCODED_MAX_MINUS_SCORE;
this.configForm.maxMinusScore != null &&
!this.isNonNegativeDecimal(this.configForm.maxMinusScore)
) {
this.$message.warning('减分最高扣分须为不小于0的数字,最多两位小数');
return false;
}
const basic = this.configForm.categories.find(item => item.categoryCode === 'basic'); const basic = this.configForm.categories.find(item => item.categoryCode === 'basic');
if (!basic || basic.items.length === 0) { if (!basic || basic.items.length === 0) {
this.$message.warning('至少配置1个基础得分项目'); this.$message.warning('至少配置1个基础得分项目');
@@ -1586,6 +1572,13 @@ export default {
overflow: auto; overflow: auto;
} }
.score-config__hint {
margin-top: 6px;
color: #909399;
font-size: 12px;
line-height: 1.4;
}
:deep(.score-config__name-input) { :deep(.score-config__name-input) {
width: 500px; width: 500px;
} }
+31 -33
View File
@@ -1624,6 +1624,9 @@ const customerArchiveFormRoute = '/vehicle/customer-archive/form';
const customerArchiveListRefreshKey = 'customer-archive-list-refresh'; const customerArchiveListRefreshKey = 'customer-archive-list-refresh';
/** 客商所属组织固定为顶层组织 */ /** 客商所属组织固定为顶层组织 */
const FIXED_OWNER_DEPT_NAME = '广西现代物流集团有限公司'; const FIXED_OWNER_DEPT_NAME = '广西现代物流集团有限公司';
/** 暂定:加分/减分合计上限均为 15 分,超出按 15 分计 */
const HARDCODED_MAX_PLUS_SCORE = 15;
const HARDCODED_MAX_MINUS_SCORE = 15;
const createTimeRangeMap = { const createTimeRangeMap = {
createTimeRange: ['createTimeStart', 'createTimeEnd'], createTimeRange: ['createTimeStart', 'createTimeEnd'],
@@ -2336,16 +2339,18 @@ export default {
{ categoryCode: 'plus', categoryName: '加分项目' }, { categoryCode: 'plus', categoryName: '加分项目' },
{ categoryCode: 'minus', categoryName: '减分项目' }, { categoryCode: 'minus', categoryName: '减分项目' },
]; ];
const maxMinus = Number(this.currentScore?.maxMinusScore);
const hasMaxMinus = Number.isFinite(maxMinus) && maxMinus >= 0;
const rows = categoryMap.map((category, index) => { const rows = categoryMap.map((category, index) => {
const details = this.getScoreCategoryDetails(category.categoryCode); const details = this.getScoreCategoryDetails(category.categoryCode);
let selfScore = details.reduce((sum, item) => sum + Number(item.selfScore || 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); let reviewScore = details.reduce((sum, item) => sum + Number(item.reviewScore || 0), 0);
// 减分项目合计受量化表「最高扣分」限制 // 暂定:加分/减分合计最高按 15 分计
if (category.categoryCode === 'minus' && hasMaxMinus) { if (category.categoryCode === 'plus') {
selfScore = Math.min(selfScore, maxMinus); selfScore = Math.min(selfScore, HARDCODED_MAX_PLUS_SCORE);
reviewScore = Math.min(reviewScore, maxMinus); 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 { return {
...category, ...category,
@@ -4263,7 +4268,8 @@ export default {
scoreDate: this.$dayjs().format('YYYY-MM-DD'), scoreDate: this.$dayjs().format('YYYY-MM-DD'),
applyCreditLimit: this.archiveForm.applyCreditLimit || '', applyCreditLimit: this.archiveForm.applyCreditLimit || '',
maxCreditLimit: '', maxCreditLimit: '',
maxMinusScore: '', maxMinusScore: HARDCODED_MAX_MINUS_SCORE,
maxPlusScore: HARDCODED_MAX_PLUS_SCORE,
creditLevel: '', creditLevel: '',
standards: [], standards: [],
tempApplyCreditLimit: '', tempApplyCreditLimit: '',
@@ -4283,10 +4289,8 @@ export default {
: score.applyCreditLimit : score.applyCreditLimit
), ),
maxCreditLimit: this.normalizeOptionalAmount(score.maxCreditLimit), maxCreditLimit: this.normalizeOptionalAmount(score.maxCreditLimit),
maxMinusScore: maxMinusScore: HARDCODED_MAX_MINUS_SCORE,
score.maxMinusScore === undefined || score.maxMinusScore === null maxPlusScore: HARDCODED_MAX_PLUS_SCORE,
? ''
: score.maxMinusScore,
tempApplyCreditLimit: this.normalizeOptionalAmount(score.tempApplyCreditLimit), tempApplyCreditLimit: this.normalizeOptionalAmount(score.tempApplyCreditLimit),
attachments: score.attachments || this.parseAttachments(score.proofAttachments), attachments: score.attachments || this.parseAttachments(score.proofAttachments),
details: (score.details || []).map(detail => ({ details: (score.details || []).map(detail => ({
@@ -4748,11 +4752,8 @@ export default {
applyCreditLimit: this.currentScore.applyCreditLimit, applyCreditLimit: this.currentScore.applyCreditLimit,
maxCreditLimit: '', maxCreditLimit: '',
creditLevel: '', creditLevel: '',
maxMinusScore: maxMinusScore: HARDCODED_MAX_MINUS_SCORE,
quantificationData.maxMinusScore ?? maxPlusScore: HARDCODED_MAX_PLUS_SCORE,
scoreTemplate.maxMinusScore ??
this.currentScore.maxMinusScore ??
'',
tempApplyCreditLimit: this.currentScore.tempApplyCreditLimit, tempApplyCreditLimit: this.currentScore.tempApplyCreditLimit,
tempCreditStartDate: this.currentScore.tempCreditStartDate, tempCreditStartDate: this.currentScore.tempCreditStartDate,
tempCreditEndDate: this.currentScore.tempCreditEndDate, tempCreditEndDate: this.currentScore.tempCreditEndDate,
@@ -4771,14 +4772,11 @@ export default {
}, },
applyMaxMinusScoreFromQuantification(quantificationId) { applyMaxMinusScoreFromQuantification(quantificationId) {
if (!quantificationId) return Promise.resolve(); if (!quantificationId) return Promise.resolve();
return getCreditScoreQuantificationDetail(quantificationId).then(res => { // 暂定写死加分/减分上限,不再依赖量化表配置值
const data = res?.data?.data || {}; this.currentScore.maxMinusScore = HARDCODED_MAX_MINUS_SCORE;
this.currentScore.maxMinusScore = this.currentScore.maxPlusScore = HARDCODED_MAX_PLUS_SCORE;
data.maxMinusScore === undefined || data.maxMinusScore === null this.calculateScore(this.currentScore);
? '' return Promise.resolve();
: data.maxMinusScore;
this.calculateScore(this.currentScore);
});
}, },
normalizeScoreTemplateDetails(template = {}) { normalizeScoreTemplateDetails(template = {}) {
const categoryDetails = (template.categories || []).flatMap(category => const categoryDetails = (template.categories || []).flatMap(category =>
@@ -5122,17 +5120,17 @@ export default {
const plusDetails = this.getScoreCategoryDetailsByList(details, 'plus'); const plusDetails = this.getScoreCategoryDetailsByList(details, 'plus');
const minusDetails = this.getScoreCategoryDetailsByList(details, 'minus'); const minusDetails = this.getScoreCategoryDetailsByList(details, 'minus');
const sumScore = (list, prop) => list.reduce((sum, item) => sum + Number(item[prop] || 0), 0); const sumScore = (list, prop) => list.reduce((sum, item) => sum + Number(item[prop] || 0), 0);
const rawPlusSelf = sumScore(plusDetails, 'selfScore');
const rawPlusReview = sumScore(plusDetails, 'reviewScore');
const rawMinusSelf = sumScore(minusDetails, 'selfScore'); const rawMinusSelf = sumScore(minusDetails, 'selfScore');
const rawMinusReview = sumScore(minusDetails, 'reviewScore'); const rawMinusReview = sumScore(minusDetails, 'reviewScore');
// 量化表「减分最高扣分」:所有减分项合计不得超过该上限 // 暂定:加分/减分合计最高按 15 分计
const maxMinus = Number(score.maxMinusScore); const plusSelf = Math.min(rawPlusSelf, HARDCODED_MAX_PLUS_SCORE);
const hasMaxMinus = Number.isFinite(maxMinus) && maxMinus >= 0; const plusReview = Math.min(rawPlusReview, HARDCODED_MAX_PLUS_SCORE);
const minusSelf = hasMaxMinus ? Math.min(rawMinusSelf, maxMinus) : rawMinusSelf; const minusSelf = Math.min(rawMinusSelf, HARDCODED_MAX_MINUS_SCORE);
const minusReview = hasMaxMinus ? Math.min(rawMinusReview, maxMinus) : rawMinusReview; const minusReview = Math.min(rawMinusReview, HARDCODED_MAX_MINUS_SCORE);
const selfScore = const selfScore = sumScore(basicDetails, 'selfScore') + plusSelf - minusSelf;
sumScore(basicDetails, 'selfScore') + sumScore(plusDetails, 'selfScore') - minusSelf; const reviewScore = sumScore(basicDetails, 'reviewScore') + plusReview - minusReview;
const reviewScore =
sumScore(basicDetails, 'reviewScore') + sumScore(plusDetails, 'reviewScore') - minusReview;
const hasReviewScore = details.some( const hasReviewScore = details.some(
item => !this.isBlankScore(item.reviewScore) item => !this.isBlankScore(item.reviewScore)
); );
+2
View File
@@ -214,6 +214,7 @@
- 到场、装货、发货、在途、到货、卸货、签收节点的操作要求包含是否打卡、打卡定位;装货、发货、到货、卸货、签收还包含上传货量和上传货量类型;附件要求包含上传凭证和凭证类型。 - 到场、装货、发货、在途、到货、卸货、签收节点的操作要求包含是否打卡、打卡定位;装货、发货、到货、卸货、签收还包含上传货量和上传货量类型;附件要求包含上传凭证和凭证类型。
- 在途节点额外包含打卡频次和打卡时段,打卡频次使用普通输入框,打卡时段使用时间选择器。 - 在途节点额外包含打卡频次和打卡时段,打卡频次使用普通输入框,打卡时段使用时间选择器。
- 回单节点的操作要求包含是否确认、确认完成,是否确认提供“是 / 无需确认完成”单选,确认完成使用“内部人员”复选框。 - 回单节点的操作要求包含是否确认、确认完成,是否确认提供“是 / 无需确认完成”单选,确认完成使用“内部人员”复选框。
- 回单节点的“内部人员”复选框只读,不接受手工勾选:是否确认为“是”时自动勾选(即回单需内部人员确认),为“无需确认完成”时自动取消勾选。
- 各节点中是否打卡、上传货量、上传凭证均使用单选或复选输入,控件类型与截图保持一致。 - 各节点中是否打卡、上传货量、上传凭证均使用单选或复选输入,控件类型与截图保持一致。
- 异常与边界: - 异常与边界:
@@ -233,6 +234,7 @@
- 不允许非在途节点配置打卡频次和打卡时段。 - 不允许非在途节点配置打卡频次和打卡时段。
- 不允许是否打卡为否时仍强制填写打卡定位或打卡时段。 - 不允许是否打卡为否时仍强制填写打卡定位或打卡时段。
- 不允许上传凭证关闭后仍提交凭证类型。 - 不允许上传凭证关闭后仍提交凭证类型。
- 不允许手工勾选或取消回单节点的“内部人员”复选框。
--- ---