1、完善客商模块

2、完善基础数据模块
3、完善规范
This commit is contained in:
2026-08-01 01:23:13 +08:00
parent 6a4c5d02f4
commit 81c98f6ad8
77 changed files with 4883 additions and 711 deletions

View File

@@ -72,7 +72,6 @@
<el-button
type="primary"
text
icon="el-icon-edit"
v-if="hasPermission('credit_score_quantification_edit')"
@click="openConfig(row)"
>
@@ -81,8 +80,7 @@
<el-button
type="primary"
text
icon="el-icon-position"
v-if="hasPermission('credit_score_quantification_publish') && row.status !== 1"
v-if="hasPermission('credit_score_quantification_publish') && row.status === 3"
@click="handlePublish(row)"
>
发布
@@ -90,7 +88,6 @@
<el-button
type="primary"
text
:icon="row.status === 1 ? 'el-icon-close' : 'el-icon-check'"
v-if="hasPermission('credit_score_quantification_status') && row.status !== 3"
@click="handleStatus(row)"
>
@@ -99,7 +96,6 @@
<el-button
type="danger"
text
icon="el-icon-delete"
v-if="hasPermission('credit_score_quantification_delete') && row.status === 3"
@click="rowDel(row)"
>
@@ -147,7 +143,7 @@
<div class="section-title dialog-section-title">评分表</div>
</div>
<el-table :data="configForm.categories" border class="config-table">
<el-table-column label="序号" type="index" width="70" align="center" />
<el-table-column label="序号" type="index" width="90" align="center" />
<el-table-column label="评分类目" prop="categoryName" min-width="180" />
<el-table-column label="评分项目数" width="140" align="center">
<template #default="{ row }">{{ row.items.length }}</template>
@@ -166,7 +162,7 @@
</el-button>
</div>
<el-table :data="configForm.standards" border class="config-table">
<el-table-column label="序号" type="index" width="70" align="center" />
<el-table-column label="序号" type="index" width="90" align="center" />
<el-table-column label="信用等级" prop="creditLevel" width="120" align="center" />
<el-table-column label="得分率" width="160" align="center">
<template #default="{ row }"
@@ -201,6 +197,7 @@
v-model="categoryBox"
width="82%"
class="score-dialog"
:close-on-press-escape="false"
>
<div class="category-detail">
<div class="category-line">评分类目{{ currentCategory.categoryName }}</div>
@@ -211,7 +208,7 @@
</el-button>
</div>
<el-table :data="currentCategory.items" border class="config-table">
<el-table-column label="序号" type="index" width="70" align="center" />
<el-table-column label="序号" type="index" width="90" align="center" />
<el-table-column label="评分项目" prop="itemName" width="180" />
<el-table-column label="得分说明" prop="scoreDescription" min-width="260" />
<el-table-column label="选项数" width="100" align="center">
@@ -220,9 +217,15 @@
<el-table-column label="操作" width="160" align="center">
<template #default="{ row, $index }">
<el-button type="primary" text @click="openItem(row, $index)">编辑</el-button>
<el-button type="danger" text v-if="!readonly" @click="removeItem($index)"
>删除</el-button
<el-button
type="danger"
text
v-if="!readonly"
:disabled="currentCategory.items.length <= 1"
@click="removeItem($index)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
@@ -232,39 +235,57 @@
</template>
</el-dialog>
<el-dialog title="评分项目详情" append-to-body v-model="itemBox" width="76%">
<el-form :model="itemForm" label-position="right" label-width="auto" :disabled="readonly">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="评分项目" required>
<el-input v-model="itemForm.itemName" maxlength="20" show-word-limit />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="得分说明">
<el-input v-model="itemForm.scoreDescription" maxlength="300" show-word-limit />
</el-form-item>
</el-col>
</el-row>
<el-dialog
title="评分项目详情"
append-to-body
v-model="itemBox"
width="86%"
class="score-item-dialog"
>
<el-form
:model="itemForm"
label-position="right"
label-width="112px"
:disabled="readonly"
class="score-item-form"
>
<div class="score-item-form__top">
<el-form-item label="评分项目" required>
<el-input v-model="itemForm.itemName" maxlength="20" />
</el-form-item>
<el-form-item label="分值" required>
<el-input-number v-model="itemForm.score" :min="0" :precision="2" :controls="false" />
</el-form-item>
</div>
<el-form-item label="得分说明">
<el-input v-model="itemForm.scoreDescription" maxlength="300" />
</el-form-item>
<el-form-item label="选项描述">
<el-input v-model="itemForm.optionDescription" maxlength="100" show-word-limit />
<el-input v-model="itemForm.optionDescription" maxlength="100" />
</el-form-item>
<el-form-item label="选项" required>
<div class="option-editor">
<div class="option-row" v-for="(option, index) in itemForm.options" :key="index">
<el-input v-model="option.optionName" maxlength="100" placeholder="选项描述" />
<el-input-number v-model="option.score" :min="0" :precision="2" placeholder="分值" />
<span class="score-unit"></span>
<el-input v-model="option.optionName" maxlength="100" />
<div class="option-score">
<el-input-number v-model="option.score" :min="0" :precision="2" :controls="false" />
<span class="score-unit"></span>
</div>
<el-button
type="danger"
type="primary"
text
class="option-delete"
v-if="!readonly && itemForm.options.length > 1"
@click="removeOption(index)"
>
删除
</el-button>
</div>
<el-button type="primary" text v-if="!readonly" @click="addOption">+添加选项</el-button>
<div class="option-add">
<el-button type="primary" text v-if="!readonly" @click="addOption">
+添加选项
</el-button>
</div>
</div>
</el-form-item>
</el-form>
@@ -274,12 +295,23 @@
</template>
</el-dialog>
<el-dialog title="评估标准设置" append-to-body v-model="standardBox" width="76%">
<el-dialog
title="评估标准设置"
append-to-body
v-model="standardBox"
width="76%"
:close-on-press-escape="false"
>
<el-form :model="standardForm" label-position="right" label-width="auto" :disabled="readonly">
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="信用等级" required>
<el-select v-model="standardForm.creditLevel" placeholder="请选择信用等级" clearable>
<el-select
v-model="standardForm.creditLevel"
placeholder="请选择信用等级"
clearable
@change="handleStandardCreditLevelChange"
>
<el-option
v-for="item in creditLevelOptions"
:key="item.value"
@@ -419,6 +451,7 @@ export default {
border: true,
index: true,
indexLabel: '序号',
indexWidth: 90,
addBtn: false,
viewBtn: false,
delBtn: false,
@@ -547,6 +580,7 @@ export default {
emptyItem() {
return {
itemName: '',
score: 0,
scoreDescription: '',
optionDescription: '',
options: [{ optionName: '', score: 0 }],
@@ -608,19 +642,55 @@ export default {
},
openItem(row, index = -1) {
this.currentItemIndex = index;
this.itemForm = row ? JSON.parse(JSON.stringify(row)) : this.emptyItem();
this.itemForm = row
? { ...this.emptyItem(), ...JSON.parse(JSON.stringify(row)) }
: this.emptyItem();
this.itemBox = true;
},
hasDuplicateItemName(itemName, currentIndex = -1) {
const name = String(itemName || '').trim();
return this.currentCategory.items.some(
(item, index) => index !== currentIndex && String(item.itemName || '').trim() === name
);
},
saveItem() {
if (!this.itemForm.itemName || !this.itemForm.itemName.trim()) {
this.$message.warning('请输入评分项目');
return;
}
if (this.hasDuplicateItemName(this.itemForm.itemName, this.currentItemIndex)) {
this.$message.warning('评分项目名称不能重复');
return;
}
if (
this.itemForm.score === undefined ||
this.itemForm.score === null ||
this.itemForm.score === ''
) {
this.$message.warning('请输入分值');
return;
}
if (!this.itemForm.options.length) {
this.$message.warning('至少添加1条档位选项');
return;
}
const invalidOption = this.itemForm.options.some(
option =>
!String(option.optionName || '').trim() ||
option.score === undefined ||
option.score === null ||
option.score === ''
);
if (invalidOption) {
this.$message.warning('请完整填写档位选项');
return;
}
const item = JSON.parse(JSON.stringify(this.itemForm));
item.itemName = String(item.itemName || '').trim();
item.options = item.options.map(option => ({
...option,
optionName: String(option.optionName || '').trim(),
}));
if (this.currentItemIndex >= 0) {
this.currentCategory.items.splice(this.currentItemIndex, 1, item);
} else {
@@ -629,6 +699,10 @@ export default {
this.itemBox = false;
},
removeItem(index) {
if (this.currentCategory.items.length <= 1) {
this.$message.warning('评分类目详情至少保留1条评分项目');
return;
}
this.currentCategory.items.splice(index, 1);
},
addOption() {
@@ -648,6 +722,13 @@ export default {
(item, index) => index !== currentIndex && String(item.creditLevel || '').trim() === level
);
},
handleStandardCreditLevelChange(value) {
if (!value) return;
if (this.hasDuplicateCreditLevel(value, this.currentStandardIndex)) {
this.$message.warning('信用等级不能重复');
this.standardForm.creditLevel = '';
}
},
isValidCreditLevel(creditLevel) {
return this.creditLevelOptions.some(item => item.value === String(creditLevel || '').trim());
},
@@ -681,8 +762,28 @@ export default {
this.$message.warning('得分率下限必须小于上限');
return;
}
if (
Number(this.standardForm.creditLimitLower) >= Number(this.standardForm.creditLimitUpper)
) {
this.$message.warning('授信额度下限必须小于上限');
return;
}
const standard = JSON.parse(JSON.stringify(this.standardForm));
standard.creditLevel = String(standard.creditLevel || '').trim();
const nextStandards = JSON.parse(JSON.stringify(this.configForm.standards));
if (this.currentStandardIndex >= 0) {
nextStandards.splice(this.currentStandardIndex, 1, standard);
} else {
nextStandards.push(standard);
}
if (this.hasRangeOverlap(nextStandards, 'scoreRateLower', 'scoreRateUpper')) {
this.$message.warning('评估标准中的得分率区间不能重叠');
return;
}
if (this.hasRangeOverlap(nextStandards, 'creditLimitLower', 'creditLimitUpper')) {
this.$message.warning('评估标准中的授信额度区间不能重叠');
return;
}
if (this.currentStandardIndex >= 0) {
this.configForm.standards.splice(this.currentStandardIndex, 1, standard);
} else {
@@ -693,6 +794,21 @@ export default {
removeStandard(index) {
this.configForm.standards.splice(index, 1);
},
isBlankValue(value) {
return value === undefined || value === null || value === '';
},
hasRangeOverlap(list, lowerProp, upperProp) {
const ranges = list
.map(item => ({
lower: Number(item[lowerProp]),
upper: Number(item[upperProp]),
}))
.sort((a, b) => a.lower - b.lower);
return ranges.some((range, index) => {
const next = ranges[index + 1];
return next && range.upper > next.lower;
});
},
validateConfig() {
if (!this.configForm.name || !this.configForm.name.trim()) {
this.$message.warning('请输入评定表名称');
@@ -704,6 +820,13 @@ export default {
return false;
}
for (const category of this.configForm.categories) {
const itemNames = (category.items || [])
.map(item => String(item.itemName || '').trim())
.filter(Boolean);
if (new Set(itemNames).size !== itemNames.length) {
this.$message.warning(`${category.categoryName}的评分项目名称不能重复`);
return false;
}
for (const item of category.items) {
if (!item.options || item.options.length === 0) {
this.$message.warning(`${item.itemName}至少存在1条档位选项`);
@@ -724,9 +847,90 @@ export default {
}
return true;
},
validatePublishConfig(config) {
const detail = this.normalizeDetail(config || {});
const basic = detail.categories.find(item => item.categoryCode === 'basic');
if (!basic || !basic.items || basic.items.length === 0) {
this.$message.warning('发布前至少配置1条基础得分项目');
return false;
}
for (const category of detail.categories) {
for (const item of category.items || []) {
if (!String(item.itemName || '').trim()) {
this.$message.warning('发布前请完整填写评分项目名称');
return false;
}
if (!item.options || item.options.length === 0) {
this.$message.warning(`${item.itemName || '评分项目'}至少配置1个档位选项`);
return false;
}
const invalidOption = item.options.some(
option => !String(option.optionName || '').trim() || this.isBlankValue(option.score)
);
if (invalidOption) {
this.$message.warning(`${item.itemName || '评分项目'}的档位选项未填写完整`);
return false;
}
}
}
const standards = detail.standards || [];
if (standards.length === 0) {
this.$message.warning('发布前至少配置1条信用等级评估标准');
return false;
}
const requiredFields = [
'scoreRateLower',
'scoreRateUpper',
'scoreRateIncrease',
'creditLimitLower',
'creditLimitUpper',
'creditLimitIncrease',
];
for (const standard of standards) {
if (!standard.creditLevel) {
this.$message.warning('评估标准中的信用等级不能为空');
return false;
}
if (!this.isValidCreditLevel(standard.creditLevel)) {
this.$message.warning('评估标准中的信用等级只能选择A/B/C/D/E/F');
return false;
}
if (requiredFields.some(field => this.isBlankValue(standard[field]))) {
this.$message.warning(`${standard.creditLevel}信用等级评估标准必填项未填写完整`);
return false;
}
if (Number(standard.scoreRateLower) >= Number(standard.scoreRateUpper)) {
this.$message.warning(`${standard.creditLevel}信用等级得分率下限必须小于上限`);
return false;
}
if (Number(standard.creditLimitLower) >= Number(standard.creditLimitUpper)) {
this.$message.warning(`${standard.creditLevel}信用等级授信额度下限必须小于上限`);
return false;
}
}
const creditLevels = standards.map(item => String(item.creditLevel || '').trim());
if (new Set(creditLevels).size !== creditLevels.length) {
this.$message.warning('评估标准中的信用等级不能重复');
return false;
}
if (this.hasRangeOverlap(standards, 'scoreRateLower', 'scoreRateUpper')) {
this.$message.warning('评估标准中的得分率区间不能重叠');
return false;
}
if (this.hasRangeOverlap(standards, 'creditLimitLower', 'creditLimitUpper')) {
this.$message.warning('评估标准中的授信额度区间不能重叠');
return false;
}
return true;
},
saveConfig() {
if (!this.validateConfig()) return;
submit(this.configForm).then(() => {
submit({
...this.configForm,
status: 3,
}).then(() => {
this.configBox = false;
this.onLoad(this.page);
this.$message({ type: 'success', message: '操作成功!' });
@@ -767,8 +971,16 @@ export default {
cancelButtonText: '取消',
type: 'warning',
})
.then(() => publish(row.id))
.then(() => {
.then(() => getDetail(row.id))
.then(res => {
const detail = this.normalizeDetail(res.data.data || {});
if (!this.validatePublishConfig(detail)) {
return false;
}
return publish(row.id).then(() => true);
})
.then(published => {
if (!published) return;
this.onLoad(this.page);
this.$message({ type: 'success', message: '发布成功!' });
});
@@ -776,7 +988,7 @@ export default {
handleStatus(row) {
const nextStatus = row.status === 1 ? 2 : 1;
const actionName = nextStatus === 1 ? '启用' : '停用';
this.$confirm(`是否确认${actionName}量表?`, {
this.$confirm(`是否确认${actionName}评分量化表?`, {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
@@ -830,7 +1042,8 @@ export default {
NProgress.start();
exportBlob(
'/blade-transport/credit-score-quantification/export-credit-score-quantification',
this.buildExportParams()
this.buildExportParams(),
{ feedback: true }
)
.then(res => {
downloadXls(res.data, `评分量化表${this.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`);
@@ -854,7 +1067,8 @@ export default {
exportBlob(
`/blade-transport/credit-score-quantification/export-template?${
this.website.tokenHeader
}=${getToken()}`
}=${getToken()}`,
{ feedback: true }
).then(res => {
downloadXls(res.data, '评分量化表模板.xlsx');
});
@@ -907,18 +1121,105 @@ export default {
color: #606266;
}
.score-item-form {
width: 100%;
max-width: 1400px;
margin: 0 auto;
padding: 4px 0 6px;
:deep(.el-form-item) {
margin-bottom: 18px;
}
:deep(.el-form-item__label) {
padding-right: 10px;
color: #606266;
font-size: 14px;
line-height: 32px;
}
:deep(.el-input),
:deep(.el-input-number) {
width: 100%;
}
:deep(.el-input__wrapper),
:deep(.el-input-number .el-input__wrapper) {
min-height: 32px;
border-radius: 0;
box-shadow: 0 0 0 1px #dcdfe6 inset;
}
:deep(.el-input__inner) {
height: 32px;
color: #303133;
font-size: 14px;
font-weight: 400;
text-align: center;
}
&__top {
display: grid;
grid-template-columns: 360px 360px;
column-gap: 112px;
:deep(.el-form-item) {
margin-bottom: 18px;
}
}
}
.option-editor {
width: 100%;
}
.option-row {
display: grid;
grid-template-columns: minmax(260px, 1fr) 190px 32px 70px;
gap: 12px;
grid-template-columns: minmax(420px, 1fr) 315px 74px;
gap: 54px;
align-items: center;
margin-bottom: 14px;
}
.option-score {
position: relative;
.score-unit {
position: absolute;
top: 50%;
right: 12px;
z-index: 1;
margin: 0;
color: #303133;
font-size: 14px;
line-height: 1;
transform: translateY(-50%);
}
:deep(.el-input__inner) {
padding-right: 42px;
}
}
.option-delete {
justify-self: start;
min-height: 32px;
padding: 0;
font-size: 14px;
}
.option-add {
display: flex;
justify-content: center;
margin-top: 6px;
:deep(.el-button) {
min-height: 32px;
font-size: 16px;
font-weight: 600;
}
}
.range-input {
display: flex;
align-items: center;
@@ -934,4 +1235,42 @@ export default {
max-height: 70vh;
overflow: auto;
}
:deep(.score-item-dialog) {
.el-dialog__header {
padding: 20px 24px 10px;
}
.el-dialog__title {
color: #303133;
font-size: 20px;
font-weight: 700;
line-height: 1;
}
.el-dialog__body {
padding: 12px 24px 14px;
}
.el-dialog__footer {
padding: 12px 24px 20px;
}
.dialog-footer,
.el-dialog__footer {
text-align: right;
}
.el-dialog__footer .el-button {
min-width: 88px;
min-height: 32px;
border-radius: 4px;
font-size: 14px;
font-weight: 400;
}
.el-dialog__footer .el-button + .el-button {
margin-left: 12px;
}
}
</style>