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

变更记录详情统一为居中分页弹窗,并去掉经办人等信息行。
客商评分支持删除、提交前自评校验、复评后才能审批,以及 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
+207 -108
View File
@@ -144,9 +144,24 @@
>
撤回
</el-link>
<el-link
type="primary"
v-if="
hasPermission('customer_type_re_cert') &&
row.approvalStatus === 'reviewing' &&
row.reviewStatus !== '已完成'
"
@click="openReview(row)"
>
复评
</el-link>
<el-link
type="success"
v-if="hasPermission('customer_archive_approve') && row.approvalStatus === 'reviewing'"
v-if="
hasPermission('customer_archive_approve') &&
row.approvalStatus === 'reviewing' &&
row.reviewStatus === '已完成'
"
@click="handleApprove(row)"
>
审核通过
@@ -517,7 +532,7 @@
<span>{{ formatScoreValue(row.selfScore) }}</span>
</template>
</el-table-column>
<el-table-column label="复评得分" min-width="145" align="center">
<el-table-column v-if="archiveForm.id" label="复评得分" min-width="145" align="center">
<template #default="{ row }">
<span>{{ formatScoreValue(row.reviewScore) }}</span>
</template>
@@ -553,11 +568,18 @@
<span>{{ row.reviewStatus }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="135" fixed="right" align="center">
<el-table-column label="操作" width="160" fixed="right" align="center">
<template #default="{ row }">
<el-link type="primary" @click="openScore(row.__raw, row.__rawIndex)">
详情
</el-link>
<el-link
v-if="showScoreDelete"
type="danger"
@click="deleteScore(row.__rawIndex)"
>
删除
</el-link>
</template>
</el-table-column>
</el-table>
@@ -876,43 +898,10 @@
/>
</section-card>
<el-dialog
<change-record-detail-dialog
v-model="changeRecordDetailVisible"
title="变更记录详情"
append-to-body
destroy-on-close
width="1100px"
top="10px"
class="change-record-detail-dialog"
>
<div v-if="changeRecordDetail" class="change-record-detail-meta">
<span>变更日期:{{ changeRecordDetail.changeTime || '-' }}</span>
<span>变更账号:{{ changeRecordDetail.changeUserName || '-' }}</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="change-record-detail-value"
/>
<el-table-column
prop="after"
label="变更后"
min-width="500"
class-name="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"
/>
<div class="archive-form__footer" v-if="!isPublicViewPage">
<!-- 次要:独立页返回 / 只读关闭 / 弹窗取消 -->
@@ -1323,7 +1312,25 @@
class="score-detail-table score-detail-table--inline"
max-height="520"
>
<el-table-column label="评分项目" prop="itemName" width="150" align="center" />
<el-table-column
label="评分项目"
width="150"
align="center"
:show-overflow-tooltip="false"
>
<template #default="{ row: detail }">
<el-tooltip
:content="detail.itemName"
placement="top"
effect="dark"
append-to="body"
:show-after="200"
:disabled="!detail.itemName"
>
<span class="score-item-name">{{ detail.itemName }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column label="评分标准" min-width="430">
<template #default="{ row: detail }">
<div class="score-standard-cell">
@@ -1333,6 +1340,7 @@
inputmode="decimal"
:disabled="readonly"
@input="value => handleScoreValueInput(detail, value)"
@blur="handleScoreValueBlur(detail)"
>
<template #append>{{ getScoreRule(detail)?.changeUnit || '' }}</template>
</el-input>
@@ -1362,13 +1370,15 @@
<span>{{ formatScoreValue(getSignedScore(detail, 'selfScore')) }}分</span>
</template>
</el-table-column>
<el-table-column label="复评得分" width="150" align="center">
<el-table-column v-if="!isNewScore" label="复评得分" width="150" align="center">
<template #default="{ row: detail }">
<el-input
v-model="detail.reviewScore"
maxlength="10"
inputmode="decimal"
:disabled="readonly || !hasPermission('customer_type_re_cert')"
:disabled="
(!reviewMode && readonly) || !hasPermission('customer_type_re_cert')
"
@input="value => handleScoreDetailScoreInput(detail, 'reviewScore', value)"
>
<template
@@ -1389,7 +1399,7 @@
<template #default="{ row }">
<span class="score-category-table__name">{{ row.categoryName }}</span>
<span>自评:{{ getSignedScore(row, 'selfScore') }}分</span>
<span>复评:{{ getSignedScore(row, 'reviewScore') }}分</span>
<span v-if="!isNewScore">复评:{{ getSignedScore(row, 'reviewScore') }}分</span>
</template>
</el-table-column>
<el-table-column width="60" align="center">
@@ -1463,12 +1473,15 @@
<div class="score-detail-dialog__total">
<span>总分合计</span>
<span>自评:{{ formatScoreValue(currentScore.selfScore) }}分</span>
<span>复评:{{ formatScoreValue(currentScore.reviewScore) }}分</span>
<span v-if="!isNewScore">复评:{{ formatScoreValue(currentScore.reviewScore) }}分</span>
</div>
<el-button @click="scoreBox = false">取消</el-button>
<el-button type="primary" plain v-if="!readonly" @click="saveScoreDetail">保存</el-button>
<el-button v-if="isNewScore" type="primary" @click="submitSelfScore">提交自评</el-button>
<el-button type="primary" plain v-if="!readonly && !isNewScore" @click="saveScoreDetail">
保存
</el-button>
<el-button
v-if="!readonly && hasPermission('customer_type_re_cert')"
v-if="!isNewScore && (!readonly || reviewMode) && hasPermission('customer_type_re_cert')"
type="primary"
@click="confirmReviewScore"
>
@@ -1523,6 +1536,7 @@ import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
import SectionCard from '@/components/section-card/main.vue';
import PdfPreview from '@/components/pdf-preview/main.vue';
import ChangeRecordDetailDialog from '@/components/change-record-detail-dialog/main.vue';
const createTimeRangeMap = {
createTimeRange: ['createTimeStart', 'createTimeEnd'],
@@ -1538,6 +1552,7 @@ const qualificationViewerPlugins = [
export default {
components: {
SectionCard,
ChangeRecordDetailDialog,
ArrowRight,
ElImageViewer,
OpenFileViewer,
@@ -1631,6 +1646,8 @@ export default {
receiptBox: false,
invoiceBox: false,
readonly: false,
reviewMode: false,
listActivated: false,
originalAccessType: '',
activeTab: 'scores',
archiveForm: this.emptyArchive(),
@@ -1825,7 +1842,7 @@ export default {
editBtn: false,
selection: true,
dialogClickModal: false,
menuWidth: 280,
menuWidth: 320,
column: [
{
label: '客商编号',
@@ -2019,10 +2036,20 @@ export default {
this.initBusinessDictionaries();
this.initScoreQuantificationOptions();
if (this.isArchivePage) {
const readonly = this.$route.query.view === '1' || this.$route.query.view === 'true';
this.reviewMode = this.$route.query.review === '1';
const readonly =
this.$route.query.view === '1' || this.$route.query.view === 'true' || this.reviewMode;
this.openArchive(this.$route.query.id ? { id: this.$route.query.id } : null, readonly);
}
},
activated() {
if (this.isArchivePage || this.isPublicViewPage) return;
if (!this.listActivated) {
this.listActivated = true;
return;
}
this.onLoad(this.page, this.query);
},
computed: {
...mapGetters(['permission', 'userInfo']),
isAdmin() {
@@ -2108,6 +2135,7 @@ export default {
return ids.join(',');
},
dialogTitle() {
if (this.reviewMode) return '客商复评';
if (this.readonly) return '查看客商档案';
return this.archiveForm.id ? '编辑客商档案' : '新增客商档案';
},
@@ -2151,6 +2179,14 @@ export default {
scoreList() {
return this.archiveForm.scores || [];
},
showScoreDelete() {
if (this.readonly) return false;
const status = this.archiveForm.approvalStatus || 'draft';
return status === 'draft' || status === 'rejected';
},
isNewScore() {
return this.scoreRecordIndex < 0 && !this.reviewMode;
},
scorePageCount() {
return Math.max(Math.ceil(this.scoreList.length / this.scorePage.pageSize), 1);
},
@@ -3962,6 +3998,12 @@ export default {
query: { id: row.id, name: '查看客商档案', view: '1' },
});
},
openReview(row) {
this.$router.push({
path: '/vehicle/customer-archive/form',
query: { id: row.id, name: '客商复评', review: '1' },
});
},
closeArchive() {
if (this.isPublicViewPage) return;
this.archiveBox = false;
@@ -3993,6 +4035,7 @@ export default {
if (this.isPublicViewPage) this.ensurePublicViewOptions(this.archiveForm);
this.archiveBox = true;
this.loadChangeRecords();
if (this.reviewMode) this.openReviewScore();
})
.catch(() => {
this.archiveBox = true;
@@ -4034,14 +4077,15 @@ export default {
this.invoiceForm = this.emptyInvoice();
this.$refs.archiveForm?.clearValidate();
},
// 客商材料不做提交校验,仅在页面上提示未上传项
validateFormalForApproval(archive) {
if (archive.accessType !== 'formal') return true;
const hasCompletedScore = (archive.scores || []).some(
item => item.selfStatus === '已完成' || item.reviewStatus === '已完成' || item.finalScore
);
if (!hasCompletedScore) {
this.$message.warning('正式客商提交审核前必须完成信用评分');
// 客商材料不做提交校验,仅在页面上提示未上传项。提交审批前必须完成自评,且评分记录只能有一条。
validateScoreForApproval(archive) {
const scores = archive.scores || [];
if (scores.length > 1) {
this.$message.warning('评分记录只能有一条');
return false;
}
if (scores.length !== 1 || scores[0].selfStatus !== '已完成') {
this.$message.warning('提交审批前必须完成自评');
return false;
}
return true;
@@ -4151,7 +4195,7 @@ export default {
const archive = this.normalizeArchive();
// 仅提交时落变更记录;保存不记录
archive.recordChange = true;
if (!this.validateFormalForApproval(archive)) return;
if (!this.validateScoreForApproval(archive)) return;
submit(archive).then(res => {
const data = res.data.data;
const id = (data && typeof data === 'object' ? data.id : data) || this.archiveForm.id;
@@ -4189,9 +4233,21 @@ export default {
approvalStatus,
}).then(() => submitApproval(id));
},
openReviewScore() {
const scores = this.archiveForm.scores || [];
if (!scores.length) {
this.$message.warning('暂无评分记录,无法复评');
return;
}
this.$nextTick(() => this.openScore(scores[0], 0));
},
addScore() {
if ((this.archiveForm.scores || []).length) {
this.$message.warning('请勿重复添加');
return;
}
this.scoreRecordIndex = -1;
this.currentScore = this.normalizeScore({});
this.currentScore = this.normalizeScore({ applyCreditLimit: '' });
this.scoreAttachmentFiles = [];
this.expandedScoreCategoryKeys = [];
this.scoreBox = true;
@@ -4273,7 +4329,7 @@ export default {
JSON.stringify(
(item.options || []).map(option => ({
label: option.label || option.optionName || option.value,
value: option.value || option.optionName || option.label,
value: option.value ?? option.optionName ?? option.label,
score: option.score,
changeType: option.changeType,
changeValue: option.changeValue,
@@ -4363,20 +4419,21 @@ export default {
const isScoreOption = item.changeType || item.changeValue || item.changeUnit;
const changeLabel = item.changeType === 'decrease' ? '每减少' : '每增加';
const scoreLabel = item.scoreType === 'subtract' ? '减' : '加';
const rawScore = item.score;
const generatedLabel = isScoreOption
? `${changeLabel}${item.changeValue || ''}${item.changeUnit || ''}${scoreLabel}${
item.score || ''
this.isBlankScore(rawScore) ? '' : rawScore
}分`
: '';
const label = item.label || item.optionName || item.value || generatedLabel;
return {
...item,
label,
value: item.value || item.optionName || item.label || generatedLabel,
value: item.value ?? item.optionName ?? item.label ?? generatedLabel,
score:
item.scoreType === 'subtract' && item.score !== undefined
? -Math.abs(Number(item.score))
: item.score,
item.scoreType === 'subtract' && !this.isBlankScore(rawScore)
? -Math.abs(Number(rawScore))
: rawScore,
};
});
if (Array.isArray(value)) return normalize(value);
@@ -4440,23 +4497,35 @@ export default {
const valid = increase ? input >= base : input <= base;
if (!valid) {
detail.selfScore = '';
this.$message.warning(`输入值${increase ? '不能小于' : '不能大于'}基准数值${base}`);
this.calculateScore(this.currentScore);
return;
}
detail.selfScore = this.getScoreTypeCalculatedScore(detail);
this.calculateScore(this.currentScore);
},
handleScoreValueBlur(detail) {
const normalized = String(detail.scoreInput ?? '').trim();
if (!normalized || normalized === '-' || normalized === '.') return;
const rule = this.getScoreRule(detail);
const base = Number(detail.baseValue);
const input = Number(normalized);
if (!rule || !Number.isFinite(base) || !Number.isFinite(input)) return;
const increase = rule.changeType !== 'decrease';
const valid = increase ? input >= base : input <= base;
if (!valid) {
this.$message.warning(`输入值${increase ? '不能小于' : '不能大于'}基准数值${base}`);
}
},
scoreOptionChange(detail, optionValue) {
const option = this.parseScoreOptions(detail.optionsJson).find(
item => item.value === optionValue
);
const score = Number(option?.score || 0);
detail.selfScore = score;
const rawScore = option?.score;
detail.selfScore = this.isBlankScore(rawScore) ? 0 : Number(rawScore);
this.calculateScore(this.currentScore);
},
handleScoreDetailScoreInput(detail, prop, value) {
const normalized = String(value || '')
const normalized = String(value ?? '')
.replace(/[^\d.]/g, '')
.replace(/^\./, '')
.replace(/(\..*)\./g, '$1')
@@ -4588,8 +4657,7 @@ export default {
sumScore(plusDetails, 'reviewScore') -
sumScore(minusDetails, 'reviewScore');
const hasReviewScore = details.some(
item =>
item.reviewScore !== undefined && item.reviewScore !== null && item.reviewScore !== ''
item => !this.isBlankScore(item.reviewScore)
);
const finalScore = hasReviewScore ? reviewScore : selfScore;
const fullMark = this.getScoreFullMark(details);
@@ -4609,13 +4677,17 @@ export default {
}
return score;
},
isBlankScore(value) {
return value === undefined || value === null || String(value).trim() === '';
},
hasIncompleteBasicScoreDetail(details = []) {
return this.getScoreCategoryDetailsByList(details, 'basic').some(item => {
if (this.isScoreTypeDetail(item)) {
const input = String(item.scoreInput ?? '').trim();
return !input || !Number.isFinite(Number(input)) || item.selfScore === '';
const inputBlank = input === '' || input === '-' || input === '.';
return inputBlank || !Number.isFinite(Number(input)) || this.isBlankScore(item.selfScore);
}
return !item.selectedOption;
return this.isBlankScore(item.selectedOption);
});
},
finishScore(score) {
@@ -4632,10 +4704,13 @@ export default {
score.reviewStatus = '已完成';
this.$message.success('评分已完成');
},
saveScoreDetail() {
saveScoreDetail(successMessage) {
if (!this.validateScoreForm(false)) return;
this.currentScore.attachments = JSON.parse(JSON.stringify(this.scoreAttachmentFiles || []));
this.currentScore.proofAttachments = this.stringifyAttachments(this.scoreAttachmentFiles);
this.currentScore.selfStatus = this.hasIncompleteBasicScoreDetail(this.currentScore.details)
? '未完成'
: '已完成';
this.calculateScore(this.currentScore);
const score = JSON.parse(JSON.stringify(this.currentScore));
if (this.scoreRecordIndex > -1) {
@@ -4647,23 +4722,62 @@ export default {
this.syncArchiveCreditFromScores();
this.activeTab = 'scores';
this.scoreBox = false;
this.saveScoreArchive();
},
saveScoreArchive() {
if (!this.archiveForm.id) {
this.$message.success('评分记录已添加,请保存客商档案后生效');
return;
const pending = this.saveScoreArchive(successMessage);
if (this.reviewMode && successMessage && pending && pending.then) {
pending.then(() => this.closeArchive());
}
submit(this.normalizeArchive()).then(() => {
this.$message.success('评分记录已保存');
return pending;
},
deleteScore(index) {
this.$confirm('确定删除该评分记录?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
if (!Array.isArray(this.archiveForm.scores)) this.archiveForm.scores = [];
this.archiveForm.scores.splice(index, 1);
if (this.scoreRecordIndex === index) {
this.scoreBox = false;
} else if (this.scoreRecordIndex > index) {
this.scoreRecordIndex -= 1;
}
const latestScore = this.getLatestCreditScore(this.archiveForm.scores);
if (latestScore) {
this.syncArchiveCreditFromScores();
} else {
this.archiveForm.customerLevel = '';
this.archiveForm.maxCreditLimit = '';
this.archiveForm.applyCreditLimit = '';
}
this.handleScoreCurrentChange(this.scorePage.currentPage);
this.saveScoreArchive('评分记录已删除', '评分记录已删除,请保存客商档案后生效');
})
.catch(() => {});
},
saveScoreArchive(
successMessage = '评分记录已保存',
draftMessage = '评分记录已添加,请保存客商档案后生效'
) {
if (!this.archiveForm.id) {
this.$message.success(draftMessage);
return Promise.resolve();
}
return submit(this.normalizeArchive()).then(() => {
this.$message.success(successMessage);
});
},
submitSelfScore() {
if (!this.validateScoreForm(true)) return;
this.currentScore.selfStatus = '已完成';
this.currentScore.reviewStatus = '未完成';
this.saveScoreDetail('自评已提交');
},
confirmReviewScore() {
if (!this.validateScoreForm(true)) return;
this.currentScore.selfStatus = '已完成';
this.currentScore.reviewStatus = '已完成';
this.saveScoreDetail();
this.$message.success('复评确认成功');
this.saveScoreDetail('复评确认成功');
},
validateScoreForm(requireComplete) {
if (!this.currentScore.scoreDate) {
@@ -4701,7 +4815,7 @@ export default {
if (
item.reviewScore === undefined ||
item.reviewScore === null ||
item.reviewScore === ''
String(item.reviewScore).trim() === ''
) {
return false;
}
@@ -4718,7 +4832,7 @@ export default {
return false;
}
if (requireComplete && this.hasIncompleteBasicScoreDetail(this.currentScore.details)) {
this.$message.warning('请完整填写基础得分项评分明细');
this.$message.warning('请完整填写所有基础项目后再提交自评');
return false;
}
return true;
@@ -4773,7 +4887,7 @@ export default {
getDetail(row.id).then(res => {
const archive = this.normalizeDetail(res.data.data);
archive.qualificationAttachments = archive.qualificationAttachments || '';
if (!this.validateFormalForApproval(archive)) return;
if (!this.validateScoreForApproval(archive)) return;
this.$confirm('是否提交客商准入审批?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
@@ -5260,10 +5374,16 @@ export default {
}
:deep(.el-table__body td) {
//min-height: 112px;
color: #303133;
}
:deep(.score-item-name) {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
:deep(.el-select),
:deep(.el-input),
:deep(.el-select) {
@@ -5446,27 +5566,6 @@ export default {
word-break: break-word;
}
:deep(.change-record-detail-dialog .el-dialog__body) {
max-height: 65vh;
overflow: auto;
}
:deep(.change-record-detail-dialog .change-record-detail-value .cell) {
white-space: pre-wrap;
word-break: break-word;
overflow: visible;
text-overflow: clip;
line-height: 1.6;
}
.change-record-detail-meta {
display: flex;
gap: 32px;
margin-bottom: 16px;
color: #606266;
font-size: 14px;
}
.customer-archive-page.is-public-view {
:deep(.archive-page-form .archive-form__footer) {
left: 0;