完善变更记录、客商评分与车辆表单

变更记录详情统一为居中分页弹窗,并去掉经办人等信息行。
客商评分支持删除、提交前自评校验、复评后才能审批,以及 0 分提交。
车辆外廓尺寸和载质量单位改到输入框内,认证审核标签按四字换行,使用部门必填并默认当前部门。
临时额度的 -1 占位金额显示为空。新增项目编号可留空由系统生成。
修复缓存总单页在打开新增项目时误请求缺少 id 的详情接口。
详情页状态与运输方式标签样式统一。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-22 20:13:44 +08:00
parent 3a8442e522
commit ce1ce02551
13 changed files with 532 additions and 329 deletions
@@ -1,7 +1,17 @@
<template>
<div v-loading="loading" class="master-detail">
<section v-if="master" class="detail-overview">
<div class="detail-heading"><div><h2>多联总单详情 <span>|</span> {{ master.masterNo }}</h2><el-tag :type="statusType(master.businessStatus)" class="status-text">{{ statusName(master.businessStatus) }}</el-tag><el-tag v-if="transportFlowLabel" type="info" class="transport-flow-tag">{{ transportFlowLabel }}</el-tag></div></div>
<div class="detail-heading">
<div>
<h2>多联总单详情 <span>|</span> {{ master.masterNo }}</h2>
<el-tag :type="statusType(master.businessStatus)">{{
statusName(master.businessStatus)
}}</el-tag>
<el-tag v-if="transportFlowLabel" type="primary" class="transport-flow-tag">{{
transportFlowLabel
}}</el-tag>
</div>
</div>
<dl class="detail-meta">
<div>
<dt>客户</dt>
@@ -132,6 +142,7 @@ export default {
dateRange() { return this.master?.planStartTime && this.master?.planEndTime ? `${String(this.master.planStartTime).slice(0, 10)} ~ ${String(this.master.planEndTime).slice(0, 10)}` : '-'; },
},
async mounted() {
if (!this.id) return;
this.loading = true;
try {
const res = await api.getDetail(this.id);
@@ -282,8 +293,36 @@ export default {
<style scoped lang="scss">
.master-detail { padding-bottom: 20px; color: #303133; }
.detail-overview, .execution-detail-card { margin-bottom: 8px; border: 1px solid #eff1f7; background: #fff; }
.detail-heading { display: flex; align-items: center; justify-content: space-between; padding: 18px 24px; h2 { display: inline-block; margin: 0 16px 0 0; font-size: 20px; } h2 span { margin: 0 8px; color: #909399; font-weight: 400; } }
.transport-flow-tag { margin-left: 8px; }
.detail-heading {
display: flex;
align-items: center;
justify-content: space-between;
padding: 18px 24px;
h2 {
display: inline-block;
margin: 0 16px 0 0;
font-size: 20px;
}
h2 span {
margin: 0 8px;
color: #909399;
font-weight: 400;
}
:deep(.el-tag) {
height: 28px;
padding: 0 12px;
border: 0;
border-radius: 8px;
font-size: 13px;
line-height: 28px;
}
}
.transport-flow-tag {
margin-left: 8px;
}
.detail-meta { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 18px 24px; margin: 0; padding: 0 24px 20px; dt { margin-bottom: 6px; color: #909399; font-size: 13px; } dd { display: flex; flex-wrap: wrap; gap: 8px 16px; margin: 0; color: #303133; font-size: 14px; word-break: break-all; } :deep(.el-link) { font-size: 14px; } }
.route-map { display: flex; align-items: flex-start; padding: 16px 24px 20px; border-top: 1px solid #eff1f7; overflow-x: auto; }
.route-map__node { display: grid; flex: 0 0 150px; justify-items: center; gap: 6px; text-align: center; strong { font-size: 16px; white-space: nowrap; } small { color: #606266; white-space: nowrap; } }
@@ -295,6 +295,7 @@ export default {
},
methods: {
async load() {
if (!this.id) return;
this.loading = true;
try {
const res = await api.getDetail(this.id);
@@ -1774,12 +1774,17 @@
<div class="waybill-manage-page__waybill-heading">
<strong>运单详情</strong>
<span>{{ detailRow.waybillNo || '-' }}</span>
<span class="detail-status-text status-text-success">{{
displayStatus(detailRow, 'businessStatus')
}}</span>
<span class="detail-status-text detail-transport-type">
{{ getTransportTypeLabel(waybillTransportMode(detailRow)) || '-' }}
</span>
<el-tag :type="statusTagType(detailRow.businessStatus)">
{{ displayStatus(detailRow, 'businessStatus') }}
</el-tag>
<el-tag type="primary">
{{
detailRow.transportTypeName ||
getTransportTypeLabel(detailRow.transportType) ||
waybillTransportMode(detailRow) ||
'-'
}}
</el-tag>
<el-link
v-if="detailRow.id"
class="waybill-manage-page__waybill-route-change-link"
@@ -2184,7 +2189,11 @@
/>
</div>
</el-tab-pane>
<el-tab-pane label="司机上传" name="driverUpload">
<el-tab-pane
v-if="waybillDriverUploads.length"
label="司机上传"
name="driverUpload"
>
<div
v-loading="waybillPunchRecordsLoading"
class="waybill-manage-page__driver-upload-grid"
@@ -2201,11 +2210,6 @@
{{ photo.label || '凭证' }}
</div>
</div>
<el-empty
v-if="!waybillPunchRecordsLoading && !waybillDriverUploads.length"
description="暂无司机上传数据"
:image-size="50"
/>
</div>
</el-tab-pane>
</el-tabs>
@@ -5265,7 +5269,9 @@ export default {
: defaultText;
},
statusTagType(status) {
if ([1, 'pending', 'processing', 'approved', 'change_approved'].includes(status)) {
if (
[1, 'pending', 'processing', 'running', 'approved', 'change_approved'].includes(status)
) {
return 'success';
}
if ([2, 'cancelled', 'withdrawn', 'draft'].includes(status)) {
@@ -9763,6 +9769,15 @@ export default {
strong {
font-size: 20px;
}
:deep(.el-tag) {
height: 28px;
padding: 0 12px;
border: 0;
border-radius: 8px;
font-size: 13px;
line-height: 28px;
}
}
&__waybill-route-change-link {
+12 -57
View File
@@ -966,44 +966,10 @@
@close="attachmentImagePreviewVisible = false"
/>
<el-dialog
<change-record-detail-dialog
v-model="detailChangeRecordVisible"
title="变更记录详情"
append-to-body
destroy-on-close
width="1100px"
top="10px"
class="contract-change-record-detail-dialog"
>
<div v-if="detailChangeRecord" class="contract-change-record-detail-meta">
<span>经办人:{{ detailChangeRecord.handlerUserName || '-' }}</span>
<span>变更类型:{{ detailChangeRecord.changeType || '-' }}</span>
<span>状态:{{ detailChangeRecord.statusName || detailChangeRecord.status || '-' }}</span>
</div>
<el-table :data="detailChangeRecordDetailRows" border :show-overflow-tooltip="false">
<el-table-column prop="field" label="变更字段" min-width="180" />
<el-table-column
prop="before"
label="变更前"
min-width="360"
class-name="contract-change-record-detail-value"
/>
<el-table-column
prop="after"
label="变更后"
min-width="500"
class-name="contract-change-record-detail-value"
/>
</el-table>
<el-empty
v-if="!detailChangeRecordDetailRows.length"
description="暂无变更内容"
:image-size="60"
/>
<template #footer>
<el-button type="primary" @click="detailChangeRecordVisible = false">关闭</el-button>
</template>
</el-dialog>
:rows="detailChangeRecordDetailRows"
/>
<billing-plan-editor
v-model="detailBillingPlanBox"
@@ -1042,6 +1008,7 @@ import { getToken } from '@/utils/auth';
import { downloadFileByUrl, downloadXls } from '@/utils/util';
import { submitMkApprovalFlow } from '@/utils/mk-approval';
import BillingPlanEditor from './components/billing-plan-editor.vue';
import ChangeRecordDetailDialog from '@/components/change-record-detail-dialog/main.vue';
import ContractAttachmentSection, {
attachmentName as sharedAttachmentName,
attachmentUrl as sharedAttachmentUrl,
@@ -1194,7 +1161,14 @@ const attachmentViewerPlugins = [
export default {
name: 'ContractManage',
components: { ContractAttachmentSection, BillingPlanEditor, ElImageViewer, OpenFileViewer, PdfPreview },
components: {
ContractAttachmentSection,
BillingPlanEditor,
ChangeRecordDetailDialog,
ElImageViewer,
OpenFileViewer,
PdfPreview,
},
data() {
return {
api,
@@ -2969,25 +2943,6 @@ export default {
word-break: break-word;
}
.contract-change-record-detail-meta {
display: flex;
flex-wrap: wrap;
gap: 8px 32px;
margin-bottom: 16px;
color: #606266;
}
:deep(.contract-change-record-detail-dialog .el-dialog__body) {
max-height: 65vh;
overflow: auto;
padding-top: 12px;
}
:deep(.contract-change-record-detail-dialog .contract-change-record-detail-value .cell) {
white-space: pre-wrap;
word-break: break-all;
}
:global(.avue--collapse .contract-manage-page__footer) {
left: 60px;
}
+16 -12
View File
@@ -395,14 +395,8 @@
<div class="loading-detail__summary">
<div class="loading-detail__heading">配载单详情</div>
<div>{{ dialogForm.loadingNo || '-' }}</div>
<div>
<el-tag :type="detailStatusType" class="status-text">{{
statusText(dialogForm)
}}</el-tag>
</div>
<div>
<el-tag type="warning">{{ transportTypeLabel(dialogForm.transportType) }}</el-tag>
</div>
<el-tag :type="detailStatusType">{{ statusText(dialogForm) }}</el-tag>
<el-tag type="primary">{{ transportTypeLabel(dialogForm.transportType) }}</el-tag>
</div>
<div class="loading-detail__task-row">
<div class="loading-detail__field">
@@ -1683,11 +1677,12 @@ export default {
const typeMap = {
pending: 'success',
running: 'success',
completed: 'info',
processing: 'success',
completed: 'primary',
cancelled: 'info',
draft: 'warning',
draft: 'info',
};
return typeMap[this.dialogForm.businessStatus] || 'info';
return typeMap[this.dialogForm.businessStatus] || 'warning';
},
taskSummary() {
return (
@@ -3604,8 +3599,17 @@ export default {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 24px;
gap: 12px;
padding: 4px 0 16px;
:deep(.el-tag) {
height: 28px;
padding: 0 12px;
border: 0;
border-radius: 8px;
font-size: 13px;
line-height: 28px;
}
}
.loading-detail__heading {
font-size: 20px;
+11 -1
View File
@@ -132,7 +132,7 @@
/>
</template>
<master-order-dispatch v-else-if="mode === 'dispatch'" :id="routeId" @back="goList" />
<master-order-detail v-else :id="routeId" />
<master-order-detail v-else-if="mode === 'detail'" :id="routeId" />
<el-dialog v-model="confirm.visible" title="提示" width="400px"
><span>{{ confirm.message }}</span
><template #footer
@@ -153,6 +153,10 @@ export default {
components: { MasterOrderEditor, MasterOrderDispatch, MasterOrderDetail },
data() {
return {
// 实例创建时锁定所属路由。keep-alive 缓存后 $route 会变成其它页面,
// 不能再用全局 query.mode / query.id 决定本页形态,否则打开「新增项目管理」
//mode=add 且无 id)会落入详情分支并请求缺少 id 的详情接口。
routePathLocked: this.$route.path,
searchExpanded: false,
loading: false,
records: [],
@@ -180,10 +184,15 @@ export default {
};
},
computed: {
isOwnedRoute() {
return this.$route.path === this.routePathLocked;
},
mode() {
if (!this.isOwnedRoute) return 'list';
return this.$route.query.mode || 'list';
},
routeId() {
if (!this.isOwnedRoute) return '';
return this.$route.query.id;
},
masterEditorTitle() {
@@ -194,6 +203,7 @@ export default {
'$route.query': {
immediate: true,
handler() {
if (!this.isOwnedRoute) return;
this.syncTagTitle();
if (this.mode === 'list') this.load();
},
+8 -58
View File
@@ -196,7 +196,7 @@
<el-form-item label="项目编号" prop="projectCode">
<el-input
v-model="form.projectCode"
placeholder="请输入"
:placeholder="dialogType === 'add' ? '若不填写,系统自动生成' : '请输入'"
:disabled="dialogType === 'edit'"
/>
</el-form-item>
@@ -703,6 +703,7 @@
label="变更内容"
min-width="180"
align="center"
:show-overflow-tooltip="false"
>
<template #default="{ row }">
<el-tooltip placement="top" :show-after="200">
@@ -730,43 +731,10 @@
</el-table-column>
</el-table>
<el-dialog
<change-record-detail-dialog
v-model="changeRecordDetailVisible"
title="变更记录详情"
append-to-body
destroy-on-close
width="1100px"
top="10px"
class="project-change-record-detail-dialog"
>
<div v-if="changeRecordDetail" class="project-change-record-detail-meta">
<span>变更日期:{{ changeRecordDetail.changeDate || '-' }}</span>
<span>变更账号:{{ changeRecordDetail.handler || '-' }}</span>
</div>
<el-table :data="changeRecordDetailRows" border :show-overflow-tooltip="false">
<el-table-column prop="field" label="变更字段" min-width="180" />
<el-table-column
prop="before"
label="变更前"
min-width="360"
class-name="project-change-record-detail-value"
/>
<el-table-column
prop="after"
label="变更后"
min-width="500"
class-name="project-change-record-detail-value"
/>
</el-table>
<el-empty
v-if="!changeRecordDetailRows.length"
description="暂无变更内容"
:image-size="60"
/>
<template #footer>
<el-button type="primary" @click="changeRecordDetailVisible = false">关闭</el-button>
</template>
</el-dialog>
:rows="changeRecordDetailRows"
/>
<template v-if="isChangeDialog">
<div class="dialog-section-title">变更原因</div>
@@ -947,6 +915,7 @@ import { submitMkApprovalFlow } from '@/utils/mk-approval';
import { ElImageViewer } from 'element-plus';
import { OpenFileViewer } from '@open-file-viewer/vue';
import PdfPreview from '@/components/pdf-preview/main.vue';
import ChangeRecordDetailDialog from '@/components/change-record-detail-dialog/main.vue';
import {
fallbackPlugin,
imagePlugin,
@@ -1066,6 +1035,7 @@ export default {
name: 'ProjectApply',
components: {
CustomerArchive: defineAsyncComponent(() => import('@/views/vehicle/customer-archive.vue')),
ChangeRecordDetailDialog,
ElImageViewer,
OpenFileViewer,
PdfPreview,
@@ -1520,6 +1490,7 @@ export default {
this.fillDefaultUsers();
return;
}
if (!id) return;
this.api.getDetail(id).then(res => {
const detail = res.data.data || {};
const isChange = type === 'change';
@@ -2999,27 +2970,6 @@ export default {
}
}
:deep(.project-change-record-detail-dialog .el-dialog__body) {
max-height: 65vh;
overflow: auto;
}
:deep(.project-change-record-detail-dialog .project-change-record-detail-value .cell) {
white-space: pre-wrap;
word-break: break-word;
overflow: visible;
text-overflow: clip;
line-height: 1.6;
}
.project-change-record-detail-meta {
display: flex;
gap: 32px;
margin-bottom: 16px;
color: #606266;
font-size: 14px;
}
:global(.project-apply-dialog .el-dialog__body) {
max-height: 76vh;
overflow-y: auto;
+32 -10
View File
@@ -610,7 +610,12 @@ export default {
return value;
},
applyDetail(detail) {
this.form = { ...detail };
this.form = {
...detail,
projectFundLimit: this.blankSentinelAmount(detail.projectFundLimit),
usedFundLimit: this.blankSentinelAmount(detail.usedFundLimit),
remainingFundLimit: this.blankSentinelAmount(detail.remainingFundLimit),
};
this.selectedProjectId = detail.projectId || '';
this.attachmentRows = this.parseJsonArray(detail.attachmentsJson);
this.selectedAttachments = [];
@@ -620,6 +625,9 @@ export default {
const { projectQuotaInfoTitle, temporaryCreditInfoTitle, ...payload } = row || {};
return {
...payload,
projectFundLimit: this.toOptionalAmount(payload.projectFundLimit),
usedFundLimit: this.toOptionalAmount(payload.usedFundLimit),
remainingFundLimit: this.toOptionalAmount(payload.remainingFundLimit),
applyLimit: payload.applyLimit === '' ? '' : Number(payload.applyLimit),
attachmentsJson: JSON.stringify(this.attachmentRows),
};
@@ -765,8 +773,8 @@ export default {
projectName: project.projectName || '',
projectCode: project.projectCode || '',
undertakeDeptName: project.undertakeDeptName || '',
projectFundLimit: project.fundLimit || project.projectFundLimit || 0,
usedFundLimit: project.usedFundLimit || 0,
projectFundLimit: this.blankSentinelAmount(project.fundLimit ?? project.projectFundLimit),
usedFundLimit: this.blankSentinelAmount(project.usedFundLimit),
});
this.form.remainingFundLimit = this.calculateRemainingFundLimit();
getProjectDetail(project.id).then(res => {
@@ -774,18 +782,32 @@ export default {
Object.assign(this.form, {
projectCode: detail.projectCode || this.form.projectCode,
undertakeDeptName: detail.undertakeDeptName || this.form.undertakeDeptName,
projectFundLimit:
detail.fundLimit ?? detail.projectFundLimit ?? this.form.projectFundLimit,
usedFundLimit: detail.usedFundLimit ?? this.form.usedFundLimit,
projectFundLimit: this.blankSentinelAmount(
detail.fundLimit ?? detail.projectFundLimit ?? this.form.projectFundLimit
),
usedFundLimit: this.blankSentinelAmount(
detail.usedFundLimit ?? this.form.usedFundLimit
),
});
this.form.remainingFundLimit = this.calculateRemainingFundLimit();
});
},
blankSentinelAmount(value) {
if (value === undefined || value === null || value === '') return '';
return Number(value) === -1 ? '' : value;
},
toOptionalAmount(value) {
const amount = this.blankSentinelAmount(value);
return amount === '' ? null : amount;
},
calculateRemainingFundLimit() {
const total = Number(this.form.projectFundLimit);
const used = Number(this.form.usedFundLimit);
if (!Number.isFinite(total) || !Number.isFinite(used)) return '';
return Math.round((total - used + Number.EPSILON) * 100) / 100;
const total = this.blankSentinelAmount(this.form.projectFundLimit);
const used = this.blankSentinelAmount(this.form.usedFundLimit);
if (total === '' || used === '') return '';
const totalAmount = Number(total);
const usedAmount = Number(used);
if (!Number.isFinite(totalAmount) || !Number.isFinite(usedAmount)) return '';
return Math.round((totalAmount - usedAmount + Number.EPSILON) * 100) / 100;
},
handleAmountInput(value) {
const text = String(value || '').replace(/[^\d.]/g, '');