✨ 打卡详情展示异常状态并新增异常节点展示
运单详情「打卡详情」时间轴按节点关联异常处置:存在未了结异常时显示「异常」标签 与「跟进」链接,全部处理完成时显示「已处理异常」与「详情」链接。异常跟节点走, 节点未打卡也可能有异常。 异常来源改用异常处置表而非打卡行的 exception_flag —— 后者由打卡接口写入且未随 异常上报维护,实测有异常的节点该值仍为 0,原「异常」标签从未显示过。 「已处理」的口径与后端打卡门禁一致(已完成、已撤销都放行),故「处理中」仍算 未处理。 异常节点字段补入各处异常信息:运单详情「异常上报」表格、编辑弹窗、跟进/详情 弹窗,统一放在异常类型之前;表格内空节点显示「-」而不是留白(存量异常无节点)。 过程节点列显示配置中的中文名而非原始 key(库里混存 key 与中文名两种格式)。 删除打卡详情中在途节点的「在途」标签 —— 节点名已表明是在途。
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user