🔀 合并 websoft/dev,统一待回单确认与公路配载校验

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-24 14:55:04 +08:00
co-authored by Cursor
15 changed files with 272 additions and 16750 deletions
@@ -1200,7 +1200,11 @@
:loading="taskDriverLoading"
:disabled="dialogReadonly"
@select="handleTaskDriverSelect"
/>
>
<template #default="{ item }">
<span>{{ driverOptionText(item) }}</span>
</template>
</el-autocomplete>
</el-form-item>
<el-form-item
v-if="isRoadTransport"
@@ -1279,7 +1283,11 @@
:loading="taskEscortLoading"
:disabled="dialogReadonly"
@select="handleTaskEscortSelect"
/>
>
<template #default="{ item }">
<span>{{ driverOptionText(item) }}</span>
</template>
</el-autocomplete>
</el-form-item>
<el-form-item v-if="showRoadEscortFields" label="押运人手机号">
<el-input
@@ -1591,8 +1599,15 @@
<el-link type="primary" v-if="canCancel(row)" @click="handleAction('cancel', row)">
取消
</el-link>
<el-link type="primary" v-if="canComplete(row)" @click="handleCompleteAction(row)">
{{ completeActionLabelFor(row) }}
<el-link type="primary" v-if="canComplete(row)" @click="handleAction('complete', row)">
{{ completeActionLabel }}
</el-link>
<el-link
type="primary"
v-if="canConfirmReturn(row)"
@click="handleAction('confirmReturn', row)"
>
确认回单
</el-link>
<el-link type="primary" v-if="canMaintainMileage(row)" @click="openMileageDialog(row)">
维护里程
@@ -1653,7 +1668,11 @@
style="width: 100%"
@select="handleReassignDriverSelect"
@change="handleReassignDriverChange"
/>
>
<template #default="{ item }">
<span>{{ driverOptionText(item) }}</span>
</template>
</el-autocomplete>
</el-form-item>
<el-form-item
label="手机号"
@@ -3683,6 +3702,7 @@ import {
getVoucherImages as getProcessConfigVoucherImages,
} from '@/api/business/process-config';
import {
getList as getWaybillList,
getPunchRecords as getWaybillPunchRecords,
locateVehicle,
savePunchRecord,
@@ -3692,9 +3712,7 @@ import VoucherExecutionDetail from './voucher-execution-detail.vue';
import PunchRecordPhotos from './punch-record-photos.vue';
import AddressMapPicker from '@/components/address-map-picker/main.vue';
import { getList as getDriverList } from '@/api/transportCapacity/driver';
import {
fetchDriversByCarrierOrganizations,
} from '@/utils/carrier-org-resource';
import { fetchDriverCandidates, driverOptionText } from '@/utils/carrier-org-resource';
import { getDictionary } from '@/api/system/dictbiz';
import { getDictionary as getSystemDictionary } from '@/api/system/dict';
import { addressTypeOptions } from '@/option/base/common-address';
@@ -4370,9 +4388,15 @@ export default {
addBtn: this.canCreate,
};
},
// 打卡记录编辑入口与运单编辑同权限:能改运单的人才能订正它的打卡流水
// 打卡记录编辑入口与运单编辑同权限:能改运单的人才能订正它的打卡流水。
// 时间轴的「编辑打卡 / 补卡」链接是本页唯一写入入口,提交固定 punched=true。
// 取消打卡不在本页;待回单确认的运单不显示该链接。
canEditWaybillPunchRecord() {
return this.hasPermission(`${this.config.permission}_edit`) && !this.readonlyScope;
return (
this.hasPermission(`${this.config.permission}_edit`) &&
!this.readonlyScope &&
this.statusValue(this.detailRow) !== 'pending_return_confirm'
);
},
waybillPunchEditNode() {
return this.findWaybillPunchEditNodeConfig(this.waybillPunchEditRecord);
@@ -4800,6 +4824,7 @@ export default {
},
methods: {
formatContractOptionLabel,
driverOptionText,
parseRouteChangeContent,
// 页面被 keep-alive 缓存/卸载时调用,关闭本页所有 append-to-body 的弹窗,
// 避免 Teleport 出去的 DOM 残留在 body 上盖住后续页面(详见 data 里的说明)。
@@ -5099,7 +5124,8 @@ export default {
return false;
}
const status = this.statusValue(row);
return status === 'pending';
// 待回单确认可取消,取消不生成应收应付
return status === 'pending' || status === 'pending_return_confirm';
},
canComplete(row) {
if (
@@ -5111,29 +5137,17 @@ export default {
return false;
}
const status = this.statusValue(row);
return status === 'processing' || status === 'running' || status === 'pending_return';
// 待回单确认也可直接完成,不经确认回单
return status === 'processing' || status === 'running' || status === 'pending_return_confirm';
},
completeActionLabelFor(row) {
return this.statusValue(row) === 'pending_return' ? '确认回单' : this.completeActionLabel;
},
handleCompleteAction(row) {
const isPendingReturn = this.statusValue(row) === 'pending_return';
const actionName = this.completeActionLabelFor(row);
this.$confirm(`确定${actionName}该${this.config.title}?`, '提示', {
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);
});
canConfirmReturn(row) {
return (
this.hasAction('confirmReturn') &&
this.hasPermission(`${this.config.permission}_complete`) &&
!this.readonlyScope &&
!row.readonly &&
this.statusValue(row) === 'pending_return_confirm'
);
},
canMaintainMileage(row) {
return (
@@ -6565,7 +6579,7 @@ export default {
if (status === 'completed') {
return 'primary';
}
if (status === 'pending_return') {
if (status === 'pending_return_confirm') {
return 'warning';
}
return 'warning';
@@ -7967,19 +7981,10 @@ export default {
this.taskDriverOptions = [];
this.taskEscortOptions = [];
},
getTaskCarrierFilter() {
return {
carrierId: this.form.carrierId || '',
carrierName: this.form.carrierName || '',
};
},
loadTaskDriverOptions() {
if (!this.taskInfoFormEnabled || this.taskDriverLoading) return Promise.resolve([]);
this.taskDriverLoading = true;
return fetchDriversByCarrierOrganizations(
{ size: 9999, posts: '司机' },
this.getTaskCarrierFilter()
)
return fetchDriverCandidates({ size: 9999, posts: '司机' })
.then(records => {
this.taskDriverOptions = records;
return this.taskDriverOptions;
@@ -7990,16 +7995,8 @@ export default {
},
fetchTaskDriverSuggestions(queryString, callback) {
const keyword = String(queryString || '').trim();
const carrier = this.getTaskCarrierFilter();
if (!carrier.carrierId && !carrier.carrierName) {
callback([]);
return;
}
this.taskDriverLoading = true;
fetchDriversByCarrierOrganizations(
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' },
carrier
)
fetchDriverCandidates({ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' })
.then(records => {
this.taskDriverOptions = records;
callback(
@@ -8019,16 +8016,12 @@ export default {
},
fetchTaskEscortSuggestions(queryString, callback) {
const keyword = String(queryString || '').trim();
const carrier = this.getTaskCarrierFilter();
if (!carrier.carrierId && !carrier.carrierName) {
callback([]);
return;
}
this.taskEscortLoading = true;
fetchDriversByCarrierOrganizations(
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '押运员' },
carrier
)
fetchDriverCandidates({
size: 20,
...(keyword ? { driverName: keyword } : {}),
posts: '押运员',
})
.then(records => {
this.taskEscortOptions = records;
callback(records.map(item => ({ ...item, value: item.driverName || item.name || '' })));
@@ -10175,8 +10168,14 @@ export default {
cancel: '取消',
complete: this.completeActionLabel,
reassign: '重新派单',
confirmReturn: '确认回单',
}[action];
this.$confirm(`确定${actionName}该${this.config.title}?`, '提示', {
// 确认回单是终态动作(本轮不做撤销),把后果写在提示里,别让人以为还能退回
const confirmText =
action === 'confirmReturn'
? '确认回单后该运单将置为已完成,且不可撤销。是否继续?'
: `确定${actionName}该${this.config.title}?`;
this.$confirm(confirmText, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
@@ -10229,20 +10228,8 @@ export default {
},
fetchReassignDriverSuggestions(queryString, callback) {
const keyword = String(queryString || '').trim();
const row = this.reassignDrawer.row || {};
const carrier = {
carrierId: row.carrierId || '',
carrierName: row.carrierName || '',
};
if (!carrier.carrierId && !carrier.carrierName) {
callback([]);
return;
}
this.reassignDrawer.driverLoading = true;
fetchDriversByCarrierOrganizations(
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' },
carrier
)
fetchDriverCandidates({ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' })
.then(records => {
this.reassignDriverOptions = records;
callback(
@@ -10382,11 +10369,11 @@ export default {
return;
}
const pendingReturnCount = this.selectionList.filter(
row => this.statusValue(row) === 'pending_return'
row => this.statusValue(row) === 'pending_return_confirm'
).length;
const tip =
pendingReturnCount > 0
? `确定完成选中的运单?其中 ${pendingReturnCount} 条为待确认回单,确认后将转为已完成。`
? `确定完成选中的运单?其中 ${pendingReturnCount} 条为待回单确认,确认后将转为已完成。`
: '确定完成选中的运单?';
this.$confirm(tip, '提示', {
confirmButtonText: '确定',
@@ -10402,25 +10389,38 @@ export default {
this.onLoad(this.page, this.query);
});
},
handleRoadLoading() {
async handleRoadLoading() {
if (!this.selectionList.length) {
this.$message.warning('请选择至少一条数据');
return;
}
const nonRoadWaybills = this.selectionList.filter(row => !this.waybillIsRoadTransport(row));
if (nonRoadWaybills.length) {
this.$message.warning('公路配载仅支持运输方式为“公路运输”的运单');
const selectedIds = this.selectionList.map(row => row.id).filter(Boolean);
if (!selectedIds.length) {
this.$message.warning('请选择至少一条数据');
return;
}
const unavailableWaybills = this.selectionList.filter(
row =>
!['pending', 'processing', 'running'].includes(this.statusValue(row)) ||
row.loadingId ||
row.loadingManageId ||
row.loadingNo
);
if (unavailableWaybills.length) {
this.$message.warning('公路配载仅支持待执行/进行中且未关联有效配载单的运单');
// 可配载与否只看后端查询结果
let loadableIds;
try {
const res = await getWaybillList(1, selectedIds.length, {
loadableOnly: 1,
ids: selectedIds.join(','),
});
loadableIds = new Set(extractRecords(res).map(row => String(row.id)));
} catch (error) {
this.$message.error('校验运单可配载状态失败,请重试');
return;
}
const rejectedRows = this.selectionList.filter(row => !loadableIds.has(String(row.id)));
if (rejectedRows.length) {
const rejectedText = rejectedRows
.map(row => {
const reason = this.roadLoadingRejectReasonText(row);
const no = row.waybillNo || row.id || '';
return `${no}(${reason})`;
})
.join('、');
this.$message.warning(`${rejectedText};公路配载仅支持公路、待执行且未配载的运单`);
return;
}
try {
@@ -10443,6 +10443,13 @@ export default {
},
});
},
// 只给后端判定为不可配载的运单生成可读原因,本身不参与准入判定
roadLoadingRejectReasonText(row = {}) {
if (!this.waybillIsRoadTransport(row)) return '非公路运输';
if (String(row.businessStatus || '') !== 'pending') return '状态不是待执行';
if (row.loadingNo) return '已关联配载单';
return '运单数据可能已变化,请刷新后重试';
},
},
};
</script>