From 4d5b99da823f87c8627e16248e4d774e99bb2095 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Wed, 9 Sep 2026 11:57:52 +0800 Subject: [PATCH] =?UTF-8?q?feat(contract):=20=E4=BC=98=E5=8C=96=E5=90=88?= =?UTF-8?q?=E5=90=8C=E5=8F=98=E6=9B=B4=E5=8F=8A=E8=BD=A6=E8=BE=86=E8=AE=A4?= =?UTF-8?q?=E8=AF=81=E5=AE=A1=E6=A0=B8=E5=BC=B9=E7=AA=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 合同变更页「其它附件」上传按钮样式调整,与变更材料区按钮统一 - 新增分区标题必填星号样式,改用 ::after 避免冲突 - 变更页 load() 强制清空表单关键字段,避免带入旧数据 - 实现 mergeChangeAttachments() 合并上次变更附件,去重并格式化大小 - 合同变更记录表格新增「变更内容」列,支持单行摘要显示与悬浮全量提示 - 操作列由单「流程」改为「详情 + 流程」按钮,详情文字统一 - 变更记录值格式化增强,支持方案名、支付比例、结算规则等摘要展示 - buildDetailChangeRecordRows 过滤条件更新,前后均为空时跳过展示 - 车辆认证审核弹窗样式及结构重构,使用 section-card 白卡分组,内容补全证件图片区 - 证件图片实现只读大图展示,复用全局样式,无需新增规则 - label 单位统一为中文全角括号,提升界面一致性 - 新增长期有效显示逻辑,避免日期未填「-」与长期有效混淆 - 删除无用计算属性 auditDetailItems,改用简洁 auditValue 方法返回展示值 - 全站弹窗标题统一增加 4px 主色竖条,符合设计规范,且对无标题弹窗进行排除处理 - 客户端临时额度申请期限字段拆分为两个独立字段,清晰明了 - 统一合同变更页变更原因必填标识及上传按钮样式 - 修正 EP 表单项标签高度和对齐,完善星号方案兼容性设计 --- .workbuddy/memory/2026-09-09.md | 25 ++ src/styles/element-ui.scss | 11 + src/views/business/contract-manage-change.vue | 16 +- src/views/business/contract-manage.vue | 134 +++++++- src/views/transportCapacity/vehicle.vue | 293 ++++++++++++++++-- src/views/vehicle/customer-archive.vue | 48 ++- 6 files changed, 468 insertions(+), 59 deletions(-) diff --git a/.workbuddy/memory/2026-09-09.md b/.workbuddy/memory/2026-09-09.md index 248b9fe..419ea76 100644 --- a/.workbuddy/memory/2026-09-09.md +++ b/.workbuddy/memory/2026-09-09.md @@ -9,6 +9,31 @@ - 原理:stretch 让星号盒子在「换行时」被拉伸到 label 全高、内容从顶部排布 → 贴首行;单行时盒子=一行高,视觉不变(比 flex-start 更贴合「只换行才生效」)。 - 前提:::before 高度必须 auto 且继承行高;label 不能是 固定 height + 等高 line-height,否则退化成居中,需 JS 判行数加 class。 +## 合同变更页(contract-manage-change.vue) +- 「其它附件」上传按钮改为 `type="primary" plain icon="el-icon-upload"`,与「变更材料」区 vehicle-attachment-upload 内置按钮一致。 +- 分区标题必填星号:`.dialog-section-title.is-required::after { content:'*'; color:#f56c6c }`(`::before` 已被蓝色竖条占用,星号走 `::after`)。 +- 再次变更逻辑:`load()` 里 form 强制重置 `changeContent/changeReason/changeAttachmentsJson = ''`、`changeMaterials = []`(不带上一次内容);新增 `mergeChangeAttachments()` 把上一次的 `changeAttachmentsJson` 归集进 `attachments`(其它附件),按 url/名去重,数字 size 转格式化文本,提交时随 attachmentsJson 持久化。 + +## 合同变更记录对齐客商变更记录(contract-manage.vue) +- 编辑页与详情弹窗的两处「变更记录」表格新增「变更内容」列:`formatContractChangeContent(row)` 复用 `buildDetailChangeRecordRows` 生成「字段:变更前:x → 变更后:y」摘要,单行省略 + `el-tooltip` 全量悬浮(样式 `.contract-change-record-content-cell/-tooltip` 照搬客商 `change-record-content-*`)。 +- 编辑页操作列由单个「流程」改为「详情 + 流程」(`openDetailChangeRecord` + `openFlow`,宽度 120→150);详情弹窗表格「查看详情」改「详情」与客商一致。 +- 值格式化增强:`billingPlanJson`→方案名(默认)列表、`paymentRatioJson`→「第N笔 xx%」、`settlementRuleJson/preSettlementConfigJson/formalSettlementConfigJson`→可读摘要(自动生成开关/结算类型/周期类型/截单日/周期天数,含预结算+正式结算双段)。 +- `buildDetailChangeRecordRows` 过滤条件补「前后都为空则跳过」(原来只比 JSON 字符串,'' 与 null 会漏)。 + ## 星号方案补丁(关键) - EP 的 `.el-form-item__label` 写死 `height:32px; line-height:32px`(项目只覆盖了 line-height:14px),导致 stretch 方案文字顶到 32px 盒子顶部、单行不居中。 - 修复:`.is-required .el-form-item__label { height:auto; align-self:center; align-items:normal }`——height:auto 收缩到内容高 + align-self:center 行内居中 + normal(stretch) 仅换行时星号贴首行。 + +## 车辆认证审核弹窗重构(transportCapacity/vehicle.vue) +- 背景:用户反馈「样式不统一、内容不全」。根因是审核弹窗用 ``(大白表 + 居中 label),与同页编辑弹窗的 `` 白卡分组风格不一致。 +- 改造:弹窗主体改为 4 个 `section-card` 分组 —— 基础信息 / 尺寸重量 / 证件信息 / 证件图片;内部 `el-form label-position="right" label-width="auto"` + `el-row/el-col`(3 列 `:span="8"`,所属组织与备注占 `:span="24"`)。 +- 内容补全:新增「证件图片」分组,6 张证件照用 `` 只读展示(可点击放大);证件图片尺寸 240×151 复用已有全局 `:deep(.vehicle-uploader--large)` 规则,无需新写。 +- label 单位统一为中文全角括号:`外廓长度(毫米)` / `核定载质量(KG)`(原为半角 `外廓长度(mm)`)。 +- 方法重构:`formatAuditValue(item)` → `auditValue(prop)`,签名更简洁;新增 longTermMap,对 drivingLicenseEndDate / roadTransportCertEndDate / annualReviewEndDate / compulsoryScrapDate 在勾选长期有效时返回「长期有效」,避免与未填写的「-」混淆(与列表页 formatEndDate 行为一致)。 +- 删除已无引用的 `auditDetailItems` computed。 + +## 全站弹窗标题补 4px 主色竖条(全局 element-ui.scss) +- 发现:`.el-dialog__title` 全局只配了 `inline-flex + gap:8px`,**没有竖条**(竖条此前只在 `.dialog-section-title::before`),导致全站 el-dialog 标题都不带竖条,不符合 AGENTS.md 4.8。 +- 修复:新增 `.el-dialog__title:not(:empty)::before { width:4px; height:16px; border-radius:2px; background:var(--el-color-primary) }`。 +- 注意:必须加 `:not(:empty)`,否则无标题弹窗会出现孤立蓝条。 +- customer-archive.vue 评分详情弹窗:将「临时额度申请使用期限」区间行(span=12 + 至 连接)拆分为两个 span=6 独立字段「临时申请额度有效期起 / 止」,字段名 tempCreditStartDate/End 不变,删除废弃样式 .score-detail-form__range。 diff --git a/src/styles/element-ui.scss b/src/styles/element-ui.scss index 7eb6e4a..b40123f 100644 --- a/src/styles/element-ui.scss +++ b/src/styles/element-ui.scss @@ -213,6 +213,17 @@ gap: 8px; } +// 全站弹窗标题统一带 4px 主色竖条(与 .dialog-section-title 保持一致) +// :not(:empty) 避免无标题弹窗出现孤立竖条 +.el-dialog__title:not(:empty)::before { + content: ''; + flex: none; + width: 4px; + height: 16px; + border-radius: 2px; + background-color: var(--el-color-primary); +} + .dialog-section-title::before { content: ''; flex: none; diff --git a/src/views/business/contract-manage-change.vue b/src/views/business/contract-manage-change.vue index 787be4e..ca7315b 100644 --- a/src/views/business/contract-manage-change.vue +++ b/src/views/business/contract-manage-change.vue @@ -79,13 +79,13 @@
其它附件
批量下载
- 上传附件 + 上传附件
变更内容
-
变更原因
+
变更原因
变更材料
@@ -138,8 +138,17 @@ export default { mounted() { this.load(); }, watch: { settlementConfigTab(tab, oldTab) { if (tab === oldTab) return; if (oldTab === 'pre') this.preSettlementConfig = { ...this.settlementRule }; else this.formalSettlementConfig = { ...this.settlementRule }; this.settlementRule = { ...(tab === 'pre' ? this.preSettlementConfig : this.formalSettlementConfig) }; } }, methods: { - async load() { const id = this.$route.query.id; if (!id) return; const res = await api.getDetail(id); const data = res.data?.data || res.data || {}; this.form = { ...data, changeContent: '', changeReason: '', changeAttachmentsJson: '', copyCount: normalizeOptionalPositiveInteger(data.copyCount), paymentDays: normalizeOptionalPositiveInteger(data.paymentDays), changeType: '合同信息变更' }; this.changeMaterials = []; this.period = data.startDate && data.endDate ? [data.startDate, data.endDate] : []; this.plans = this.parse(data.billingPlanJson); this.attachments = this.parse(data.attachmentsJson); this.contractFileRows = this.parse(data.contractFileJson); const rules = this.parseObject(data.settlementRuleJson); const pre = this.parseObject(data.preSettlementConfigJson); const formal = this.parseObject(data.formalSettlementConfigJson); const legacy = Object.keys(rules).some(key => !['preSettlementConfig', 'formalSettlementConfig'].includes(key)) ? rules : {}; this.preSettlementConfig = rules.preSettlementConfig || (Object.keys(pre).length ? pre : legacy); this.formalSettlementConfig = rules.formalSettlementConfig || (Object.keys(formal).length ? formal : legacy); this.settlementRule = { ...this.preSettlementConfig }; this.feeGenerationMode = data.feeGenerationMode || (Number(data.billingEnabled) === 0 ? 'manual' : 'system'); this.paymentRatioRows = this.parse(data.paymentRatioJson); }, + async load() { const id = this.$route.query.id; if (!id) return; const res = await api.getDetail(id); const data = res.data?.data || res.data || {}; this.form = { ...data, changeContent: '', changeReason: '', changeAttachmentsJson: '', copyCount: normalizeOptionalPositiveInteger(data.copyCount), paymentDays: normalizeOptionalPositiveInteger(data.paymentDays), changeType: '合同信息变更' }; this.changeMaterials = []; this.period = data.startDate && data.endDate ? [data.startDate, data.endDate] : []; this.plans = this.parse(data.billingPlanJson); this.attachments = this.mergeChangeAttachments(this.parse(data.attachmentsJson), this.parse(data.changeAttachmentsJson)); this.contractFileRows = this.parse(data.contractFileJson); const rules = this.parseObject(data.settlementRuleJson); const pre = this.parseObject(data.preSettlementConfigJson); const formal = this.parseObject(data.formalSettlementConfigJson); const legacy = Object.keys(rules).some(key => !['preSettlementConfig', 'formalSettlementConfig'].includes(key)) ? rules : {}; this.preSettlementConfig = rules.preSettlementConfig || (Object.keys(pre).length ? pre : legacy); this.formalSettlementConfig = rules.formalSettlementConfig || (Object.keys(formal).length ? formal : legacy); this.settlementRule = { ...this.preSettlementConfig }; this.feeGenerationMode = data.feeGenerationMode || (Number(data.billingEnabled) === 0 ? 'manual' : 'system'); this.paymentRatioRows = this.parse(data.paymentRatioJson); }, parse(value) { try { const result = JSON.parse(value || '[]'); return Array.isArray(result) ? result : []; } catch { return []; } }, + // 审核通过后上一次的变更材料归集到「其它附件」,按文件地址/文件名去重,避免重复展示 + mergeChangeAttachments(attachments = [], changeAttachments = []) { + const key = item => this.attachmentUrl(item) || this.attachmentName(item); + const exists = new Set(attachments.map(key)); + const merged = changeAttachments + .filter(item => !exists.has(key(item))) + .map(item => ({ ...item, size: /^\d+$/.test(String(item.size ?? '')) ? this.formatFileSize(item.size) : item.size })); + return [...attachments, ...merged]; + }, parseObject(value) { try { return { autoGenerate: 1, settlementType: '月结', billCycleType: '固定截单日', billCutoffDay: 25, cycleDays: 15, ...(JSON.parse(value || '{}') || {}) }; } catch { return { autoGenerate: 1, settlementType: '月结', billCycleType: '固定截单日', billCutoffDay: 25, cycleDays: 15 }; } }, positiveIntegerInput(prop, value) { this.form[prop] = String(value ?? '').replace(/\D/g, '').replace(/^0+/, ''); }, addPlan() { this.planEditorIndex = -1; this.planEditor = { planName: `计费方案${this.plans.length + 1}`, defaultPlan: !this.plans.length, remark: '', rules: [{}] }; this.planDialogVisible = true; }, @@ -187,6 +196,7 @@ export default { .contract-basic-section :deep(.el-date-editor) { width: 360px; max-width: 100%; } .dialog-section-title { margin-bottom: 18px; font-size: 16px; font-weight: 600; } .dialog-section-title::before { display: inline-block; width: 4px; height: 16px; margin-right: 8px; vertical-align: -2px; background: #409eff; content: ''; } +.dialog-section-title.is-required::after { margin-left: 4px; color: #f56c6c; content: '*'; } .section-head { display: flex; align-items: center; justify-content: space-between; } .change-table { margin: 16px 0; } .ratio-tip { margin-bottom: 8px; color: #f56c6c; } diff --git a/src/views/business/contract-manage.vue b/src/views/business/contract-manage.vue index 26052f0..c422580 100644 --- a/src/views/business/contract-manage.vue +++ b/src/views/business/contract-manage.vue @@ -480,6 +480,20 @@ /> + + + - + @@ -759,6 +774,20 @@ /> + + + 详情 @@ -2250,12 +2279,73 @@ export default { .filter(Boolean); return names.length ? names.join('、') : '空'; }, + formatChangeRecordBillingPlans(value) { + const list = Array.isArray(value) ? value : []; + if (!list.length) return '空'; + return list + .map(item => { + if (!item || typeof item !== 'object') return String(item ?? ''); + const name = item.planName || item.name || '未命名方案'; + return item.defaultPlan ? `${name}(默认)` : name; + }) + .filter(Boolean) + .join('、'); + }, + formatChangeRecordPaymentRatios(value) { + const list = Array.isArray(value) ? value : []; + if (!list.length) return '空'; + return list + .map(item => { + if (!item || typeof item !== 'object') return String(item ?? ''); + const term = item.paymentTerm || ''; + const ratio = item.ratioLimit === undefined || item.ratioLimit === null || item.ratioLimit === '' ? '' : `${item.ratioLimit}%`; + return [term, ratio].filter(Boolean).join(' '); + }) + .filter(Boolean) + .join(';'); + }, + formatChangeRecordSettlementRule(value) { + const normalized = this.normalizeChangeRecordValue(value); + if (!normalized || typeof normalized !== 'object' || Array.isArray(normalized)) { + return this.isEmptyChangeRecordValue(normalized) ? '空' : String(normalized); + } + const summary = config => { + if (!config || typeof config !== 'object') return ''; + const parts = []; + const autoGenerate = config.autoGenerate; + if (autoGenerate !== undefined && autoGenerate !== null && autoGenerate !== '') { + parts.push(`自动生成结算单:${Number(autoGenerate) === 1 ? '开启' : '关闭'}`); + } + if (config.settlementType) parts.push(`结算类型:${config.settlementType}`); + if (config.billCycleType) parts.push(`账单周期类型:${config.billCycleType}`); + if (config.billCutoffDay) parts.push(`账单截单日:${config.billCutoffDay}日`); + if (config.cycleDays) parts.push(`周期天数:${config.cycleDays}天`); + if (config.billStartDate) parts.push(`账单起始日期:${config.billStartDate}`); + return parts.join(','); + }; + if (normalized.preSettlementConfig || normalized.formalSettlementConfig) { + return [ + `预结算:${summary(normalized.preSettlementConfig) || '空'}`, + `正式结算:${summary(normalized.formalSettlementConfig) || '空'}`, + ].join(';'); + } + return summary(normalized) || '空'; + }, formatContractChangeValue(field, value) { const normalized = this.normalizeChangeRecordValue(value); if (this.isEmptyChangeRecordValue(normalized)) return '空'; if (['contractFileJson', 'attachmentsJson', 'changeAttachmentsJson'].includes(field)) { return this.formatChangeRecordAttachments(normalized); } + if (field === 'billingPlanJson') return this.formatChangeRecordBillingPlans(normalized); + if (field === 'paymentRatioJson') return this.formatChangeRecordPaymentRatios(normalized); + if ( + ['settlementRuleJson', 'preSettlementConfigJson', 'formalSettlementConfigJson'].includes( + field + ) + ) { + return this.formatChangeRecordSettlementRule(normalized); + } if (field === 'legalSealFlag') return Number(normalized) === 1 ? '是' : '否'; if (field === 'feeGenerationMode') return normalized === 'manual' ? '手动生成' : '系统生成'; if (field === 'invoiceCycle' || field === 'paymentDays') return `${normalized}天`; @@ -2264,16 +2354,33 @@ export default { } return String(normalized); }, + // 列表「变更内容」列:把变更前后内容拼成一段摘要(参照客商变更记录) + formatContractChangeContent(row = {}) { + const rows = this.buildDetailChangeRecordRows(row).filter( + item => item.field !== '变更原因' + ); + return rows + .map(item => `${item.field}:变更前:${item.before} → 变更后:${item.after}`) + .join(';'); + }, buildDetailChangeRecordRows(row = {}) { const beforeData = this.parseChangeRecordData(row.beforeData); const afterData = this.parseChangeRecordData(row.afterData); const fields = [...new Set([...Object.keys(beforeData), ...Object.keys(afterData)])]; const rows = fields - .filter( - field => + .filter(field => { + // 前后都为空的字段不展示('' 与 null 序列化不同,单靠 JSON 比对会漏掉) + if ( + this.isEmptyChangeRecordValue(beforeData[field]) && + this.isEmptyChangeRecordValue(afterData[field]) + ) { + return false; + } + return ( JSON.stringify(this.normalizeChangeRecordValue(beforeData[field])) !== JSON.stringify(this.normalizeChangeRecordValue(afterData[field])) - ) + ); + }) .map(field => ({ field: this.getContractChangeFieldLabel(field), before: this.formatContractChangeValue(field, beforeData[field]), @@ -2496,6 +2603,21 @@ export default { } } +.contract-change-record-content-cell { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.contract-change-record-content-tooltip { + max-width: 900px; + max-height: 320px; + overflow: auto; + white-space: pre-wrap; + word-break: break-word; +} + .contract-change-record-detail-meta { display: flex; flex-wrap: wrap; @@ -2505,6 +2627,8 @@ export default { } :deep(.contract-change-record-detail-dialog .el-dialog__body) { + max-height: 65vh; + overflow: auto; padding-top: 12px; } diff --git a/src/views/transportCapacity/vehicle.vue b/src/views/transportCapacity/vehicle.vue index b324d3a..e7afbbd 100644 --- a/src/views/transportCapacity/vehicle.vue +++ b/src/views/transportCapacity/vehicle.vue @@ -120,15 +120,252 @@ - - - - {{ formatAuditValue(item) }} - - - + + + + + + + {{ auditValue('organizationName') }} + + + + + + + {{ auditValue('plateNo') }} + + + + + {{ auditValue('plateColor') }} + + + + + {{ auditValue('vehicleType') }} + + + + + + + {{ auditValue('businessRelation') }} + + + + + {{ auditValue('energyType') }} + + + + + + {{ auditValue('compulsoryScrapDate') }} + + + + + + + + + + + {{ auditValue('outerLength') }} + + + + + {{ auditValue('outerWidth') }} + + + + + {{ auditValue('outerHeight') }} + + + + + + + {{ auditValue('approvedLoadKg') }} + + + + + {{ auditValue('tractionMassKg') }} + + + + + + + + + + {{ auditValue('customsRecordNo') }} + + + + + {{ auditValue('drivingLicenseNo') }} + + + + + + {{ auditValue('drivingLicenseStartDate') }} + + + + + + + + + {{ auditValue('drivingLicenseEndDate') }} + + + + + + + {{ auditValue('roadTransportCertNo') }} + + + + + + + {{ auditValue('roadTransportCertStartDate') }} + + + + + + + + + {{ auditValue('roadTransportCertEndDate') }} + + + + + + + {{ auditValue('annualReviewEndDate') }} + + + + + + {{ auditValue('registrationNo') }} + + + + + + + {{ auditValue('registrationDate') }} + + + + + + + {{ auditValue('remark') }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - +