✨ 评分减分累计与最高扣分、用户部门标签联动树,并修复对账导入与结算调整等
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -101,6 +101,17 @@
|
||||
style="width: 500px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="减分最高扣分">
|
||||
<el-input
|
||||
:model-value="configForm.maxMinusScore"
|
||||
placeholder="所有减分项合计不得超过该分值"
|
||||
clearable
|
||||
style="width: 500px"
|
||||
@input="handleMaxMinusScoreInput"
|
||||
>
|
||||
<template #append>分</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input
|
||||
v-model="configForm.remark"
|
||||
@@ -628,6 +639,7 @@ export default {
|
||||
name: '',
|
||||
remark: '',
|
||||
standardDescription: '',
|
||||
maxMinusScore: '',
|
||||
status: 3,
|
||||
categories: this.defaultCategories(),
|
||||
standards: [],
|
||||
@@ -684,6 +696,7 @@ export default {
|
||||
});
|
||||
return {
|
||||
...detail,
|
||||
maxMinusScore: this.normalizePlainDecimal(detail.maxMinusScore),
|
||||
categories: categories.map(category => ({
|
||||
...(map[category.categoryCode] || category),
|
||||
categoryName: category.categoryName,
|
||||
@@ -691,6 +704,14 @@ export default {
|
||||
standards: (detail.standards || []).map(standard => this.normalizeStandard(standard)),
|
||||
};
|
||||
},
|
||||
handleMaxMinusScoreInput(value) {
|
||||
const raw = String(value ?? '')
|
||||
.replace(/[^\d.]/g, '')
|
||||
.replace(/^\./, '')
|
||||
.replace(/(\..*)\./g, '$1')
|
||||
.replace(/^(\d+)(\.\d{0,2})?.*$/, '$1$2');
|
||||
this.configForm.maxMinusScore = raw;
|
||||
},
|
||||
openConfig(row, readonly = false) {
|
||||
this.readonly = readonly;
|
||||
if (row && row.id) {
|
||||
@@ -816,6 +837,9 @@ export default {
|
||||
},
|
||||
prepareSubmitConfig(config) {
|
||||
const payload = JSON.parse(JSON.stringify(config || {}));
|
||||
const maxMinus = this.normalizePlainDecimal(payload.maxMinusScore);
|
||||
payload.maxMinusScore =
|
||||
maxMinus === '' || maxMinus == null ? null : this.toScoreValue(maxMinus);
|
||||
(payload.categories || []).forEach(category => {
|
||||
(category.items || []).forEach(item => {
|
||||
item.score = this.toScoreValue(item.score);
|
||||
@@ -1072,6 +1096,14 @@ export default {
|
||||
this.$message.warning('请输入评定表名称');
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
this.configForm.maxMinusScore !== '' &&
|
||||
this.configForm.maxMinusScore != null &&
|
||||
!this.isNonNegativeDecimal(this.configForm.maxMinusScore)
|
||||
) {
|
||||
this.$message.warning('减分最高扣分须为不小于0的数字,最多两位小数');
|
||||
return false;
|
||||
}
|
||||
const basic = this.configForm.categories.find(item => item.categoryCode === 'basic');
|
||||
if (!basic || basic.items.length === 0) {
|
||||
this.$message.warning('至少配置1个基础得分项目');
|
||||
|
||||
@@ -592,18 +592,27 @@
|
||||
<span>{{ row.reviewStatus }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="160" fixed="right" align="center">
|
||||
<el-table-column label="操作" min-width="220" 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>
|
||||
<div class="score-list-table__actions">
|
||||
<el-link type="primary" @click="openScore(row.__raw, row.__rawIndex)">
|
||||
详情
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="canReviewScore(row)"
|
||||
type="primary"
|
||||
@click="startReviewScore(row.__raw, row.__rawIndex)"
|
||||
>
|
||||
复评
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="showScoreDelete"
|
||||
type="danger"
|
||||
@click="deleteScore(row.__rawIndex)"
|
||||
>
|
||||
删除
|
||||
</el-link>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -2231,7 +2240,8 @@ export default {
|
||||
showScoreDelete() {
|
||||
if (this.readonly) return false;
|
||||
const status = this.archiveForm.approvalStatus || 'draft';
|
||||
return status === 'draft' || status === 'rejected';
|
||||
// 草稿/驳回可删;审批通过后也允许删除并重建评分表(旧评分逻辑删除保留,新评分审核通过前仍用旧额度)
|
||||
return status === 'draft' || status === 'rejected' || status === 'approved';
|
||||
},
|
||||
isNewScore() {
|
||||
return this.scoreRecordIndex < 0 && !this.reviewMode;
|
||||
@@ -2281,10 +2291,17 @@ export default {
|
||||
{ categoryCode: 'plus', categoryName: '加分项目' },
|
||||
{ categoryCode: 'minus', categoryName: '减分项目' },
|
||||
];
|
||||
const maxMinus = Number(this.currentScore?.maxMinusScore);
|
||||
const hasMaxMinus = Number.isFinite(maxMinus) && maxMinus >= 0;
|
||||
const rows = categoryMap.map((category, index) => {
|
||||
const details = this.getScoreCategoryDetails(category.categoryCode);
|
||||
const selfScore = details.reduce((sum, item) => sum + Number(item.selfScore || 0), 0);
|
||||
const reviewScore = details.reduce((sum, item) => sum + Number(item.reviewScore || 0), 0);
|
||||
let selfScore = details.reduce((sum, item) => sum + Number(item.selfScore || 0), 0);
|
||||
let reviewScore = details.reduce((sum, item) => sum + Number(item.reviewScore || 0), 0);
|
||||
// 减分项目合计受量化表「最高扣分」限制
|
||||
if (category.categoryCode === 'minus' && hasMaxMinus) {
|
||||
selfScore = Math.min(selfScore, maxMinus);
|
||||
reviewScore = Math.min(reviewScore, maxMinus);
|
||||
}
|
||||
return {
|
||||
...category,
|
||||
index: index + 1,
|
||||
@@ -4199,6 +4216,7 @@ export default {
|
||||
scoreDate: this.$dayjs().format('YYYY-MM-DD'),
|
||||
applyCreditLimit: this.archiveForm.applyCreditLimit || '',
|
||||
maxCreditLimit: '',
|
||||
maxMinusScore: '',
|
||||
creditLevel: '',
|
||||
standards: [],
|
||||
tempApplyCreditLimit: '',
|
||||
@@ -4218,6 +4236,10 @@ export default {
|
||||
: score.applyCreditLimit
|
||||
),
|
||||
maxCreditLimit: this.normalizeOptionalAmount(score.maxCreditLimit),
|
||||
maxMinusScore:
|
||||
score.maxMinusScore === undefined || score.maxMinusScore === null
|
||||
? ''
|
||||
: score.maxMinusScore,
|
||||
tempApplyCreditLimit: this.normalizeOptionalAmount(score.tempApplyCreditLimit),
|
||||
attachments: score.attachments || this.parseAttachments(score.proofAttachments),
|
||||
details: (score.details || []).map(detail => ({
|
||||
@@ -4256,7 +4278,14 @@ export default {
|
||||
String(item.score.creditLevel || '').trim()
|
||||
)[0]?.score;
|
||||
},
|
||||
/** 曾审核通过后:删除/重建评分在再次审核通过前,主表等级与额度保持旧值暂时生效 */
|
||||
shouldFreezeEffectiveCredit(archive = this.archiveForm) {
|
||||
return Boolean(archive?.approvedTime);
|
||||
},
|
||||
applyArchiveCreditFromScores(archive) {
|
||||
if (this.shouldFreezeEffectiveCredit(archive)) {
|
||||
return archive;
|
||||
}
|
||||
const latestScore = this.getLatestCreditScore(archive.scores || []);
|
||||
archive.customerLevel = latestScore?.creditLevel || archive.customerLevel || '';
|
||||
archive.maxCreditLimit = latestScore?.maxCreditLimit || archive.maxCreditLimit || '';
|
||||
@@ -4588,9 +4617,31 @@ export default {
|
||||
}
|
||||
this.$nextTick(() => this.openScore(scores[0], 0));
|
||||
},
|
||||
/** 提交后详情页评分表:审批中且未完成复评时可复评 */
|
||||
canReviewScore(row = {}) {
|
||||
return (
|
||||
this.hasPermission('customer_type_re_cert') &&
|
||||
this.archiveForm.approvalStatus === 'reviewing' &&
|
||||
row.reviewStatus !== '已完成'
|
||||
);
|
||||
},
|
||||
startReviewScore(row, index = 0) {
|
||||
if (!row) {
|
||||
this.$message.warning('暂无评分记录,无法复评');
|
||||
return;
|
||||
}
|
||||
// 详情只读进入时也要能改复评得分并确认
|
||||
this.reviewMode = true;
|
||||
this.openScore(row, index);
|
||||
},
|
||||
addScore() {
|
||||
if ((this.archiveForm.scores || []).length) {
|
||||
this.$message.warning('请勿重复添加');
|
||||
const existingScores = this.archiveForm.scores || [];
|
||||
if (existingScores.length) {
|
||||
this.$message.warning(
|
||||
this.shouldFreezeEffectiveCredit()
|
||||
? '请先删除原评分记录后再添加新评分表'
|
||||
: '请勿重复添加'
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.scoreRecordIndex = -1;
|
||||
@@ -4611,9 +4662,9 @@ export default {
|
||||
const loadOptions = this.scoreQuantificationOptions.length
|
||||
? Promise.resolve()
|
||||
: this.initScoreQuantificationOptions();
|
||||
loadOptions.then(() =>
|
||||
this.ensureScoreQuantificationOption(this.currentScore.quantificationId)
|
||||
);
|
||||
loadOptions
|
||||
.then(() => this.ensureScoreQuantificationOption(this.currentScore.quantificationId))
|
||||
.then(() => this.applyMaxMinusScoreFromQuantification(this.currentScore.quantificationId));
|
||||
},
|
||||
resetScoreDialog() {
|
||||
this.scoreRecordIndex = -1;
|
||||
@@ -4648,6 +4699,11 @@ export default {
|
||||
applyCreditLimit: this.currentScore.applyCreditLimit,
|
||||
maxCreditLimit: '',
|
||||
creditLevel: '',
|
||||
maxMinusScore:
|
||||
quantificationData.maxMinusScore ??
|
||||
scoreTemplate.maxMinusScore ??
|
||||
this.currentScore.maxMinusScore ??
|
||||
'',
|
||||
tempApplyCreditLimit: this.currentScore.tempApplyCreditLimit,
|
||||
tempCreditStartDate: this.currentScore.tempCreditStartDate,
|
||||
tempCreditEndDate: this.currentScore.tempCreditEndDate,
|
||||
@@ -4664,6 +4720,17 @@ export default {
|
||||
this.calculateScore(this.currentScore);
|
||||
});
|
||||
},
|
||||
applyMaxMinusScoreFromQuantification(quantificationId) {
|
||||
if (!quantificationId) return Promise.resolve();
|
||||
return getCreditScoreQuantificationDetail(quantificationId).then(res => {
|
||||
const data = res?.data?.data || {};
|
||||
this.currentScore.maxMinusScore =
|
||||
data.maxMinusScore === undefined || data.maxMinusScore === null
|
||||
? ''
|
||||
: data.maxMinusScore;
|
||||
this.calculateScore(this.currentScore);
|
||||
});
|
||||
},
|
||||
normalizeScoreTemplateDetails(template = {}) {
|
||||
const categoryDetails = (template.categories || []).flatMap(category =>
|
||||
(category.items || []).map(item => ({
|
||||
@@ -4808,13 +4875,24 @@ export default {
|
||||
if (!valid) return null;
|
||||
|
||||
const distance = increase ? input - base : base - input;
|
||||
const changeValue = Number(rule.changeValue);
|
||||
const stepSize = Number.isFinite(changeValue) && changeValue > 0 ? changeValue : 1;
|
||||
const steps = Math.floor(distance / stepSize + 1e-9);
|
||||
const stepScore = Math.abs(Number(rule.score || 0));
|
||||
if (!Number.isFinite(stepScore)) return null;
|
||||
if (!Number.isFinite(stepScore) || steps < 0) return null;
|
||||
|
||||
const projectScore = this.getScoreItemFullScore(detail);
|
||||
// 减分项:按阶梯累计扣分,存正数,总分计算时再减;不做项目满分截断
|
||||
if (this.isMinusScoreDetail(detail)) {
|
||||
return Number((steps * stepScore).toFixed(2));
|
||||
}
|
||||
|
||||
// 基础/加分项:从项目分值起步,按阶梯加减,限制在 [0, 项目满分]
|
||||
const baseScore = Number(detail.score || 0);
|
||||
const signedStep = rule.scoreType === 'subtract' ? -stepScore : stepScore;
|
||||
const calculatedScore = Number(detail.score || 0) + distance * signedStep;
|
||||
return Number(Math.min(Math.max(calculatedScore, 0), projectScore).toFixed(2));
|
||||
const calculatedScore = baseScore + steps * signedStep;
|
||||
const projectScore = this.getScoreItemFullScore(detail);
|
||||
const upper = Math.max(projectScore, baseScore, 0);
|
||||
return Number(Math.min(Math.max(calculatedScore, 0), upper).toFixed(2));
|
||||
},
|
||||
handleScoreValueInput(detail, value) {
|
||||
const raw = String(value ?? '')
|
||||
@@ -4995,14 +5073,17 @@ export default {
|
||||
const plusDetails = this.getScoreCategoryDetailsByList(details, 'plus');
|
||||
const minusDetails = this.getScoreCategoryDetailsByList(details, 'minus');
|
||||
const sumScore = (list, prop) => list.reduce((sum, item) => sum + Number(item[prop] || 0), 0);
|
||||
const rawMinusSelf = sumScore(minusDetails, 'selfScore');
|
||||
const rawMinusReview = sumScore(minusDetails, 'reviewScore');
|
||||
// 量化表「减分最高扣分」:所有减分项合计不得超过该上限
|
||||
const maxMinus = Number(score.maxMinusScore);
|
||||
const hasMaxMinus = Number.isFinite(maxMinus) && maxMinus >= 0;
|
||||
const minusSelf = hasMaxMinus ? Math.min(rawMinusSelf, maxMinus) : rawMinusSelf;
|
||||
const minusReview = hasMaxMinus ? Math.min(rawMinusReview, maxMinus) : rawMinusReview;
|
||||
const selfScore =
|
||||
sumScore(basicDetails, 'selfScore') +
|
||||
sumScore(plusDetails, 'selfScore') -
|
||||
sumScore(minusDetails, 'selfScore');
|
||||
sumScore(basicDetails, 'selfScore') + sumScore(plusDetails, 'selfScore') - minusSelf;
|
||||
const reviewScore =
|
||||
sumScore(basicDetails, 'reviewScore') +
|
||||
sumScore(plusDetails, 'reviewScore') -
|
||||
sumScore(minusDetails, 'reviewScore');
|
||||
sumScore(basicDetails, 'reviewScore') + sumScore(plusDetails, 'reviewScore') - minusReview;
|
||||
const hasReviewScore = details.some(
|
||||
item => !this.isBlankScore(item.reviewScore)
|
||||
);
|
||||
@@ -5076,7 +5157,11 @@ export default {
|
||||
return pending;
|
||||
},
|
||||
deleteScore(index) {
|
||||
this.$confirm('确定删除该评分记录?', '提示', {
|
||||
const freezeCredit = this.shouldFreezeEffectiveCredit();
|
||||
const confirmText = freezeCredit
|
||||
? '确定删除该评分记录?删除后可重新添加评分表;原评分数据会保留,新评分再次审核通过前,客商等级与额度仍按原评分暂时生效。'
|
||||
: '确定删除该评分记录?';
|
||||
this.$confirm(confirmText, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
@@ -5089,16 +5174,23 @@ export default {
|
||||
} 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 = '';
|
||||
if (!freezeCredit) {
|
||||
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('评分记录已删除', '评分记录已删除,请保存客商档案后生效');
|
||||
this.saveScoreArchive(
|
||||
freezeCredit
|
||||
? '评分记录已删除,原等级与额度暂时继续生效'
|
||||
: '评分记录已删除',
|
||||
'评分记录已删除,请保存客商档案后生效'
|
||||
);
|
||||
})
|
||||
.catch(() => {});
|
||||
},
|
||||
@@ -5795,6 +5887,14 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.score-list-table__actions {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.contact-form,
|
||||
.receipt-form,
|
||||
.invoice-form {
|
||||
|
||||
Reference in New Issue
Block a user