调整评分量化表

This commit is contained in:
2026-08-25 03:25:56 +08:00
parent e5992a0c63
commit 9405c51de8
2 changed files with 638 additions and 276 deletions
+308 -44
View File
@@ -129,7 +129,13 @@
<section-card title="评估标准">
<template #extra>
<el-button type="primary" plain icon="el-icon-plus" v-if="!readonly" @click="openStandard()">
<el-button
type="primary"
plain
icon="el-icon-plus"
v-if="!readonly"
@click="openStandard()"
>
添加评估标准
</el-button>
</template>
@@ -166,7 +172,13 @@
<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
type="primary"
plain
icon="el-icon-plus"
v-if="!readonly"
@click="openItem()"
>
添加评分项目
</el-button>
</template>
@@ -218,23 +230,93 @@
<el-input v-model="itemForm.itemName" maxlength="20" />
</el-form-item>
<el-form-item label="分值" required>
<el-input-number v-model="itemForm.score" :min="1" :precision="0" :step="1" :controls="false" />
<el-input-number
v-model="itemForm.score"
:min="0.01"
: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-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-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" v-for="(option, index) in itemForm.options" :key="index">
<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-number v-model="option.score" :min="1" :precision="0" :step="1" :controls="false" />
<span class="score-unit"></span>
<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"
@@ -244,10 +326,8 @@
删除
</el-link>
</div>
<div class="option-add">
<el-link type="primary" v-if="!readonly" @click="addOption">
+添加选项
</el-link>
<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>
@@ -267,7 +347,13 @@
: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-form
:model="standardForm"
label-position="right"
label-width="auto"
:disabled="readonly"
class="standard-form archive-form"
>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="信用等级" required>
@@ -413,6 +499,7 @@ export default {
label: item,
value: item,
})),
scoreUnitOptions: ['%', '件', '次', '项', '天'],
option: {
height: 'auto',
calcHeight: 32,
@@ -536,7 +623,19 @@ export default {
score: 1,
scoreDescription: '',
optionDescription: '',
options: [{ optionName: '', score: 1 }],
optionType: 'option',
baseValue: '',
options: [this.emptyOption()],
};
},
emptyOption() {
return {
optionName: '',
score: 1,
changeType: 'increase',
changeValue: '',
changeUnit: '%',
scoreType: 'add',
};
},
emptyStandard() {
@@ -559,14 +658,20 @@ export default {
...category,
items: (category.items || []).map(item => ({
...item,
optionType: this.normalizeOptionType(item.optionType),
options:
item.options && item.options.length ? item.options : [{ optionName: '', score: 1 }],
item.options && item.options.length
? item.options.map(option => this.normalizeOption(option))
: [this.emptyOption()],
})),
};
});
return {
...detail,
categories: categories.map(category => map[category.categoryCode] || category),
categories: categories.map(category => ({
...(map[category.categoryCode] || category),
categoryName: category.categoryName,
})),
standards: detail.standards || [],
};
},
@@ -596,10 +701,84 @@ export default {
openItem(row, index = -1) {
this.currentItemIndex = index;
this.itemForm = row
? { ...this.emptyItem(), ...JSON.parse(JSON.stringify(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 = {}) {
return {
...this.emptyItem(),
...item,
optionType: this.normalizeOptionType(item.optionType),
options:
item.options && item.options.length
? item.options.map(option => this.normalizeOption(option))
: [this.emptyOption()],
};
},
normalizeOption(option = {}) {
const normalized = { ...this.emptyOption(), ...option };
return {
...normalized,
changeType:
normalized.changeType === 'decrease' ||
normalized.changeType === '每减少' ||
normalized.changeType === 2
? 'decrease'
: 'increase',
scoreType:
normalized.scoreType === 'subtract' ||
normalized.scoreType === '减' ||
normalized.scoreType === 2
? 'subtract'
: 'add',
};
},
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.options || []).forEach(option => {
option.score = this.toScoreValue(option.score);
if (this.normalizeOptionType(item.optionType) === 'score') {
option.changeValue = this.toScoreValue(option.changeValue);
}
});
});
});
return payload;
},
hasDuplicateItemName(itemName, currentIndex = -1) {
const name = String(itemName || '').trim();
return this.currentCategory.items.some(
@@ -623,14 +802,32 @@ export default {
this.$message.warning('请输入分值');
return;
}
if (!this.isPositiveInteger(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.isPositiveDecimal(option.score)
);
if (invalidScoreOption) {
this.$message.warning('请完整填写基础分值,并确保数值为正数且最多两位小数');
return;
}
} else {
const invalidOption = this.itemForm.options.some(
option =>
!String(option.optionName || '').trim() ||
@@ -642,18 +839,18 @@ export default {
this.$message.warning('请完整填写档位选项');
return;
}
if (this.itemForm.options.some(option => !this.isPositiveInteger(option.score))) {
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' ? String(item.baseValue).trim() : null;
item.options = item.options.map(option => ({
...option,
optionName: String(option.optionName || '').trim(),
score: Number(option.score),
score: this.toScoreValue(option.score),
...(item.optionType === 'score' ? { changeValue: this.toScoreValue(option.changeValue) } : {}),
}));
item.score = Number(item.score);
item.score = this.toScoreValue(item.score);
if (this.currentItemIndex >= 0) {
this.currentCategory.items.splice(this.currentItemIndex, 1, item);
} else {
@@ -669,7 +866,7 @@ export default {
this.currentCategory.items.splice(index, 1);
},
addOption() {
this.itemForm.options.push({ optionName: '', score: 1 });
this.itemForm.options.push(this.emptyOption());
},
removeOption(index) {
this.itemForm.options.splice(index, 1);
@@ -788,8 +985,20 @@ export default {
isBlankValue(value) {
return value === undefined || value === null || value === '';
},
isPositiveInteger(value) {
return /^\d+$/.test(String(value)) && Number(value) > 0;
isPositiveDecimal(value) {
const number = Number(value);
console.log(
value,
Number.isFinite(number),
number,
Math.abs(number * 100 - Math.round(number * 100))
);
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
@@ -822,18 +1031,33 @@ export default {
return false;
}
for (const item of category.items) {
if (!this.isPositiveInteger(item.score)) {
this.$message.warning(`${item.itemName || '评分项目'}的分值只能为正整数`);
return false;
}
if (!item.options || item.options.length === 0) {
this.$message.warning(`${item.itemName}至少存在1条档位选项`);
return false;
}
if (item.options.some(option => !this.isPositiveInteger(option.score))) {
this.$message.warning(`${item.itemName || '评分项目'}的选项分数只能为正整数`);
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.isPositiveDecimal(option.score)
)
) {
this.$message.warning(`${item.itemName || '评分项目'}的基础分值配置不完整`);
return false;
}
}
}
}
const creditLevels = this.configForm.standards
@@ -862,14 +1086,33 @@ export default {
this.$message.warning('发布前请完整填写评分项目名称');
return false;
}
if (!this.isPositiveInteger(item.score)) {
this.$message.warning(`${item.itemName || '评分项目'}的分值只能为正整数`);
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.isPositiveDecimal(option.score)
)
) {
this.$message.warning(`${item.itemName || '评分项目'}的基础分值配置不完整`);
return false;
}
} else {
const invalidOption = item.options.some(
option => !String(option.optionName || '').trim() || this.isBlankValue(option.score)
);
@@ -877,9 +1120,6 @@ export default {
this.$message.warning(`${item.itemName || '评分项目'}的档位选项未填写完整`);
return false;
}
if (item.options.some(option => !this.isPositiveInteger(option.score))) {
this.$message.warning(`${item.itemName || '评分项目'}的选项分数只能为正整数`);
return false;
}
}
}
@@ -938,7 +1178,7 @@ export default {
saveConfig() {
if (!this.validateConfig()) return;
submit({
...this.configForm,
...this.prepareSubmitConfig(this.configForm),
status: 3,
}).then(() => {
this.configBox = false;
@@ -1157,6 +1397,31 @@ export default {
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;
@@ -1278,5 +1543,4 @@ export default {
margin-left: 12px;
}
}
</style>
+111 -13
View File
@@ -1210,6 +1210,17 @@
<el-table-column label="评分标准" min-width="430">
<template #default="{ row: detail }">
<div class="score-standard-cell">
<template v-if="isScoreTypeDetail(detail)">
<el-input
:model-value="detail.scoreInput"
inputmode="decimal"
:disabled="readonly"
@input="value => handleScoreValueInput(detail, value)"
>
<template #append>{{ getScoreRule(detail)?.changeUnit || '' }}</template>
</el-input>
</template>
<template v-else>
<div class="score-standard-cell__label">请选择:</div>
<el-select
v-model="detail.selectedOption"
@@ -1224,6 +1235,7 @@
:value="item.value"
/>
</el-select>
</template>
</div>
</template>
</el-table-column>
@@ -2154,8 +2166,8 @@ export default {
const scoreDetails = this.normalizeChangeFieldValue(score.details);
const details = (Array.isArray(scoreDetails) ? scoreDetails : [])
.map(detail => {
const category =
detail.categoryName || categoryNames[this.getScoreDetailCategoryCode(detail)] || '';
const categoryCode = this.getScoreDetailCategoryCode(detail);
const category = categoryNames[categoryCode] || detail.categoryName || '';
const itemName = detail.itemName || '评分项目';
const option = detail.selectedOption || detail.scoreDescription || '';
const points = [
@@ -3537,7 +3549,10 @@ export default {
quantificationId: score.quantificationId ? String(score.quantificationId) : '',
tempApplyCreditLimit: this.normalizeOptionalAmount(score.tempApplyCreditLimit),
attachments: score.attachments || this.parseAttachments(score.proofAttachments),
details: score.details || [],
details: (score.details || []).map(detail => ({
...detail,
scoreInput: this.normalizeScoreInputValue(detail.scoreInput),
})),
};
},
normalizeOptionalAmount(value) {
@@ -3836,6 +3851,7 @@ export default {
...item,
categoryCode: category.categoryCode,
categoryName: category.categoryName,
scoreInput: this.normalizeScoreInputValue(item.scoreInput),
optionsJson:
item.optionsJson ||
JSON.stringify(
@@ -3843,6 +3859,10 @@ export default {
label: option.label || option.optionName || option.value,
value: option.value || option.optionName || option.label,
score: option.score,
changeType: option.changeType,
changeValue: option.changeValue,
changeUnit: option.changeUnit,
scoreType: option.scoreType,
}))
),
}))
@@ -3862,9 +3882,16 @@ export default {
...detail,
categoryCode: detail.categoryCode || category?.categoryCode || '',
categoryName: detail.categoryName || category?.categoryName || '',
scoreInput: this.normalizeScoreInputValue(detail.scoreInput),
};
});
},
normalizeScoreInputValue(value) {
if (value === undefined || value === null || value === '' || value === -1 || value === '-1') {
return '';
}
return String(value);
},
getScoreDetailCategoryCode(detail = {}) {
const rawCode = String(
detail.categoryCode ||
@@ -3916,12 +3943,24 @@ export default {
parseScoreOptions(value) {
if (!value) return [];
const normalize = list =>
list.map(item => ({
list.map(item => {
const isScoreOption = item.changeType || item.changeValue || item.changeUnit;
const changeLabel = item.changeType === 'decrease' ? '每减少' : '每增加';
const scoreLabel = item.scoreType === 'subtract' ? '减' : '加';
const generatedLabel = isScoreOption
? `${changeLabel}${item.changeValue || ''}${item.changeUnit || ''}${scoreLabel}${item.score || ''}分`
: '';
const label = item.label || item.optionName || item.value || generatedLabel;
return {
...item,
label: item.label || item.optionName || item.value,
value: item.value || item.optionName || item.label,
score: item.score,
}));
label,
value: item.value || item.optionName || item.label || generatedLabel,
score:
item.scoreType === 'subtract' && item.score !== undefined
? -Math.abs(Number(item.score))
: item.score,
};
});
if (Array.isArray(value)) return normalize(value);
try {
const options = JSON.parse(value);
@@ -3930,6 +3969,50 @@ export default {
return [];
}
},
isScoreTypeDetail(detail = {}) {
return String(detail.optionType || '').toLowerCase() === 'score';
},
getScoreRule(detail = {}) {
return this.parseScoreOptions(detail.optionsJson)[0] || null;
},
handleScoreValueInput(detail, value) {
const raw = String(value ?? '')
.replace(/[^\d.-]/g, '')
.replace(/(?!^)-/g, '')
.replace(/(\..*)\./g, '$1');
const sign = raw.startsWith('-') ? '-' : '';
const unsigned = raw.replace(/^-/, '');
const hasDecimal = unsigned.includes('.');
const [integerPart, decimalPart = ''] = unsigned.split('.');
const normalized = `${sign}${integerPart}${hasDecimal ? `.${decimalPart.slice(0, 2)}` : ''}`;
detail.scoreInput = normalized;
const rule = this.getScoreRule(detail);
if (!normalized || normalized === '-' || normalized === '.') {
detail.selfScore = '';
this.calculateScore(this.currentScore);
return;
}
const base = Number(detail.baseValue);
const input = Number(normalized);
if (!rule || !Number.isFinite(base) || !Number.isFinite(input)) {
detail.selfScore = '';
this.calculateScore(this.currentScore);
return;
}
const increase = rule.changeType !== 'decrease';
const valid = increase ? input >= base : input <= base;
if (!valid) {
detail.selfScore = '';
this.$message.warning(`输入值${increase ? '不能小于' : '不能大于'}基准数值${base}`);
this.calculateScore(this.currentScore);
return;
}
const distance = increase ? input - base : base - input;
const stepScore = Math.abs(Number(rule.score || 0));
const signedStep = rule.scoreType === 'subtract' ? -stepScore : stepScore;
detail.selfScore = Number((Number(detail.score || 0) + distance * signedStep).toFixed(2));
this.calculateScore(this.currentScore);
},
scoreOptionChange(detail, optionValue) {
const option = this.parseScoreOptions(detail.optionsJson).find(
item => item.value === optionValue
@@ -4076,9 +4159,13 @@ export default {
return score;
},
hasIncompleteBasicScoreDetail(details = []) {
return this.getScoreCategoryDetailsByList(details, 'basic').some(
item => !item.selectedOption
);
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 === '';
}
return !item.selectedOption;
});
},
finishScore(score) {
if (!score.details || !score.details.length) {
@@ -4086,7 +4173,7 @@ export default {
return;
}
if (this.hasIncompleteBasicScoreDetail(score.details)) {
this.$message.warning('请完整选择基础得分项评分明细选项');
this.$message.warning('请完整填写基础得分项评分明细');
return;
}
this.calculateScore(score);
@@ -4148,6 +4235,17 @@ export default {
this.$message.warning('当前评分量化表未配置评分项目');
return false;
}
const invalidScoreInput = this.currentScore.details.find(item => {
if (!this.isScoreTypeDetail(item)) return false;
const input = String(item.scoreInput ?? '').trim();
return input !== '' && item.selfScore === '';
});
if (invalidScoreInput) {
this.$message.warning(
`${invalidScoreInput.itemName || '评分项目'}的输入值不符合基准数值限制,请重新填写`
);
return false;
}
const overMaxReviewScore = this.currentScore.details.find(item => {
if (
item.reviewScore === undefined ||
@@ -4169,7 +4267,7 @@ export default {
return false;
}
if (requireComplete && this.hasIncompleteBasicScoreDetail(this.currentScore.details)) {
this.$message.warning('请完整选择基础得分项评分明细选项');
this.$message.warning('请完整填写基础得分项评分明细');
return false;
}
return true;