✨ 打卡详情展示异常状态并新增异常节点展示

运单详情「打卡详情」时间轴按节点关联异常处置:存在未了结异常时显示「异常」标签
与「跟进」链接,全部处理完成时显示「已处理异常」与「详情」链接。异常跟节点走,
节点未打卡也可能有异常。

异常来源改用异常处置表而非打卡行的 exception_flag —— 后者由打卡接口写入且未随
异常上报维护,实测有异常的节点该值仍为 0,原「异常」标签从未显示过。

「已处理」的口径与后端打卡门禁一致(已完成、已撤销都放行),故「处理中」仍算
未处理。

异常节点字段补入各处异常信息:运单详情「异常上报」表格、编辑弹窗、跟进/详情
弹窗,统一放在异常类型之前;表格内空节点显示「-」而不是留白(存量异常无节点)。

过程节点列显示配置中的中文名而非原始 key(库里混存 key 与中文名两种格式)。

删除打卡详情中在途节点的「在途」标签 —— 节点名已表明是在途。
This commit is contained in:
2026-10-05 15:13:34 +08:00
parent 0e853ea751
commit 509f5e163c
5 changed files with 155 additions and 16 deletions
+19
View File
@@ -98,6 +98,24 @@ const getProcessConfigNodes = row => {
const isAcceptProcessNode = node => node.key === 'accept' || node.name === '接单';
/**
* 过程节点列显示文案。
*
* 该列在库里存的是「原始值」,且历史上写入了两种格式:司机打卡推进写的是配置的
* key(unload / dispatch / load …),配载单新建默认值写的是中文名(接单)。
* 故这里按两种格式都认一次,认不出就原样显示,不把值吞掉。
*
* 运单创建后、司机首次打卡前该字段为空,属正常——显示「-」,避免留白看不出是没值还是没渲染。
*/
const formatCurrentProcessNode = row => {
const value = String(getFirstValue(row, ['currentProcessNode', 'currentNode']) || '').trim();
if (!value) return '-';
const node = getProcessConfigNodes(row).find(
item => item.key === value || item.name === value
);
return node?.name || node?.key || value;
};
const isTruthyFlag = value => {
if (value === true || value === 1) return true;
if (value === false || value === 0 || value === null || value === undefined || value === '') {
@@ -608,6 +626,7 @@ export const option = {
{
label: '过程节点',
prop: 'currentProcessNode',
formatter: row => formatCurrentProcessNode(row),
minWidth: 140,
addDisplay: false,
editDisplay: false,
+2
View File
@@ -81,6 +81,7 @@ export const tableColumns = [
{ label: '上报时间', prop: 'reportTime', minWidth: 170 },
{ label: '异常类型', prop: 'exceptionTypeName', minWidth: 160 },
{ label: '上报说明', prop: 'reportDescription', minWidth: 180 },
{ label: '异常节点', prop: 'nodeName', minWidth: 120 },
{ label: '承运商', prop: 'carrierName', minWidth: 160 },
{ label: '最新跟进说明', prop: 'latestFollowContent', minWidth: 200 },
{ label: '状态', prop: 'disposalStatusName', minWidth: 120 },
@@ -100,6 +101,7 @@ export const detailWaybillFields = [
export const detailExceptionFields = [
{ label: '上报人', prop: 'reporterName' },
{ label: '上报时间', prop: 'reportTime' },
{ label: '异常节点', prop: 'nodeName' },
{ label: '异常类型', prop: 'exceptionTypeName' },
{ label: '异常原因', prop: 'exceptionReason' },
{ label: '上报说明', prop: 'reportDescription', span: 2 },
@@ -2061,6 +2061,11 @@
<el-table v-loading="waybillExceptionLoading" :data="waybillExceptionRows" border>
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column prop="reporterName" label="上报人" min-width="120" align="center" />
<el-table-column label="异常节点" min-width="120" align="center">
<template #default="{ row }">
{{ row.nodeName || '-' }}
</template>
</el-table-column>
<el-table-column
prop="exceptionTypeName"
label="异常类型"
@@ -2159,7 +2164,7 @@
<el-timeline v-if="waybillPunchRecords.length">
<el-timeline-item
v-for="(record, index) in orderedWaybillPunchRecords"
:key="`${record.nodeCode || record.id || 'punch'}-${index}`"
:key="waybillPunchRecordKey(record, index)"
:timestamp="record.punchTime || record.statusName || '未打卡'"
:type="record.punched ? 'primary' : 'info'"
:color="record.punched ? '#2b6ce8' : '#c0c4cc'"
@@ -2174,20 +2179,42 @@
effect="plain"
>{{ record.statusName || (record.punched ? '已打卡' : '未打卡') }}</el-tag
>
<el-tag
v-if="record.type === 'enroute'"
size="small"
type="info"
effect="plain"
>在途</el-tag
>
<el-tag
v-if="record.exceptionFlag"
size="small"
type="danger"
effect="plain"
>异常</el-tag
<template
v-if="waybillPunchRecordExceptions[waybillPunchRecordKey(record, index)]"
>
<el-tag
size="small"
:type="
waybillPunchRecordExceptions[waybillPunchRecordKey(record, index)]
.unfinished
? 'danger'
: 'success'
"
effect="plain"
>{{
waybillPunchRecordExceptions[waybillPunchRecordKey(record, index)]
.unfinished
? '异常'
: '已处理异常'
}}</el-tag
>
<el-link
class="waybill-manage-page__punch-record-exception-link"
type="primary"
:underline="false"
@click="
openWaybillPunchRecordException(
waybillPunchRecordExceptions[waybillPunchRecordKey(record, index)]
)
"
>{{
waybillPunchRecordExceptions[waybillPunchRecordKey(record, index)]
.unfinished
? '跟进'
: '详情'
}}</el-link
>
</template>
</div>
<template v-if="record.punched">
<div
@@ -2498,6 +2525,13 @@
}}</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="异常节点">
<span class="waybill-manage-page__exception-readonly">{{
waybillExceptionValue('nodeName')
}}</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="异常类型" prop="exceptionType">
<el-select
@@ -3508,6 +3542,9 @@ const standaloneWaybillFormPaths = [standaloneWaybillListRoute, standaloneWaybil
const standaloneWaybillDetailRoute = '/business/waybill-manage/detail';
const loadingCreateWaybillsStorageKey = 'loading-manage-create-waybills';
const waybillManageListRefreshKey = 'waybill-manage-list-refresh';
// 视为「已经处理完」的异常处置状态,与后端打卡门禁同一口径(已完成、已撤销都放行),
// 所以「处理中」仍算未完成,节点上继续显示异常。
const settledExceptionStatuses = ['completed', 'revoked'];
const attachmentViewerPlugins = [
imagePlugin(),
@@ -4072,6 +4109,7 @@ export default {
return [
{ label: '上报人', prop: 'reporterName' },
{ label: '上报时间', prop: 'reportTime' },
{ label: '异常节点', prop: 'nodeName' },
{ label: '异常类型', prop: 'exceptionTypeName' },
{ label: '异常原因', prop: 'exceptionReason' },
{ label: '上报说明', prop: 'reportDescription', span: 2 },
@@ -4124,6 +4162,47 @@ export default {
})
.map(item => item.record);
},
// 打卡记录 → 该节点的异常处置。异常跟节点走,不跟打卡走:节点未打卡也可能有异常。
// 数据源是异常处置表(waybillExceptionRows),不是打卡行上的 exception_flag
// ——后者由打卡接口写入且未随异常上报维护,实测有异常的节点它的值仍是 0。
// 匹配优先用 nodeCode,回退 nodeName:两者各有一批历史数据只填了一边。
waybillPunchRecordExceptions() {
const rows = Array.isArray(this.waybillExceptionRows) ? this.waybillExceptionRows : [];
const index = new Map();
rows.forEach(row => {
const nodeCode = String(row.nodeCode || '').trim().toLowerCase();
const nodeName = String(row.nodeName || '').trim();
if (nodeCode) {
if (!index.has(`code:${nodeCode}`)) index.set(`code:${nodeCode}`, []);
index.get(`code:${nodeCode}`).push(row);
}
if (nodeName) {
if (!index.has(`name:${nodeName}`)) index.set(`name:${nodeName}`, []);
index.get(`name:${nodeName}`).push(row);
}
});
const result = {};
(Array.isArray(this.waybillPunchRecords) ? this.waybillPunchRecords : []).forEach(
(record, position) => {
const nodeCode = String(record.nodeCode || '').trim().toLowerCase();
const nodeName = String(record.nodeName || '').trim();
const matched =
(nodeCode && index.get(`code:${nodeCode}`)) ||
(nodeName && index.get(`name:${nodeName}`)) ||
[];
if (!matched.length) return;
const isSettled = item => settledExceptionStatuses.includes(item.disposalStatus);
result[this.waybillPunchRecordKey(record, position)] = {
total: matched.length,
// 只要还有一条没处理完,这个节点就算「有事没完」,链接送去跟进
unfinished: matched.filter(item => !isSettled(item)).length,
// 取未完成的那条(没有则取第一条)作为跟进/详情的打开对象
target: matched.find(item => !isSettled(item)) || matched[0],
};
}
);
return result;
},
isStandaloneWaybillPage() {
return this.standaloneFormPage && standaloneWaybillFormPaths.includes(this.$route.path);
},
@@ -5483,6 +5562,16 @@ export default {
const fallback = new Date(text.replace(/-/g, '/')).getTime();
return Number.isNaN(fallback) ? null : fallback;
},
// 时间轴 key:与模板 v-for 的 :key 保持同一算法,异常索引也用它做键,
// 两边各写一份迟早会错位。
waybillPunchRecordKey(record, index) {
return `${record?.nodeCode || record?.id || 'punch'}-${index}`;
},
// 未处理完 → 跟进;全处理完 → 只读详情(与异常处置列表的操作口径一致)
openWaybillPunchRecordException(exception) {
if (!exception?.target) return;
this.openWaybillException(exception.target, exception.unfinished ? 'follow' : 'detail');
},
loadWaybillPunchRecords() {
const waybillId = this.detailRow?.id;
if (!waybillId || this.waybillPunchRecordsLoading) return;
@@ -11117,6 +11206,12 @@ export default {
color: #303133;
}
// 异常标签与文本链接紧挨在节点名右边,不推到行尾
&__punch-record-exception-link {
font-size: 13px;
font-weight: 400;
}
&__punch-record-line {
font-size: 13px;
color: #606266;
+18 -2
View File
@@ -429,7 +429,7 @@ const NODE_TEMPLATES = [
type: 'confirm',
supportVoucher: false,
},
{ key: 'arrive_scene', name: '到场', type: 'punch', enabled: false },
{ key: 'arrive_scene', name: '到场', type: 'punch', enabled: false, supportCargo: false },
{
key: 'load',
name: '装货',
@@ -485,6 +485,7 @@ const NODE_TEMPLATES = [
key: 'return',
name: '回单',
type: 'confirm',
supportCargo: false,
supportVoucher: true,
voucherOptions: ['磅单', '签收单'],
},
@@ -654,7 +655,9 @@ export default {
enabled: false,
punch: true,
location: true,
uploadCargo: true,
// 不支持货量的节点(到场/回单/接单等)不得默认勾上,否则会写出
// 「上传货量为是、货量类型为空」的配置,而这类节点在界面上没有货量开关可关
uploadCargo: !!template?.supportCargo,
cargoTypes: [],
uploadVoucher: undefined,
voucherTypes: [],
@@ -669,6 +672,10 @@ export default {
confirmInternal: false,
...template,
};
if (!node.supportCargo) {
node.uploadCargo = false;
node.cargoTypes = [];
}
if ((node.key === 'accept' || node.name === '接单') && node.confirmMode === 'yes') {
node.confirmDriver = true;
} else if ((node.key === 'accept' || node.name === '接单') && node.confirmMode !== 'yes') {
@@ -941,6 +948,15 @@ export default {
return false;
}
}
// 勾了上传货量却没选类型,在小程序侧等同于不采集货量;此处直接拦住,
// 避免保存出「显示采集、实际不采集」的配置
const cargoWithoutTypes = this.nodeRows.find(
item => item.enabled && item.supportCargo && item.uploadCargo && !(item.cargoTypes || []).length
);
if (cargoWithoutTypes) {
this.$message.warning(`「${cargoWithoutTypes.name}」已选择上传货量,请至少勾选一个货量类型`);
return false;
}
return true;
},
needsDuplicateVoucherConfirm() {
+7
View File
@@ -215,6 +215,13 @@
}}</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="异常节点">
<span class="exception-disposal-page__readonly">{{
detailValue('nodeName')
}}</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="异常类型" prop="exceptionType">
<el-select