Files
tms-erp-web/src/views/vehicle/credit-score-quantification.vue
T
2026-08-25 18:39:51 +08:00

1585 lines
51 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<basic-container>
<avue-crud
:option="option"
:table-loading="loading"
:data="data"
v-model:page="page"
v-model="form"
ref="crud"
:permission="permissionList"
@search-change="searchChange"
@search-reset="searchReset"
@selection-change="selectionChange"
@current-change="currentChange"
@size-change="sizeChange"
@refresh-change="refreshChange"
@on-load="onLoad"
>
<template #menu-left>
<el-button
type="primary"
icon="el-icon-plus"
v-if="hasPermission('credit_score_quantification_add')"
@click="openConfig()"
>新增
</el-button>
<el-button
type="danger"
icon="el-icon-delete"
plain
v-if="hasPermission('credit_score_quantification_delete')"
@click="handleDelete"
>批量删除
</el-button>
</template>
<template #name="{ row }">
<el-button type="primary" link @click="openConfig(row, true)">
{{ row.name }}
</el-button>
</template>
<template #status="{ row }">
<el-tag :type="statusMap[row.status].type" class="status-text">
{{ statusMap[row.status].label }}
</el-tag>
</template>
<template #menu="{ row }">
<el-link
type="primary"
v-if="hasPermission('credit_score_quantification_status') && row.status === 1"
@click="handleStatus(row)"
>
停用
</el-link>
<el-link
type="primary"
v-if="hasPermission('credit_score_quantification_publish') && row.status !== 1"
@click="handlePublish(row)"
>
发布
</el-link>
<el-link
type="primary"
v-if="hasPermission('credit_score_quantification_edit') && row.status !== 1"
@click="openConfig(row)"
>
编辑
</el-link>
<el-link
type="danger"
v-if="hasPermission('credit_score_quantification_delete')"
@click="rowDel(row)"
>
删除
</el-link>
</template>
</avue-crud>
<el-dialog
:title="readonly ? '查看评分量化表' : configForm.id ? '编辑评分量化表' : '新增评分量化表'"
append-to-body
v-model="configBox"
width="65%"
class="score-dialog"
@closed="resetConfig"
>
<div class="score-config">
<section-card title="评分量化表">
<el-form
:model="configForm"
label-position="right"
label-width="auto"
class="archive-form"
:disabled="readonly"
>
<el-form-item label="评定表名称" required>
<el-input
v-model="configForm.name"
class="score-config__name-input"
maxlength="30"
show-word-limit
style="width: 500px"
/>
</el-form-item>
<el-form-item label="备注">
<el-input
v-model="configForm.remark"
type="textarea"
:rows="1"
maxlength="500"
show-word-limit
/>
</el-form-item>
</el-form>
</section-card>
<section-card title="评分表">
<el-table :data="configForm.categories" border class="config-table">
<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>
</el-table-column>
<el-table-column label="操作" width="160" align="center">
<template #default="{ row }">
<el-link type="primary" @click="openCategory(row)">编辑</el-link>
</template>
</el-table-column>
</el-table>
</section-card>
<section-card title="评估标准">
<template #extra>
<el-button
type="primary"
plain
icon="el-icon-plus"
v-if="!readonly"
@click="openStandard()"
>
添加评估标准
</el-button>
</template>
<el-table :data="configForm.standards" border class="config-table">
<el-table-column label="序号" type="index" width="90" align="center" />
<el-table-column label="信用等级" prop="creditLevel" width="120" align="center" />
<el-table-column label="标准说明" prop="standardDescription" min-width="320" />
<el-table-column label="操作" width="160" align="center">
<template #default="{ row, $index }">
<el-link type="primary" @click="openStandard(row, $index)">编辑</el-link>
<el-link type="danger" v-if="!readonly" @click="removeStandard($index)"
>删除</el-link
>
</template>
</el-table-column>
</el-table>
</section-card>
</div>
<template #footer>
<el-button @click="configBox = false">{{ readonly ? '关闭' : '取消' }}</el-button>
<el-button type="primary" v-if="!readonly" @click="saveConfig">保存</el-button>
</template>
</el-dialog>
<el-dialog
title="评分类目详情"
append-to-body
v-model="categoryBox"
width="61%"
class="score-dialog category-dialog"
:close-on-press-escape="false"
>
<div class="category-detail">
<div class="category-line">评分类目{{ currentCategory.categoryName }}</div>
<section-card title="评分项目">
<template #extra>
<el-button
type="primary"
plain
icon="el-icon-plus"
v-if="!readonly"
@click="openItem()"
>
添加评分项目
</el-button>
</template>
<el-table :data="currentCategory.items" border class="config-table">
<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">
<template #default="{ row }">{{ row.options.length }}</template>
</el-table-column>
<el-table-column label="操作" width="160" align="center">
<template #default="{ row, $index }">
<el-link type="primary" @click="openItem(row, $index)">编辑</el-link>
<el-link
type="danger"
v-if="!readonly"
:disabled="currentCategory.items.length <= 1"
@click="removeItem($index)"
>
删除
</el-link>
</template>
</el-table-column>
</el-table>
</section-card>
</div>
<template #footer>
<el-button type="primary" @click="categoryBox = false">保存</el-button>
</template>
</el-dialog>
<el-dialog
title="评分项目详情"
append-to-body
v-model="itemBox"
width="58%"
class="score-item-dialog"
>
<section-card title="评分项目详情">
<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"
:step="0.01"
:controls="false"
/>
</el-form-item>
</div>
<el-form-item label="得分说明">
<el-input
v-model="itemForm.scoreDescription"
maxlength="300"
class="score-description-input"
/>
</el-form-item>
<el-form-item label="选项描述">
<el-input v-model="itemForm.optionDescription" maxlength="100" />
</el-form-item>
<el-form-item label="选项类型" required>
<el-radio-group v-model="itemForm.optionType" @change="handleOptionTypeChange">
<el-radio value="option">选项</el-radio>
<el-radio value="score">分值</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="itemForm.optionType === 'score'" label="基准数值" required>
<el-input
:model-value="itemForm.baseValue"
inputmode="decimal"
@input="handleBaseValueInput"
/>
</el-form-item>
<el-form-item :label="itemForm.optionType === 'score' ? '阶梯配置' : '选项'" required>
<div class="option-editor">
<div
class="option-row"
:class="{ 'option-row--score': itemForm.optionType === 'score' }"
v-for="(option, index) in itemForm.options"
:key="index"
>
<template v-if="itemForm.optionType === 'score'">
<el-select v-model="option.changeType" class="option-change-type">
<el-option label="每增加" value="increase" />
<el-option label="每减少" value="decrease" />
</el-select>
<el-input
:model-value="option.changeValue"
inputmode="decimal"
@input="value => handleOptionDecimalInput(option, 'changeValue', value)"
>
<template #append>
<el-select v-model="option.changeUnit" class="option-unit-select">
<el-option
v-for="unit in scoreUnitOptions"
:key="unit"
:label="unit"
:value="unit"
/>
</el-select>
</template>
</el-input>
<el-select v-model="option.scoreType" class="option-score-type">
<el-option label="加" value="add" />
<el-option label="减" value="subtract" />
</el-select>
<div class="option-score">
<el-input
:model-value="option.score"
inputmode="decimal"
@input="value => handleOptionDecimalInput(option, 'score', value)"
>
<template #append></template>
</el-input>
</div>
</template>
<template v-else>
<el-input v-model="option.optionName" maxlength="100" />
<div class="option-score">
<el-input
:model-value="option.score"
inputmode="decimal"
@input="value => handleOptionDecimalInput(option, 'score', value)"
>
<template #append></template>
</el-input>
</div>
</template>
<el-link
type="danger"
class="option-delete"
v-if="!readonly && itemForm.options.length > 1"
@click="removeOption(index)"
>
删除
</el-link>
</div>
<div class="option-add" v-if="itemForm.optionType !== 'score'">
<el-link type="primary" v-if="!readonly" @click="addOption"> +添加选项 </el-link>
</div>
</div>
</el-form-item>
</el-form>
</section-card>
<template #footer>
<el-button @click="itemBox = false">取消</el-button>
<el-button type="primary" v-if="!readonly" @click="saveItem">保存</el-button>
</template>
</el-dialog>
<el-dialog
title="评估标准设置"
append-to-body
v-model="standardBox"
width="50%"
:close-on-press-escape="false"
>
<section-card title="评估标准设置">
<el-form
:model="standardForm"
label-position="right"
label-width="auto"
:disabled="readonly"
class="standard-form archive-form"
>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="信用等级" required>
<el-select
v-model="standardForm.creditLevel"
placeholder="请选择信用等级"
clearable
@change="handleStandardCreditLevelChange"
>
<el-option
v-for="item in creditLevelOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="得分率(%" required>
<div class="range-input standard-range-input score-rate-range">
<el-input
v-model="standardForm.scoreRateLower"
inputmode="numeric"
@input="value => handleStandardNumberInput('scoreRateLower', value, 0, 100)"
/>
<span></span>
<el-input
v-model="standardForm.scoreRateUpper"
inputmode="numeric"
@input="value => handleStandardNumberInput('scoreRateUpper', value, 0, 100)"
/>
</div>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="得分率每增加" required>
<el-input
v-model="standardForm.scoreRateIncrease"
inputmode="numeric"
@input="value => handleStandardNumberInput('scoreRateIncrease', value)"
>
<template #suffix>%</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="额度增加(万元)" required>
<el-input
v-model="standardForm.creditLimitIncrease"
inputmode="decimal"
@input="value => handleStandardNumberInput('creditLimitIncrease', value, 2)"
class="credit-increase-input"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="最大资金使用额度(万元)" required>
<div class="range-input standard-range-input credit-limit-range">
<el-input
v-model="standardForm.creditLimitLower"
inputmode="decimal"
@input="value => handleStandardNumberInput('creditLimitLower', value, 2)"
/>
<span></span>
<el-input
v-model="standardForm.creditLimitUpper"
inputmode="decimal"
@input="value => handleStandardNumberInput('creditLimitUpper', value, 2)"
/>
</div>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="标准说明">
<el-input
v-model="standardForm.standardDescription"
type="textarea"
maxlength="500"
show-word-limit
/>
</el-form-item>
</el-form>
</section-card>
<template #footer>
<el-button @click="standardBox = false">取消</el-button>
<el-button type="primary" v-if="!readonly" @click="saveStandard">保存</el-button>
</template>
</el-dialog>
</basic-container>
</template>
<script>
import {
getList,
getDetail,
submit,
remove,
publish,
changeStatus,
} from '@/api/vehicle/credit-score-quantification';
import { normalizeSearchRangeParams } from '@/utils/search-range';
import { mapGetters } from 'vuex';
const createTimeRangeMap = {
createTimeRange: ['createTimeStart', 'createTimeEnd'],
};
const standardNumberFields = [
'scoreRateLower',
'scoreRateUpper',
'scoreRateIncrease',
'creditLimitLower',
'creditLimitUpper',
'creditLimitIncrease',
];
export default {
data() {
return {
form: {},
query: {},
loading: true,
data: [],
page: {
pageSize: 10,
currentPage: 1,
total: 0,
},
selectionList: [],
configBox: false,
categoryBox: false,
itemBox: false,
standardBox: false,
readonly: false,
configForm: this.emptyConfig(),
currentCategory: { items: [] },
currentItemIndex: -1,
currentStandardIndex: -1,
itemForm: this.emptyItem(),
standardForm: this.emptyStandard(),
statusMap: {
1: { label: '正常', type: 'success' },
2: { label: '停用', type: 'info' },
3: { label: '草稿', type: 'warning' },
},
creditLevelOptions: ['A', 'B', 'C', 'D', 'E', 'F'].map(item => ({
label: item,
value: item,
})),
scoreUnitOptions: ['%', '件', '次', '项', '天'],
option: {
height: 'auto',
calcHeight: 32,
dialogWidth: 760,
labelPosition: 'right',
tip: false,
searchShow: true,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 4,
searchMenuPosition: 'right',
border: true,
index: true,
indexLabel: '序号',
indexWidth: 90,
addBtn: false,
viewBtn: false,
delBtn: false,
editBtn: false,
selection: true,
dialogClickModal: false,
menuWidth: 180,
column: [
{
label: '评分表名称',
prop: 'name',
slot: true,
minWidth: 180,
search: true,
maxlength: 30,
rules: [{ required: true, message: '请输入评定表名称', trigger: 'blur' }],
},
{
label: '创建人',
prop: 'createUserName',
minWidth: 120,
display: false,
},
{
label: '创建时间',
prop: 'createTime',
sortable: true,
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
minWidth: 160,
display: false,
},
{
label: '状态',
prop: 'status',
type: 'select',
search: true,
slot: true,
dataType: 'number',
searchValue: '',
dicData: [
{ label: '全部', value: '' },
{ label: '正常', value: 1 },
{ label: '停用', value: 2 },
],
clearable: true,
},
{
label: '创建时间',
prop: 'createTimeRange',
type: 'date',
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD',
searchRange: true,
hide: true,
display: false,
},
],
},
};
},
computed: {
...mapGetters(['permission', 'userInfo']),
isAdmin() {
const authority = this.userInfo.authority || '';
return authority.includes('admin');
},
permissionList() {
return {
addBtn: false,
};
},
ids() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
},
methods: {
hasPermission(code) {
return this.isAdmin || this.validData(this.permission[code], false);
},
emptyConfig() {
return {
name: '',
remark: '',
standardDescription: '',
status: 3,
categories: this.defaultCategories(),
standards: [],
};
},
defaultCategories() {
return [
{ categoryCode: 'basic', categoryName: '基础得分项', sort: 1, items: [] },
{ categoryCode: 'plus', categoryName: '加分项目', sort: 2, items: [] },
{ categoryCode: 'minus', categoryName: '减分项目', sort: 3, items: [] },
];
},
emptyItem() {
return {
itemName: '',
score: 1,
scoreDescription: '',
optionDescription: '',
optionType: 'option',
baseValue: '',
options: [this.emptyOption()],
};
},
emptyOption() {
return {
optionName: '',
score: 1,
changeType: 'increase',
changeValue: '',
changeUnit: '%',
scoreType: 'add',
};
},
emptyStandard() {
return {
creditLevel: '',
scoreRateLower: undefined,
scoreRateUpper: undefined,
scoreRateIncrease: undefined,
creditLimitLower: undefined,
creditLimitUpper: undefined,
creditLimitIncrease: undefined,
standardDescription: '',
};
},
normalizeDetail(detail) {
const categories = this.defaultCategories();
const map = {};
(detail.categories || []).forEach(category => {
map[category.categoryCode] = {
...category,
items: (category.items || []).map(item => this.normalizeItem(item)),
};
});
return {
...detail,
categories: categories.map(category => ({
...(map[category.categoryCode] || category),
categoryName: category.categoryName,
})),
standards: (detail.standards || []).map(standard => this.normalizeStandard(standard)),
};
},
openConfig(row, readonly = false) {
this.readonly = readonly;
if (row && row.id) {
getDetail(row.id).then(res => {
this.configForm = this.normalizeDetail(res.data.data);
this.configBox = true;
});
return;
}
this.configForm = this.emptyConfig();
if (row && row.name) {
this.configForm.name = row.name;
}
this.configBox = true;
},
resetConfig() {
this.readonly = false;
this.configForm = this.emptyConfig();
},
openCategory(category) {
this.currentCategory = category;
this.categoryBox = true;
},
openItem(row, index = -1) {
this.currentItemIndex = index;
this.itemForm = row
? this.normalizeItem({ ...this.emptyItem(), ...JSON.parse(JSON.stringify(row)) })
: this.emptyItem();
this.itemBox = true;
},
normalizeOptionType(value) {
return value === 'score' || value === 2 || String(value) === 'score' ? 'score' : 'option';
},
normalizeItem(item = {}) {
const normalized = { ...this.emptyItem(), ...item };
const score = Number(this.normalizePlainDecimal(normalized.score));
return {
...normalized,
score: Number.isFinite(score) ? score : normalized.score,
baseValue: this.normalizePlainDecimal(normalized.baseValue),
optionType: this.normalizeOptionType(normalized.optionType),
options:
normalized.options && normalized.options.length
? normalized.options.map(option => this.normalizeOption(option))
: [this.emptyOption()],
};
},
normalizeOption(option = {}) {
const normalized = { ...this.emptyOption(), ...option };
return {
...normalized,
score: this.normalizePlainDecimal(normalized.score),
changeValue: this.normalizePlainDecimal(normalized.changeValue),
changeType:
normalized.changeType === 'decrease' ||
normalized.changeType === '每减少' ||
normalized.changeType === 2
? 'decrease'
: 'increase',
scoreType:
normalized.scoreType === 'subtract' ||
normalized.scoreType === '减' ||
normalized.scoreType === 2
? 'subtract'
: 'add',
};
},
normalizePlainDecimal(value) {
if (value === undefined || value === null || value === '') return '';
const text = String(value).trim();
const matched = text.match(/^([+-]?)(\d+)(?:\.(\d*))?[eE]([+-]?\d+)$/);
if (!matched) return text;
const [, sign, integer, decimal = '', exponentText] = matched;
const exponent = Number(exponentText);
const digits = `${integer}${decimal}`;
const decimalIndex = integer.length + exponent;
const resultLength =
decimalIndex <= 0 ? 2 - decimalIndex + digits.length : Math.max(decimalIndex, digits.length);
if (!Number.isSafeInteger(exponent) || resultLength > 1000) return text;
let result;
if (decimalIndex <= 0) {
result = `0.${'0'.repeat(-decimalIndex)}${digits}`;
} else if (decimalIndex >= digits.length) {
result = `${digits}${'0'.repeat(decimalIndex - digits.length)}`;
} else {
result = `${digits.slice(0, decimalIndex)}.${digits.slice(decimalIndex)}`;
}
return `${sign === '-' ? '-' : ''}${result}`;
},
normalizeStandard(standard = {}) {
const normalized = { ...this.emptyStandard(), ...standard };
standardNumberFields.forEach(field => {
normalized[field] = this.normalizePlainDecimal(normalized[field]);
});
return normalized;
},
handleOptionTypeChange(value) {
this.itemForm.optionType = this.normalizeOptionType(value);
this.itemForm.options = (this.itemForm.options || []).map(option =>
this.normalizeOption(option)
);
},
handleBaseValueInput(value) {
const source = String(value ?? '').replace(/[^\d.-]/g, '');
const negative = source.startsWith('-');
const unsigned = source.replace(/-/g, '');
const [integer = '', ...decimalParts] = unsigned.split('.');
const decimal = decimalParts.join('').slice(0, 10);
this.itemForm.baseValue = `${negative ? '-' : ''}${integer}${decimalParts.length ? `.${decimal}` : ''}`;
},
handleOptionDecimalInput(option, field, value) {
option[field] = this.normalizeDecimalInput(value);
},
normalizeDecimalInput(value) {
const source = String(value ?? '').replace(/[^\d.]/g, '');
const [integer = '', ...decimalParts] = source.split('.');
const decimal = decimalParts.join('').slice(0, 2);
return decimalParts.length ? `${integer}.${decimal}` : integer;
},
toScoreValue(value) {
const number = Number(value);
return Number.isFinite(number) ? number.toFixed(2) : value;
},
prepareSubmitConfig(config) {
const payload = JSON.parse(JSON.stringify(config || {}));
(payload.categories || []).forEach(category => {
(category.items || []).forEach(item => {
item.score = this.toScoreValue(item.score);
item.baseValue =
this.normalizeOptionType(item.optionType) === 'score'
? this.normalizePlainDecimal(item.baseValue)
: null;
(item.options || []).forEach(option => {
option.score = this.toScoreValue(option.score);
if (this.normalizeOptionType(item.optionType) === 'score') {
option.changeValue = this.toScoreValue(option.changeValue);
}
});
});
});
(payload.standards || []).forEach(standard => {
standardNumberFields.forEach(field => {
standard[field] = this.normalizePlainDecimal(standard[field]);
});
});
return payload;
},
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;
}
if (this.itemForm.optionType === 'score') {
if (this.itemForm.options.length !== 1) {
this.$message.warning('分值类型只能配置1条阶梯配置');
return;
}
if (!this.isAnyNumber(this.itemForm.baseValue)) {
this.$message.warning('请输入基准数值');
return;
}
const invalidScoreOption = this.itemForm.options.some(
option =>
!option.changeType ||
!this.isPositiveDecimal(option.changeValue) ||
!option.changeUnit ||
!option.scoreType ||
!this.isNonNegativeDecimal(option.score)
);
if (invalidScoreOption) {
this.$message.warning('请完整填写基础分值,并确保数值不能为负数且最多两位小数');
return;
}
} else {
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.optionType = this.normalizeOptionType(item.optionType);
item.baseValue =
item.optionType === 'score' ? this.normalizePlainDecimal(item.baseValue) : null;
item.options = item.options.map(option => ({
...option,
optionName: String(option.optionName || '').trim(),
score: this.toScoreValue(option.score),
...(item.optionType === 'score' ? { changeValue: this.toScoreValue(option.changeValue) } : {}),
}));
item.score = this.toScoreValue(item.score);
if (this.currentItemIndex >= 0) {
this.currentCategory.items.splice(this.currentItemIndex, 1, item);
} else {
this.currentCategory.items.push(item);
}
this.itemBox = false;
},
removeItem(index) {
if (this.currentCategory.items.length <= 1) {
this.$message.warning('评分类目详情至少保留1条评分项目');
return;
}
this.currentCategory.items.splice(index, 1);
},
addOption() {
this.itemForm.options.push(this.emptyOption());
},
removeOption(index) {
this.itemForm.options.splice(index, 1);
},
openStandard(row, index = -1) {
this.currentStandardIndex = index;
this.standardForm = row
? this.normalizeStandard(JSON.parse(JSON.stringify(row)))
: this.emptyStandard();
this.standardBox = true;
},
hasDuplicateCreditLevel(creditLevel, currentIndex = -1) {
const level = String(creditLevel || '').trim();
return this.configForm.standards.some(
(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 = '';
}
},
handleStandardNumberInput(field, value, precision = 0, max) {
let normalized = String(value || '').replace(precision > 0 ? /[^\d.]/g : /\D/g, '');
if (precision > 0) {
const [integer = '', ...decimalParts] = normalized.split('.');
const decimal = decimalParts.join('').slice(0, precision);
normalized = decimalParts.length ? `${integer}.${decimal}` : integer;
}
if (max !== undefined && normalized !== '' && Number(normalized) > max) {
normalized = String(max);
}
this.standardForm[field] = normalized;
},
isValidCreditLevel(creditLevel) {
return this.creditLevelOptions.some(item => item.value === String(creditLevel || '').trim());
},
saveStandard() {
if (!this.standardForm.creditLevel) {
this.$message.warning('请选择信用等级');
return;
}
if (!this.isValidCreditLevel(this.standardForm.creditLevel)) {
this.$message.warning('信用等级只能选择A/B/C/D/E/F');
return;
}
if (this.hasDuplicateCreditLevel(this.standardForm.creditLevel, this.currentStandardIndex)) {
this.$message.warning('信用等级不能重复');
return;
}
if (
[
this.standardForm.scoreRateLower,
this.standardForm.scoreRateUpper,
this.standardForm.scoreRateIncrease,
this.standardForm.creditLimitLower,
this.standardForm.creditLimitUpper,
this.standardForm.creditLimitIncrease,
].some(value => value === undefined || value === null || value === '')
) {
this.$message.warning('请完整填写评估标准必填项');
return;
}
if (Number(this.standardForm.scoreRateLower) >= Number(this.standardForm.scoreRateUpper)) {
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();
standardNumberFields.forEach(field => {
standard[field] = Number(standard[field]);
});
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 {
this.configForm.standards.push(standard);
}
this.standardBox = false;
},
removeStandard(index) {
this.$confirm('确定删除该评估标准?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
this.configForm.standards.splice(index, 1);
});
},
isBlankValue(value) {
return value === undefined || value === null || value === '';
},
isPositiveDecimal(value) {
const number = Number(value);
if (!Number.isFinite(number) || number <= 0) return false;
return Math.abs(number * 100 - Math.round(number * 100)) < 1e-8;
},
isNonNegativeDecimal(value) {
if (value === undefined || value === null || String(value).trim() === '') return false;
const number = Number(value);
if (!Number.isFinite(number) || number < 0) return false;
return Math.abs(number * 100 - Math.round(number * 100)) < 1e-8;
},
isAnyNumber(value) {
const text = String(value ?? '').trim();
return /^[-+]?(?:\d+(?:\.\d*)?|\.\d+)$/.test(text) && Number.isFinite(Number(text));
},
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('请输入评定表名称');
return false;
}
const basic = this.configForm.categories.find(item => item.categoryCode === 'basic');
if (!basic || basic.items.length === 0) {
this.$message.warning('至少配置1个基础得分项目');
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条档位选项`);
return false;
}
if (this.normalizeOptionType(item.optionType) === 'score') {
if (item.options.length !== 1) {
this.$message.warning(`${item.itemName || '评分项目'}的分值类型只能配置1条阶梯配置`);
return false;
}
if (!this.isAnyNumber(item.baseValue)) {
this.$message.warning(`${item.itemName || '评分项目'}的基准数值不能为空且必须为数字`);
return false;
}
if (
item.options.some(
option =>
!option.changeType ||
!this.isPositiveDecimal(option.changeValue) ||
!this.scoreUnitOptions.includes(option.changeUnit) ||
!option.scoreType ||
!this.isNonNegativeDecimal(option.score)
)
) {
this.$message.warning(`${item.itemName || '评分项目'}的基础分值配置不完整`);
return false;
}
}
}
}
const creditLevels = this.configForm.standards
.map(item => String(item.creditLevel || '').trim())
.filter(Boolean);
if (creditLevels.some(level => !this.isValidCreditLevel(level))) {
this.$message.warning('评估标准中的信用等级只能选择A/B/C/D/E/F');
return false;
}
if (new Set(creditLevels).size !== creditLevels.length) {
this.$message.warning('评估标准中的信用等级不能重复');
return false;
}
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;
}
if (this.normalizeOptionType(item.optionType) === 'score') {
if (item.options.length !== 1) {
this.$message.warning(`${item.itemName || '评分项目'}的分值类型只能配置1条阶梯配置`);
return false;
}
if (!this.isAnyNumber(item.baseValue)) {
this.$message.warning(`${item.itemName || '评分项目'}的基准数值不能为空且必须为数字`);
return false;
}
if (
item.options.some(
option =>
!option.changeType ||
!this.isPositiveDecimal(option.changeValue) ||
!this.scoreUnitOptions.includes(option.changeUnit) ||
!option.scoreType ||
!this.isNonNegativeDecimal(option.score)
)
) {
this.$message.warning(`${item.itemName || '评分项目'}的基础分值配置不完整`);
return false;
}
} else {
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;
}
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 (standardNumberFields.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.prepareSubmitConfig(this.configForm),
status: 3,
}).then(() => {
this.configBox = false;
this.onLoad(this.page);
this.$message({ type: 'success', message: '操作成功!' });
});
},
rowDel(row) {
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => remove(row.id))
.then(() => {
this.onLoad(this.page);
this.$message({ type: 'success', message: '操作成功!' });
});
},
handleDelete() {
if (this.selectionList.length === 0) {
this.$message.warning('请选择至少一条数据');
return;
}
this.$confirm('确定删除所选评分量化表?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => remove(this.ids))
.then(() => {
this.onLoad(this.page);
this.$message({ type: 'success', message: '操作成功!' });
this.$refs.crud.toggleSelection();
});
},
handlePublish(row) {
this.$confirm('发布前将校验所有评分项目、档位选项和评估标准,是否继续?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.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: '发布成功!' });
});
},
handleStatus(row) {
const nextStatus = row.status === 1 ? 2 : 1;
const actionName = nextStatus === 1 ? '启用' : '停用';
this.$confirm(`是否确认${actionName}该评分量化表?`, {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => changeStatus(row.id, nextStatus))
.then(() => {
this.onLoad(this.page);
this.$message({ type: 'success', message: '操作成功!' });
});
},
searchReset() {
this.query = {};
this.onLoad(this.page);
},
searchChange(params, done) {
this.query = this.buildQuery(params);
this.page.currentPage = 1;
this.onLoad(this.page);
done();
},
selectionChange(list) {
this.selectionList = list;
},
currentChange(currentPage) {
this.page.currentPage = currentPage;
},
sizeChange(pageSize) {
this.page.pageSize = pageSize;
},
refreshChange() {
this.onLoad(this.page, this.query);
},
onLoad(page, params = {}) {
this.loading = true;
getList(page.currentPage, page.pageSize, this.buildQuery(params)).then(res => {
const data = res.data.data;
this.page.total = data.total;
this.data = data.records;
this.loading = false;
});
},
buildQuery(params = {}) {
return normalizeSearchRangeParams({ ...params, ...this.query }, createTimeRangeMap);
},
},
};
</script>
<style lang="scss" scoped>
.score-config {
&__name-input {
width: min(420px, 100%);
}
}
.archive-form {
:deep(.el-form-item__label) {
flex: 0 0 calc(6em + 24px) !important;
width: calc(6em + 24px) !important;
height: auto;
white-space: normal !important;
word-break: break-all;
overflow-wrap: anywhere;
line-height: 18px;
}
:deep(.el-form-item) {
align-items: center;
}
:deep(.el-form-item__content > .el-input),
:deep(.el-form-item__content > .el-select),
:deep(.el-form-item__content > .el-cascader),
:deep(.el-form-item__content > .el-input-number),
:deep(.el-form-item__content > .el-date-editor) {
width: 250px;
max-width: 100%;
}
}
.config-table {
width: 100%;
}
.category-line {
margin: 0 0 24px;
font-size: 16px;
font-weight: 400;
color: #606266;
}
:deep(.category-dialog .el-dialog__title) {
font-weight: 700;
}
.score-item-form {
width: 100%;
max-width: 1400px;
margin: 0 auto;
padding: 4px 0 6px;
:deep(.el-form-item) {
margin-bottom: 14px;
}
: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;
}
:deep(.score-description-input .el-input__inner) {
text-align: left;
}
&__top {
display: grid;
grid-template-columns: 360px 360px;
column-gap: 112px;
:deep(.el-form-item) {
margin-bottom: 14px;
}
}
}
.option-editor {
width: 100%;
}
.option-row {
display: grid;
grid-template-columns: minmax(560px, 2fr) 200px 74px;
gap: 24px;
align-items: center;
margin-bottom: 14px;
}
.option-row--score {
grid-template-columns: 150px minmax(220px, 1fr) 110px 180px 74px;
gap: 16px;
.option-score {
min-width: 0;
}
}
.option-change-type,
.option-score-type {
width: 100%;
}
.option-unit-select {
width: 78px;
:deep(.el-input__wrapper) {
border: 0;
border-left: 1px solid #dcdfe6;
border-radius: 0;
box-shadow: none;
}
}
.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;
gap: 10px;
}
.standard-range-input {
width: 100%;
min-width: 0;
:deep(.el-input) {
flex: 1;
width: 0;
min-width: 0;
}
}
.credit-limit-range {
width: 520px;
max-width: 100%;
:deep(.el-input) {
width: 250px;
flex: none;
}
}
.score-rate-range {
width: 250px;
max-width: 100%;
}
.credit-increase-input {
}
.score-unit {
margin-left: 8px;
color: #606266;
}
:deep(.score-dialog .el-dialog__body) {
max-height: 70vh;
overflow: auto;
}
:deep(.score-config__name-input) {
width: 500px;
}
: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>