Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 74988c0b5e | |||
| 4c117d87a5 | |||
| d6bcb12c20 | |||
| e1d87f5788 | |||
| 56b8f68449 | |||
| 0ac17dea0b | |||
| 39e3115fbe | |||
| 9c0f7cb3f4 | |||
| 7b478bdbf2 | |||
| 87d6ff97a3 | |||
| e3bd44e8fc | |||
| 324daf9d4d | |||
| 37ac683fb4 | |||
| c402d68aea | |||
| e8f6311477 | |||
| fa78a68df6 | |||
| 320422856c | |||
| 0add4c5cdc | |||
| 372e8826a2 | |||
| 92a8d4aab1 | |||
| 59a1ca8a0d | |||
| 5544cfb3ca | |||
| fcd5ef7df9 | |||
| 489b406214 | |||
| 353148eb7f | |||
| 293b4fc6ce | |||
| 53b588063e | |||
| 0889f29c59 | |||
| f1c004813e | |||
| d67cf55491 | |||
| 6384e80758 | |||
| d3af455374 | |||
| 1622015724 | |||
| f7d8fbf16b | |||
| 847f09b14d | |||
| 2ae817ec38 | |||
| 6d80ffde6c | |||
| 44809ad303 | |||
| 91cb3ee8c2 | |||
| b2a6d73bac |
@@ -24,11 +24,16 @@ export const getDetail = id =>
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getCarrierContracts = projectIds =>
|
export const getCarrierContracts = (projectIds, waybillIds = []) =>
|
||||||
request({
|
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 =>
|
||||||
|
|||||||
@@ -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`,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -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',
|
||||||
|
|||||||
@@ -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' },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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'],
|
||||||
|
|||||||
@@ -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: '所属组织',
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 = {},
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
|||||||
+181
-91
@@ -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="节点联动"
|
|
||||||
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
|
<el-button type="primary" size="small" @click="openDataScopeSetting">设置</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.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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
},
|
},
|
||||||
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>
|
||||||
|
|||||||
@@ -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%');
|
||||||
|
|||||||
@@ -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 !== '自运') {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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-tooltip
|
||||||
|
:content="
|
||||||
|
formatOrgCascaderPath(organizationCascaderPath) ||
|
||||||
|
formatOrgDisplayName(vehicleForm.organizationName)
|
||||||
|
"
|
||||||
|
placement="top"
|
||||||
|
:disabled="
|
||||||
|
!(
|
||||||
|
formatOrgCascaderPath(organizationCascaderPath) ||
|
||||||
|
formatOrgDisplayName(vehicleForm.organizationName)
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="org-cascader-tooltip">
|
||||||
<el-cascader
|
<el-cascader
|
||||||
v-model="organizationCascaderValue"
|
v-model="organizationCascaderValue"
|
||||||
:options="organizationTreeOptions"
|
:options="organizationFormTreeOptions"
|
||||||
:props="organizationCascaderProps"
|
:props="formOrgCascaderProps"
|
||||||
filterable
|
filterable
|
||||||
clearable
|
clearable
|
||||||
placeholder="请选择"
|
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-tooltip
|
||||||
|
:content="
|
||||||
|
formatOrgCascaderPath(useDepartmentCascaderPath) ||
|
||||||
|
formatOrgDisplayName(vehicleForm.useDepartment)
|
||||||
|
"
|
||||||
|
placement="top"
|
||||||
|
:disabled="
|
||||||
|
!(
|
||||||
|
formatOrgCascaderPath(useDepartmentCascaderPath) ||
|
||||||
|
formatOrgDisplayName(vehicleForm.useDepartment)
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="org-cascader-tooltip">
|
||||||
<el-cascader
|
<el-cascader
|
||||||
v-model="useDepartmentCascaderValue"
|
v-model="useDepartmentCascaderValue"
|
||||||
:options="organizationTreeOptions"
|
:options="useDepartmentFormTreeOptions"
|
||||||
:props="organizationCascaderProps"
|
:props="formOrgCascaderProps"
|
||||||
filterable
|
filterable
|
||||||
clearable
|
clearable
|
||||||
placeholder="请选择"
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
? ''
|
|
||||||
: data.maxMinusScore;
|
|
||||||
this.calculateScore(this.currentScore);
|
this.calculateScore(this.currentScore);
|
||||||
});
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
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)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -214,6 +214,7 @@
|
|||||||
- 到场、装货、发货、在途、到货、卸货、签收节点的操作要求包含是否打卡、打卡定位;装货、发货、到货、卸货、签收还包含上传货量和上传货量类型;附件要求包含上传凭证和凭证类型。
|
- 到场、装货、发货、在途、到货、卸货、签收节点的操作要求包含是否打卡、打卡定位;装货、发货、到货、卸货、签收还包含上传货量和上传货量类型;附件要求包含上传凭证和凭证类型。
|
||||||
- 在途节点额外包含打卡频次和打卡时段,打卡频次使用普通输入框,打卡时段使用时间选择器。
|
- 在途节点额外包含打卡频次和打卡时段,打卡频次使用普通输入框,打卡时段使用时间选择器。
|
||||||
- 回单节点的操作要求包含是否确认、确认完成,是否确认提供“是 / 无需确认完成”单选,确认完成使用“内部人员”复选框。
|
- 回单节点的操作要求包含是否确认、确认完成,是否确认提供“是 / 无需确认完成”单选,确认完成使用“内部人员”复选框。
|
||||||
|
- 回单节点的“内部人员”复选框只读,不接受手工勾选:是否确认为“是”时自动勾选(即回单需内部人员确认),为“无需确认完成”时自动取消勾选。
|
||||||
- 各节点中是否打卡、上传货量、上传凭证均使用单选或复选输入,控件类型与截图保持一致。
|
- 各节点中是否打卡、上传货量、上传凭证均使用单选或复选输入,控件类型与截图保持一致。
|
||||||
|
|
||||||
- 异常与边界:
|
- 异常与边界:
|
||||||
@@ -233,6 +234,7 @@
|
|||||||
- 不允许非在途节点配置打卡频次和打卡时段。
|
- 不允许非在途节点配置打卡频次和打卡时段。
|
||||||
- 不允许是否打卡为否时仍强制填写打卡定位或打卡时段。
|
- 不允许是否打卡为否时仍强制填写打卡定位或打卡时段。
|
||||||
- 不允许上传凭证关闭后仍提交凭证类型。
|
- 不允许上传凭证关闭后仍提交凭证类型。
|
||||||
|
- 不允许手工勾选或取消回单节点的“内部人员”复选框。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user