From d4ae6d521a1459a51b20901939e6d6ece709ee7c Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Wed, 4 Nov 2026 23:46:20 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20=E5=AE=A2=E5=95=86=E8=AF=84?= =?UTF-8?q?=E5=88=86=E5=8F=8C=E6=A8=A1=E5=BC=8F=E4=B8=8E=E9=87=91=E8=9D=B6?= =?UTF-8?q?=E5=85=AC=E5=8F=B8=E4=BB=A3=E7=A0=81=E6=94=B9=E5=9B=9E=E7=BB=84?= =?UTF-8?q?=E7=BB=87=E7=BB=B4=E6=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- src/api/vehicle/customer-archive.js | 28 ++ src/views/business/contract-manage.vue | 2 +- src/views/system/business-dept-config.vue | 104 ++-- src/views/system/dept.vue | 55 +++ .../vehicle/credit-score-quantification.vue | 22 +- src/views/vehicle/customer-archive.vue | 466 +++++++++++++++--- 6 files changed, 577 insertions(+), 100 deletions(-) diff --git a/src/api/vehicle/customer-archive.js b/src/api/vehicle/customer-archive.js index 150ec85..6c9107d 100644 --- a/src/api/vehicle/customer-archive.js +++ b/src/api/vehicle/customer-archive.js @@ -152,6 +152,34 @@ export const getScoreTemplate = quantificationId => { }); }; +/** 线下评分 Excel 模板下载 */ +export const downloadScoreExcelTemplate = quantificationId => { + return request({ + url: '/blade-transport/customer-archive/score-excel-template', + method: 'get', + responseType: 'blob', + params: { + quantificationId, + }, + }); +}; + +/** 线下评分 Excel 导入(成功返回评分明细 JSON;失败返回失败明细 Excel 流) */ +export const importScoreExcel = (quantificationId, file) => { + const formData = new FormData(); + formData.append('file', file); + return request({ + url: '/blade-transport/customer-archive/score-import', + method: 'post', + data: formData, + responseType: 'blob', + timeout: 60000, + params: { + quantificationId, + }, + }); +}; + export const queryKingdeeCustomer = creditCode => { return request({ url: '/blade-transport/bfi/customer/query-by-credit-code', diff --git a/src/views/business/contract-manage.vue b/src/views/business/contract-manage.vue index a21f689..4b56676 100644 --- a/src/views/business/contract-manage.vue +++ b/src/views/business/contract-manage.vue @@ -3084,7 +3084,7 @@ export default { const message = error?.response?.data?.msg || error?.message || - '部门合同前缀未配置,无法自动生成,请手动输入合同编号'; + '业务部门配置中未维护该平台公司与业务部门的合同编码前缀,无法自动生成,请先在业务部门配置中维护或手动输入合同编号'; this.$message.warning(message); }); }, diff --git a/src/views/system/business-dept-config.vue b/src/views/system/business-dept-config.vue index 6a7a614..01f00a7 100644 --- a/src/views/system/business-dept-config.vue +++ b/src/views/system/business-dept-config.vue @@ -79,6 +79,13 @@ /> + @@ -185,8 +192,11 @@ export default { prop: 'kingdeeCompanyCode', minWidth: 140, span: 12, - maxlength: 64, - rules: [{ required: true, message: '请输入金蝶公司代码', trigger: 'blur' }], + formslot: true, + addDisplay: true, + editDisplay: true, + // 列表展示,维护入口在组织管理的平台公司 + hide: false, }, { label: '初始流水号', @@ -197,6 +207,7 @@ export default { minRows: 1, maxRows: 999, controls: false, + clearable: true, slot: true, placeholder: '如果系统已有相同流水号,则跳过这个流水号', }, @@ -250,6 +261,14 @@ export default { }; }, }, + watch: { + // Avue number 清空后可能回写成 -1,统一清成空,避免未填却落库 -1 + 'form.initialSerial'(value) { + if (value === -1 || value === '-1') { + this.form.initialSerial = undefined; + } + }, + }, created() { this.loadSelectOptions(); }, @@ -270,65 +289,94 @@ export default { return (list || []).map(item => ({ label: item.deptName || item.fullName || String(item.id), value: String(item.id), + kingdeeDeptCode: item.kingdeeDeptCode || '', })); }, formatSerial(value) { - if (value === null || value === undefined || value === '') return '-'; + // Avue number 空值常落成 -1,历史脏数据也按空展示 + if (value === null || value === undefined || value === '' || value === -1 || value === '-1') { + return ''; + } const number = Number(value); - if (!Number.isInteger(number) || number < 1) return String(value); + if (!Number.isInteger(number) || number < 1) return ''; return String(number).padStart(3, '0'); }, + /** 初始流水号空值归一:未填 / -1 都按空处理,禁止自动落库 -1 */ + normalizeInitialSerial(value) { + if (value === '' || value === undefined || value === null || value === -1 || value === '-1') { + return null; + } + const number = Number(value); + return Number.isFinite(number) ? number : null; + }, handlePlatformCompanyChange(value) { const matched = this.platformCompanyOptions.find(item => String(item.value) === String(value)); this.form.platformCompanyName = matched ? matched.label : ''; + // 只读回填组织中平台公司的金蝶公司代码 + this.form.kingdeeCompanyCode = matched ? matched.kingdeeDeptCode || '' : ''; }, handleBusinessDeptChange(value) { const matched = this.businessDeptOptions.find(item => String(item.value) === String(value)); this.form.businessDeptName = matched ? matched.label : ''; }, beforeOpen(done, type) { - this.loadSelectOptions(); + const loadOptions = Promise.all([getPlatformCompanySelect(), getBusinessDeptSelect()]).then( + ([platformRes, businessRes]) => { + this.platformCompanyOptions = this.normalizeDeptOptions(platformRes?.data?.data); + this.businessDeptOptions = this.normalizeDeptOptions(businessRes?.data?.data); + const platformColumn = this.findColumn(this.option.column, 'platformCompanyId'); + if (platformColumn) platformColumn.dicData = this.platformCompanyOptions; + const businessColumn = this.findColumn(this.option.column, 'businessDeptId'); + if (businessColumn) businessColumn.dicData = this.businessDeptOptions; + } + ); if (['edit', 'view'].includes(type) && this.form.id) { - getDetail(this.form.id).then(res => { - const detail = res.data?.data || {}; + Promise.all([loadOptions, getDetail(this.form.id)]).then(([, detailRes]) => { + const detail = detailRes.data?.data || {}; + const platformCompanyId = detail.platformCompanyId ? String(detail.platformCompanyId) : ''; + const platformMatched = this.platformCompanyOptions.find( + item => String(item.value) === platformCompanyId + ); this.form = { ...detail, - platformCompanyId: detail.platformCompanyId ? String(detail.platformCompanyId) : '', + platformCompanyId, businessDeptId: detail.businessDeptId ? String(detail.businessDeptId) : '', + // 展示以组织当前配置为准,避免配置表镜像滞后 + kingdeeCompanyCode: platformMatched?.kingdeeDeptCode || detail.kingdeeCompanyCode || '', + // 历史 -1 / 无效值在表单中显示为空,避免 Avue number 回显 -1 + initialSerial: this.normalizeInitialSerial(detail.initialSerial) ?? undefined, }; done(); }); return; } - if (type === 'add') { - this.form = { - platformCompanyId: '', - platformCompanyName: '', - businessDeptId: '', - businessDeptName: '', - contractCodePrefix: '', - initialSerial: undefined, - kingdeeCompanyCode: '', - kingdeeDeptCode: '', - remark: '', - }; - } - done(); + loadOptions.finally(() => { + if (type === 'add') { + this.form = { + platformCompanyId: '', + platformCompanyName: '', + businessDeptId: '', + businessDeptName: '', + contractCodePrefix: '', + initialSerial: undefined, + kingdeeCompanyCode: '', + kingdeeDeptCode: '', + remark: '', + }; + } + done(); + }); }, normalizeSubmitRow(row) { - const initialSerial = - row.initialSerial === '' || row.initialSerial === undefined || row.initialSerial === null - ? null - : Number(row.initialSerial); return { ...row, platformCompanyId: row.platformCompanyId || null, businessDeptId: row.businessDeptId || null, contractCodePrefix: String(row.contractCodePrefix || '').trim(), - kingdeeCompanyCode: String(row.kingdeeCompanyCode || '').trim(), + // 金蝶公司代码由后端按平台公司组织配置覆盖,前端仅展示 kingdeeDeptCode: String(row.kingdeeDeptCode || '').trim(), remark: String(row.remark || '').trim(), - initialSerial: Number.isFinite(initialSerial) ? initialSerial : null, + initialSerial: this.normalizeInitialSerial(row.initialSerial), }; }, rowSave(row, done, loading) { diff --git a/src/views/system/dept.vue b/src/views/system/dept.vue index b1e90c4..63e0b05 100644 --- a/src/views/system/dept.vue +++ b/src/views/system/dept.vue @@ -408,6 +408,7 @@ export default { searchOrder: 2, searchPlaceholder: '请选择是否平台公司', formatter: row => (Number(row.isPlatformCompany) === 1 ? '是' : '否'), + change: ({ value }) => this.handlePlatformCompanyChange(value), rules: [ { validator: (rule, value, callback) => { @@ -421,6 +422,37 @@ export default { }, ], }, + { + label: '金蝶公司代码', + prop: 'kingdeeDeptCode', + minWidth: 140, + order: 35, + maxlength: 64, + showWordLimit: true, + // 仅平台公司维护/展示金蝶公司代码;非平台公司列表留空 + addDisplay: false, + editDisplay: false, + viewDisplay: false, + formatter: row => + Number(row.isPlatformCompany) === 1 ? String(row.kingdeeDeptCode || '').trim() : '', + rules: [ + { + validator: (rule, value, callback) => { + const code = String(value || '').trim(); + if (!code) { + callback(); + return; + } + if (code.length > 64) { + callback(new Error('金蝶公司代码不能超过64个字符')); + return; + } + callback(); + }, + trigger: 'blur', + }, + ], + }, { label: '是否业务部门', prop: 'isBusinessDept', @@ -961,6 +993,21 @@ export default { normalizeYesNoFlag(value) { return Number(value) === 1 ? 1 : 0; }, + handlePlatformCompanyChange(value) { + const isPlatform = Number(value) === 1; + this.syncKingdeeCompanyCodeDisplay(isPlatform); + if (!isPlatform && this.form) { + this.form.kingdeeDeptCode = ''; + } + }, + syncKingdeeCompanyCodeDisplay(isPlatformCompany) { + const column = this.findColumn(this.option.column, 'kingdeeDeptCode'); + if (!column) return; + const visible = Number(isPlatformCompany) === 1; + column.addDisplay = visible; + column.editDisplay = visible; + column.viewDisplay = visible; + }, resolveDeptCategoryValue(value) { if (value === null || value === undefined || value === '') { this.$message.warning('请选择组织类型'); @@ -1261,6 +1308,9 @@ export default { rowSave(row, done, loading) { row.tenantId = row.tenantId || this.userInfo.tenantId || website.tenantId; row.deptCode = String(row.deptCode || '').trim(); + // 金蝶公司代码仅平台公司维护 + row.kingdeeDeptCode = + Number(row.isPlatformCompany) === 1 ? String(row.kingdeeDeptCode || '').trim() : ''; row.leaderId = func.join(row.leaderId); this.normalizeTopParent(row); if (!this.prepareDeptSubmitRow(row)) { @@ -1306,6 +1356,9 @@ export default { rowUpdate(row, index, done, loading) { row.tenantId = row.tenantId || this.userInfo.tenantId || website.tenantId; row.deptCode = String(row.deptCode || '').trim(); + // 金蝶公司代码仅平台公司维护 + row.kingdeeDeptCode = + Number(row.isPlatformCompany) === 1 ? String(row.kingdeeDeptCode || '').trim() : ''; row.leaderId = func.join(row.leaderId); this.normalizeTopParent(row); if (!this.prepareDeptSubmitRow(row)) { @@ -1414,6 +1467,7 @@ export default { this.form.tenantId = this.form.tenantId || this.userInfo.tenantId || website.tenantId; this.form.isPlatformCompany = this.normalizeYesNoFlag(this.form.isPlatformCompany); this.form.isBusinessDept = this.normalizeYesNoFlag(this.form.isBusinessDept); + this.syncKingdeeCompanyCodeDisplay(this.form.isPlatformCompany); this.initData(this.form.tenantId); this.loadParentDeptCode(this.form.parentId); done(); @@ -1435,6 +1489,7 @@ export default { if (this.form.parentId === '0') { this.form.parentId = ''; } + this.syncKingdeeCompanyCodeDisplay(this.form.isPlatformCompany); this.loadParentDeptCode(this.form.parentId); if (Object.prototype.hasOwnProperty.call(this.form, 'leaderId')) { const splitResult = func.split(this.form.leaderId); diff --git a/src/views/vehicle/credit-score-quantification.vue b/src/views/vehicle/credit-score-quantification.vue index 5508615..78de802 100644 --- a/src/views/vehicle/credit-score-quantification.vue +++ b/src/views/vehicle/credit-score-quantification.vue @@ -1012,7 +1012,9 @@ export default { this.$message.warning('得分率下限必须小于上限'); return; } + // F 级无授信允许额度上下限均为 0;其余等级仍要求下限严格小于上限 if ( + !this.isZeroCreditLimitRange(this.standardForm) && Number(this.standardForm.creditLimitLower) >= Number(this.standardForm.creditLimitUpper) ) { this.$message.warning('授信额度下限必须小于上限'); @@ -1033,7 +1035,7 @@ export default { this.$message.warning('评估标准中的得分率区间不能重叠'); return; } - if (this.hasRangeOverlap(nextStandards, 'creditLimitLower', 'creditLimitUpper')) { + if (this.hasCreditLimitOverlap(nextStandards)) { this.$message.warning('评估标准中的授信额度区间不能重叠'); return; } @@ -1083,6 +1085,17 @@ export default { return next && range.upper > next.lower; }); }, + /** F 级无授信:额度上下限均为 0 */ + isZeroCreditLimitRange(standard = {}) { + return ( + Number(standard.creditLimitLower) === 0 && Number(standard.creditLimitUpper) === 0 + ); + }, + /** 授信额度区间重叠校验:跳过 0~0 无授信区间,避免与 E 级 0~500 冲突 */ + hasCreditLimitOverlap(list = []) { + const ranges = list.filter(item => !this.isZeroCreditLimitRange(item)); + return this.hasRangeOverlap(ranges, 'creditLimitLower', 'creditLimitUpper'); + }, validateConfig() { if (!this.configForm.name || !this.configForm.name.trim()) { this.$message.warning('请输入评定表名称'); @@ -1219,7 +1232,10 @@ export default { this.$message.warning(`${standard.creditLevel}信用等级得分率下限必须小于上限`); return false; } - if (Number(standard.creditLimitLower) >= Number(standard.creditLimitUpper)) { + if ( + !this.isZeroCreditLimitRange(standard) && + Number(standard.creditLimitLower) >= Number(standard.creditLimitUpper) + ) { this.$message.warning(`${standard.creditLevel}信用等级授信额度下限必须小于上限`); return false; } @@ -1234,7 +1250,7 @@ export default { this.$message.warning('评估标准中的得分率区间不能重叠'); return false; } - if (this.hasRangeOverlap(standards, 'creditLimitLower', 'creditLimitUpper')) { + if (this.hasCreditLimitOverlap(standards)) { this.$message.warning('评估标准中的授信额度区间不能重叠'); return false; } diff --git a/src/views/vehicle/customer-archive.vue b/src/views/vehicle/customer-archive.vue index ba2af27..7a1154b 100644 --- a/src/views/vehicle/customer-archive.vue +++ b/src/views/vehicle/customer-archive.vue @@ -42,6 +42,21 @@ >批量删除 + + @@ -1301,6 +1321,19 @@ class="score-detail-form" > + + + + 线上评估 + 线下评估 + + 线下请上传评分表 Excel + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - @@ -1386,6 +1372,108 @@ + + +
+ + 下载模板 + + + + 上传 Excel + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1395,7 +1483,7 @@ sum + Number(item.selfScore || 0), 0); let reviewScore = details.reduce((sum, item) => sum + Number(item.reviewScore || 0), 0); - // 暂定:加分/减分合计最高按 15 分计 + // 暂定:加分/减分合计最高按 15 分计(分类汇总展示封顶后值) if (category.categoryCode === 'plus') { selfScore = Math.min(selfScore, HARDCODED_MAX_PLUS_SCORE); reviewScore = Math.min(reviewScore, HARDCODED_MAX_PLUS_SCORE); @@ -2406,6 +2509,16 @@ export default { }); return rows; }, + isOfflineScore() { + return String(this.currentScore?.evaluateMode || 'online') === 'offline'; + }, + expiryTagOptions() { + return [ + { label: '全部', value: 'all' }, + { label: '30天内到期', value: 'soon' }, + { label: '已过期', value: 'expired' }, + ]; + }, }, watch: { $route(to, from) { @@ -3518,13 +3631,22 @@ export default { return Number.isFinite(rate) ? `${rate.toFixed(2)}%` : '-'; }, isBusinessLicenseExpiringSoon(row = {}) { - if (row.businessTermType === '长期' || !row.businessEndDate) return false; - const endDate = this.$dayjs(row.businessEndDate); + return this.isDateExpiringSoon(row.businessEndDate, row.businessTermType === '长期'); + }, + /** 日期是否在 30 天内到期(含当天);skip=true 时不提示(如营业执照长期) */ + isDateExpiringSoon(dateValue, skip = false) { + if (skip || !dateValue) return false; + const endDate = this.$dayjs(dateValue); if (!endDate.isValid()) return false; const today = this.$dayjs().startOf('day'); const expireSoonDate = today.add(30, 'day'); return !endDate.isAfter(expireSoonDate, 'day'); }, + handleExpireChange(expireStatus) { + this.query.expireStatus = expireStatus || 'all'; + this.page.currentPage = 1; + this.onLoad(this.page, this.query); + }, handleBusinessEndDateChange(value) { if (value) { this.archiveForm.businessTermType = '固定期限'; @@ -4408,7 +4530,9 @@ export default { normalizeScore(score) { return { quantificationId: '', + evaluateMode: 'online', scoreDate: this.$dayjs().format('YYYY-MM-DD'), + nextEvaluateDate: '', applyCreditLimit: this.archiveForm.applyCreditLimit || '', maxCreditLimit: '', maxMinusScore: HARDCODED_MAX_MINUS_SCORE, @@ -4426,6 +4550,8 @@ export default { details: [], ...score, quantificationId: score.quantificationId ? String(score.quantificationId) : '', + evaluateMode: score.evaluateMode === 'offline' ? 'offline' : 'online', + nextEvaluateDate: score.nextEvaluateDate || '', applyCreditLimit: this.normalizeOptionalAmount( score.applyCreditLimit === undefined ? this.archiveForm.applyCreditLimit @@ -4588,19 +4714,38 @@ export default { this.invoiceForm = this.emptyInvoice(); this.$refs.archiveForm?.clearValidate(); }, - // 客商材料不做提交校验,仅在页面上提示未上传项。提交审批前必须完成自评,且评分记录只能有一条。 + // 仅客商分类含「客户」时强制评分;纯承运商不强制。评分记录至多一条。 validateScoreForApproval(archive) { const scores = archive.scores || []; if (scores.length > 1) { this.$message.warning('评分记录只能有一条'); return false; } + if (!this.requiresCustomerScore(archive.customerType)) { + return true; + } if (scores.length !== 1 || scores[0].selfStatus !== '已完成') { - this.$message.warning('提交审批前必须完成自评'); + this.$message.warning('客户提交审批前必须完成评分自评'); + return false; + } + if (!scores[0].nextEvaluateDate) { + this.$message.warning('请填写下次评定日期'); return false; } return true; }, + /** 客商分类是否包含客户(字典值「客户」或历史值 customer) */ + requiresCustomerScore(customerType) { + const values = Array.isArray(customerType) + ? customerType + : String(customerType || '') + .replace(/,/g, ',') + .split(','); + return values + .map(item => String(item || '').trim()) + .filter(Boolean) + .some(item => item === '客户' || item.toLowerCase() === 'customer'); + }, normalizeArchive() { // 提交前再次固定所属组织,防止被绕过 this.applyFixedOwnerDept(); @@ -4891,7 +5036,9 @@ export default { const template = this.normalizeScore({ ...scoreTemplate, quantificationId, + evaluateMode: this.currentScore.evaluateMode || 'online', scoreDate: this.currentScore.scoreDate, + nextEvaluateDate: this.currentScore.nextEvaluateDate, applyCreditLimit: this.currentScore.applyCreditLimit, maxCreditLimit: '', creditLevel: '', @@ -5145,7 +5292,8 @@ export default { .replace(/^\./, '') .replace(/(\..*)\./g, '$1') .replace(/^(\d+)(\.\d{0,2})?.*$/, '$1$2'); - if (prop === 'reviewScore' && normalized !== '') { + // 减分项复评得分存正数扣分,允许填 0(不扣分);不做项目满分截断 + if (prop === 'reviewScore' && normalized !== '' && !this.isMinusScoreDetail(detail)) { const maxScore = this.getScoreItemFullScore(detail); if (Number(normalized) > maxScore) { detail[prop] = String(maxScore); @@ -5206,7 +5354,12 @@ export default { const optionScores = this.parseScoreOptions(detail.optionsJson) .map(item => Number(item.score)) .filter(score => Number.isFinite(score)); - return optionScores.length ? Math.max(...optionScores) : 0; + if (!optionScores.length) return 0; + // 减分项选项分常为负(如 -5),项目满分按绝对值取最大可扣分;避免 0 分被误判为高于“最高得分-5” + if (this.isMinusScoreDetail(detail)) { + return Math.max(...optionScores.map(score => Math.abs(score))); + } + return Math.max(...optionScores); }, getScoreFullMark(details = []) { return details.reduce((sum, item) => { @@ -5224,6 +5377,7 @@ export default { }); }, matchCreditStandard(score, scoreRate) { + // 闭开区间:≥lower 且 item && item.creditLevel) .sort((a, b) => Number(b.scoreRateLower || 0) - Number(a.scoreRateLower || 0)); @@ -5231,7 +5385,10 @@ export default { const lower = Number(standard.scoreRateLower); const upper = Number(standard.scoreRateUpper); if (!Number.isFinite(lower) || !Number.isFinite(upper)) return false; - return scoreRate >= lower && scoreRate <= upper; + if (upper >= 100) { + return scoreRate >= lower && scoreRate <= upper; + } + return scoreRate >= lower && scoreRate < upper; }); }, calculateCreditLimitByStandard(standard, scoreRate) { @@ -5267,25 +5424,32 @@ export default { const rawPlusReview = sumScore(plusDetails, 'reviewScore'); const rawMinusSelf = sumScore(minusDetails, 'selfScore'); const rawMinusReview = sumScore(minusDetails, 'reviewScore'); - // 暂定:加分/减分合计最高按 15 分计 + // 暂定:加分/减分合计最高按 15 分计(未超按实际,超 15 按 15/-15) const plusSelf = Math.min(rawPlusSelf, HARDCODED_MAX_PLUS_SCORE); const plusReview = Math.min(rawPlusReview, HARDCODED_MAX_PLUS_SCORE); const minusSelf = Math.min(rawMinusSelf, HARDCODED_MAX_MINUS_SCORE); const minusReview = Math.min(rawMinusReview, HARDCODED_MAX_MINUS_SCORE); - const selfScore = sumScore(basicDetails, 'selfScore') + plusSelf - minusSelf; + const computedSelfScore = sumScore(basicDetails, 'selfScore') + plusSelf - minusSelf; const reviewScore = sumScore(basicDetails, 'reviewScore') + plusReview - minusReview; const hasReviewScore = details.some( item => !this.isBlankScore(item.reviewScore) ); + // 线下模式允许手工改自评得分:有手工值时优先采用,否则回退明细合计 + const isOffline = String(score.evaluateMode || '') === 'offline'; + const manualSelfScore = Number(score.selfScore); + const selfScore = + isOffline && Number.isFinite(manualSelfScore) && String(score.selfScore ?? '').trim() !== '' + ? manualSelfScore + : computedSelfScore; const finalScore = hasReviewScore ? reviewScore : selfScore; const fullMark = this.getScoreFullMark(details); const scoreRate = fullMark > 0 ? Math.max(0, Math.min(100, (finalScore / fullMark) * 100)) : 0; const standard = this.matchCreditStandard(score, scoreRate); const hasStandards = (score.standards || []).some(item => item && item.creditLevel); - score.selfScore = Number(selfScore.toFixed(2)); + score.selfScore = Number(Number(selfScore).toFixed(2)); score.reviewScore = Number(reviewScore.toFixed(2)); - score.finalScore = Number(finalScore.toFixed(2)); + score.finalScore = Number(Number(finalScore).toFixed(2)); score.scoreRate = Number(scoreRate.toFixed(2)); if (hasStandards) { score.creditLevel = standard?.creditLevel || ''; @@ -5413,6 +5577,10 @@ export default { this.$message.warning('请选择评定日期'); return false; } + if (!this.currentScore.nextEvaluateDate) { + this.$message.warning('请选择下次评定日期'); + return false; + } if (!this.currentScore.applyCreditLimit) { this.$message.warning('请输入拟申请总资金使用额度'); return false; @@ -5426,7 +5594,11 @@ export default { return false; } if (!this.currentScore.details || !this.currentScore.details.length) { - this.$message.warning('当前评分量化表未配置评分项目'); + this.$message.warning( + this.isOfflineScore + ? '请先上传评分表 Excel 解析回填明细' + : '当前评分量化表未配置评分项目' + ); return false; } const invalidScoreInput = this.currentScore.details.find(item => { @@ -5440,7 +5612,9 @@ export default { ); return false; } + // 减分项允许填 0(不扣分),且按阶梯累计扣分不做项目满分截断 const overMaxReviewScore = this.currentScore.details.find(item => { + if (this.isMinusScoreDetail(item)) return false; if ( item.reviewScore === undefined || item.reviewScore === null || @@ -5635,13 +5809,15 @@ export default { }); }, searchReset() { - this.query = {}; - this.onLoad(this.page); + this.query = { expireStatus: this.query.expireStatus || 'all' }; + this.onLoad(this.page, this.query); }, searchChange(params, done) { // Avue 只回传非空筛选项,被清空的字段不会出现在 params 里, // 因此查询条件必须整体重建,不能与上一次的 this.query 合并,否则清空后仍会带上次的值 + const expireStatus = this.query.expireStatus || 'all'; this.query = this.buildQuery(params, true); + this.query.expireStatus = expireStatus; this.page.currentPage = 1; this.onLoad(this.page, this.query); done(); @@ -5780,8 +5956,145 @@ export default { if (status === 0 || status === '0' || status === -1 || status === '-1') { delete values.status; } + if (!values.expireStatus) { + values.expireStatus = this.query.expireStatus || 'all'; + } + if (values.expireStatus === 'all') { + delete values.expireStatus; + } return values; }, + handleEvaluateModeChange(mode) { + this.currentScore.evaluateMode = mode === 'offline' ? 'offline' : 'online'; + if (this.isOfflineScore) { + this.expandedScoreCategoryKeys = []; + } + }, + handleOfflineSelfScoreInput(value) { + const normalized = String(value || '') + .replace(/[^\d.-]/g, '') + .replace(/(?!^)-/g, '') + .replace(/^\./, '') + .replace(/(\..*)\./g, '$1') + .replace(/^(-?\d+)(\.\d{0,2})?.*$/, '$1$2'); + this.currentScore.selfScore = normalized; + }, + recalculateOfflineScore() { + if (!this.isOfflineScore) return; + const selfScore = Number(this.currentScore.selfScore); + if (!Number.isFinite(selfScore)) return; + this.currentScore.selfScore = Number(selfScore.toFixed(2)); + const hasReviewScore = (this.currentScore.details || []).some( + item => !this.isBlankScore(item.reviewScore) + ); + if (!hasReviewScore) { + this.currentScore.finalScore = this.currentScore.selfScore; + } + const fullMark = this.getScoreFullMark(this.currentScore.details || []); + const scoreRate = + fullMark > 0 + ? Math.max(0, Math.min(100, (Number(this.currentScore.finalScore) / fullMark) * 100)) + : 0; + this.currentScore.scoreRate = Number(scoreRate.toFixed(2)); + const standard = this.matchCreditStandard(this.currentScore, scoreRate); + const hasStandards = (this.currentScore.standards || []).some(item => item && item.creditLevel); + if (hasStandards) { + this.currentScore.creditLevel = standard?.creditLevel || ''; + this.currentScore.maxCreditLimit = standard + ? this.calculateCreditLimitByStandard(standard, scoreRate) + : ''; + } + }, + downloadScoreTemplate() { + if (!this.currentScore.quantificationId) { + this.$message.warning('请先选择评分量化表'); + return; + } + NProgress.start(); + const loading = ElLoading.service({ + lock: true, + text: '模板下载中', + background: 'rgba(255, 255, 255, 0.7)', + }); + downloadScoreExcelTemplate(this.currentScore.quantificationId) + .then(res => { + downloadXls( + res.data, + `客商评分明细模板${this.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx` + ); + }) + .finally(() => { + loading.close(); + NProgress.done(); + }); + }, + async uploadScoreExcel(option) { + const file = option?.file; + if (!file) return; + if (!this.currentScore.quantificationId) { + this.$message.warning('请先选择评分量化表'); + return; + } + const fileName = String(file.name || '').toLowerCase(); + if (!/\.(xls|xlsx)$/.test(fileName)) { + this.$message.error('请上传 .xls,.xlsx 标准格式文件'); + return; + } + this.scoreImporting = true; + NProgress.start(); + const loading = ElLoading.service({ + lock: true, + text: '导入中', + background: 'rgba(255, 255, 255, 0.7)', + }); + try { + const res = await importScoreExcel(this.currentScore.quantificationId, file); + if (isExcelResponse(res)) { + downloadXls( + res.data, + `客商评分导入失败明细${this.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx` + ); + this.$message.warning('部分数据导入失败,已下载失败明细'); + return; + } + const result = isBusinessJson(res?.data) ? res.data : await parseBlobJson(res?.data); + if (Number(result?.code) !== 200) { + this.$message.error(result?.msg || '导入失败'); + return; + } + const imported = result?.data || {}; + const keepFields = { + evaluateMode: 'offline', + scoreDate: this.currentScore.scoreDate, + nextEvaluateDate: this.currentScore.nextEvaluateDate, + applyCreditLimit: this.currentScore.applyCreditLimit, + tempApplyCreditLimit: this.currentScore.tempApplyCreditLimit, + tempCreditStartDate: this.currentScore.tempCreditStartDate, + tempCreditEndDate: this.currentScore.tempCreditEndDate, + remark: this.currentScore.remark, + attachments: this.scoreAttachmentFiles, + quantificationId: this.currentScore.quantificationId, + }; + this.currentScore = this.normalizeScore({ + ...imported, + ...keepFields, + standards: imported.standards || this.currentScore.standards || [], + details: (imported.details || []).map(detail => ({ + ...detail, + scoreInput: this.normalizeScoreInputValue(detail.scoreInput), + })), + }); + this.calculateScore(this.currentScore); + this.$message.success('评分表解析成功,请在证明材料中上传评分表原件'); + } catch (error) { + const message = await resolveImportErrorMessage(error); + this.$message.error(message); + } finally { + this.scoreImporting = false; + loading.close(); + NProgress.done(); + } + }, }, }; @@ -5816,6 +6129,23 @@ export default { color: var(--el-color-danger); } +.customer-archive-page__expiry-tabs { + margin-bottom: 8px; +} + +.score-offline-tip { + margin-left: 12px; + color: #909399; + font-size: 12px; +} + +.score-offline-actions { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + // 风险客商悬浮提示:popper 挂在 body 下,需用 :global 约束宽度 :global(.customer-risk-popper) { max-width: 360px;