From eaece0209d9f6a19d663638c579d26c4f024c3f6 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Sat, 10 Oct 2026 00:53:17 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20=E5=90=88=E5=90=8C=E7=BB=93?= =?UTF-8?q?=E7=AE=97=E5=B8=81=E7=A7=8D=E8=81=94=E5=8A=A8=E8=AE=A1=E8=B4=B9?= =?UTF-8?q?=E4=B8=8E=E8=BF=90=E5=8D=95=E8=AE=A1=E4=BB=B7=E5=8D=95=E4=BD=8D?= =?UTF-8?q?=EF=BC=8C=E5=B9=B6=E5=AE=8C=E5=96=84=E5=90=88=E5=90=8C=E4=B8=8E?= =?UTF-8?q?=E6=B4=BE=E8=BD=A6=E4=BA=A4=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 结算币种驱动计费单价单位及运单/计划/总单计价单位;同步合同编号预览、部门前缀配置、派车备注与查看弹窗等交互优化。 Co-authored-by: Cursor --- src/api/business/contract-manage.js | 8 + src/option/business/common.js | 1 + src/option/business/contract-manage.js | 47 +++- .../transportCapacity/vehicle-dispatch.js | 6 + src/utils/currency.js | 109 +++++++++ .../components/billing-plan-editor.vue | 7 +- .../contract-attachment-section.vue | 207 ++++++++++++++---- .../components/master-order-dispatch.vue | 52 ++++- .../components/transport-plan-page.vue | 84 ++++++- .../components/waybill-import-dialog.vue | 41 +++- .../components/waybill-manage-page.vue | 79 +++++-- src/views/business/contract-manage-change.vue | 69 ++++-- src/views/business/contract-manage.vue | 172 ++++++++++++--- src/views/system/dept.vue | 29 +++ .../transportCapacity/vehicle-dispatch.vue | 39 ++++ src/views/transportCapacity/vehicle.vue | 75 +++++-- 16 files changed, 867 insertions(+), 158 deletions(-) create mode 100644 src/utils/currency.js diff --git a/src/api/business/contract-manage.js b/src/api/business/contract-manage.js index 6010d39..872a5ea 100644 --- a/src/api/business/contract-manage.js +++ b/src/api/business/contract-manage.js @@ -90,3 +90,11 @@ export const expireStats = params => method: 'get', params, }); + +/** 按合同负责部门预览下一合同编号 */ +export const nextContractNo = organizationId => + request({ + url: `${baseUrl}/next-contract-no`, + method: 'get', + params: { organizationId }, + }); diff --git a/src/option/business/common.js b/src/option/business/common.js index 1f6feb2..8221c47 100644 --- a/src/option/business/common.js +++ b/src/option/business/common.js @@ -117,6 +117,7 @@ export const contractCategoryOptions = [ export const signTypeOptions = [ { label: '新签', value: '新签' }, { label: '续签', value: '续签' }, + { label: '补充协议', value: '补充协议' }, ]; export const contractStageOptions = [ diff --git a/src/option/business/contract-manage.js b/src/option/business/contract-manage.js index 2bb7999..0d1e912 100644 --- a/src/option/business/contract-manage.js +++ b/src/option/business/contract-manage.js @@ -6,6 +6,7 @@ import { contractStageOptions, createCrudOption, effectiveTypeOptions, + nonNegativeRule, selectRule, signTypeOptions, textRule, @@ -61,13 +62,14 @@ export const config = { ['contractNo', '合同编号'], ['contractName', '合同名称'], ['projectName', '所属项目'], - ['organizationName', '所属组织'], + ['organizationName', '合同负责部门'], ['contractCategory', '合同类别'], ['signType', '签约类型'], ['partyA', '甲方'], ['partyB', '乙方'], ['handlerUserName', '经办人'], ['signDate', '签订日期'], + ['applyDate', '申请日期'], ['contractFormat', '合同格式'], ['legalSealFlag', '是否需要加盖法人章'], ['copyCount', '一式(份)'], @@ -76,7 +78,8 @@ export const config = { ['templateFlag', '是否范本'], ['originalContractNo', '原件合同编号'], ['electronicSealFlag', '是否电子章'], - ['remark', '备注', 2], + ['mainContent', '合同主要内容', 2], + ['remark', '其他事项说明', 2], ], }, { @@ -294,17 +297,17 @@ export const option = createCrudOption([ display: false, }, { - label: '所属组织', + label: '合同负责部门', prop: 'organizationName', search: true, searchOrder: 3, searchslot: true, - searchPlaceholder: '请选择 所属组织', + searchPlaceholder: '请选择 合同负责部门', formslot: true, order: 120, dicData: [], minWidth: 150, - rules: selectRule('所属组织'), + rules: selectRule('合同负责部门'), }, { label: '合同类别', @@ -435,6 +438,17 @@ export const option = createCrudOption([ minWidth: 130, rules: [{ required: true, message: '请选择签订日期', trigger: 'change' }], }, + { + label: '申请日期', + prop: 'applyDate', + type: 'date', + format: 'YYYY-MM-DD', + valueFormat: 'YYYY-MM-DD', + sortable: true, + minWidth: 130, + addDisplay: false, + editDisplay: false, + }, { label: '结算类型', prop: 'settlementMode', @@ -468,11 +482,10 @@ export const option = createCrudOption([ { label: '否', value: 0 }, { label: '是', value: 1 }, ], - value: 0, + clearable: true, hide: true, order: 70, minWidth: 100, - rules: selectRule('是否需要加盖法人章'), }, { label: '一式(份)', @@ -500,6 +513,10 @@ export const option = createCrudOption([ hide: true, order: 45, minWidth: 130, + rules: [ + { required: true, message: '请输入合同金额', trigger: 'blur' }, + nonNegativeRule('合同金额'), + ], }, { label: '是否范本', @@ -617,7 +634,19 @@ export const option = createCrudOption([ minWidth: 110, }, { - label: '备注', + label: '合同主要内容', + prop: 'mainContent', + type: 'textarea', + span: 24, + minRows: 4, + hide: true, + order: 41, + maxlength: 1000, + showWordLimit: true, + rules: textRule('合同主要内容', 1000), + }, + { + label: '其他事项说明', prop: 'remark', type: 'textarea', span: 24, @@ -626,7 +655,7 @@ export const option = createCrudOption([ order: 40, maxlength: 2000, showWordLimit: true, - rules: textRule('备注', 2000), + rules: textRule('其他事项说明', 2000), }, { label: '', diff --git a/src/option/transportCapacity/vehicle-dispatch.js b/src/option/transportCapacity/vehicle-dispatch.js index 0e50813..55361af 100644 --- a/src/option/transportCapacity/vehicle-dispatch.js +++ b/src/option/transportCapacity/vehicle-dispatch.js @@ -101,6 +101,7 @@ export const option = { minWidth: 120, addDisplay: false, editDisplay: false, + viewDisplay: false, }, { label: '当前节点', @@ -108,6 +109,7 @@ export const option = { minWidth: 120, addDisplay: false, editDisplay: false, + viewDisplay: false, }, { label: '当前处理人', @@ -115,6 +117,7 @@ export const option = { minWidth: 130, addDisplay: false, editDisplay: false, + viewDisplay: false, }, { label: '创建人', @@ -122,6 +125,7 @@ export const option = { minWidth: 120, addDisplay: false, editDisplay: false, + viewDisplay: false, }, { label: '创建时间', @@ -132,6 +136,7 @@ export const option = { minWidth: 170, addDisplay: false, editDisplay: false, + viewDisplay: false, }, // ========== 新增/编辑表单专用(不进表格)========== { @@ -165,6 +170,7 @@ export const option = { type: 'textarea', hide: true, span: 24, + minRows: 2, maxlength: 200, showWordLimit: true, rules: [{ max: 200, message: '备注不能超过200个字', trigger: 'blur' }], diff --git a/src/utils/currency.js b/src/utils/currency.js new file mode 100644 index 0000000..ea06789 --- /dev/null +++ b/src/utils/currency.js @@ -0,0 +1,109 @@ +const CURRENCY_NAME_MAP = { + CNY: '人民币', + RMB: '人民币', + USD: '美元', + EUR: '欧元', + JPY: '日元', + GBP: '英镑', +}; + +const CURRENCY_UNIT_MAP = { + CNY: '元', + RMB: '元', + USD: '美元', + EUR: '欧元', + JPY: '日元', + GBP: '英镑', +}; + +const CURRENCY_NAME_TO_UNIT = { + 人民币: '元', + 美元: '美元', + 欧元: '欧元', + 日元: '日元', + 英镑: '英镑', +}; + +/** + * 归一化币种代码(CNY → RMB) + */ +export function normalizeCurrencyCode(value, fallback = 'RMB') { + const raw = + typeof value === 'object' + ? value?.dictKey || value?.value || value?.code || value?.dictValue || '' + : value; + const normalized = String(raw || fallback) + .trim() + .toUpperCase(); + const code = normalized.includes(' - ') ? normalized.split(' - ')[0].trim() : normalized; + if (!code) return fallback; + return code === 'CNY' ? 'RMB' : code; +} + +/** + * 币种中文名称 + */ +export function currencyName(value) { + const text = String(value || ''); + if (text.includes(' - ')) return text.slice(text.indexOf(' - ') + 3); + return CURRENCY_NAME_MAP[text.toUpperCase()] || text; +} + +/** + * 金额单位文案:RMB/CNY → 元,USD → 美元 + */ +export function currencyUnit(value) { + const text = String(value || ''); + let code = text; + if (text.includes(' - ')) { + code = text.split(' - ')[0].trim(); + } + const upperCode = code.toUpperCase(); + if (CURRENCY_UNIT_MAP[upperCode]) { + return CURRENCY_UNIT_MAP[upperCode]; + } + if (CURRENCY_NAME_TO_UNIT[code]) { + return CURRENCY_NAME_TO_UNIT[code]; + } + return text || '元'; +} + +/** + * 将计价单位前缀替换为当前币种单位,如 元/吨 → 美元/吨 + */ +export function remapPriceUnitCurrency(priceUnit, currency) { + const text = String(priceUnit || '').trim(); + if (!text || !text.includes('/')) return text; + const suffix = text.slice(text.indexOf('/')); + return `${currencyUnit(currency)}${suffix}`; +} + +/** + * 按数量单位生成默认计价单位 + */ +export function resolvePriceUnitByQuantityUnit(quantityUnit, currency = 'RMB') { + const unit = currencyUnit(currency); + return ( + { + 吨: `${unit}/吨`, + 方: `${unit}/立方`, + 件: `${unit}/件`, + }[String(quantityUnit || '').trim()] || '' + ); +} + +/** + * 将字典中的计价单位选项映射到当前币种 + */ +export function mapFeeUnitOptions(options = [], currency = 'RMB') { + const unit = currencyUnit(currency); + return (options || []).map(item => { + const label = String(item.label || item.value || ''); + const value = String(item.value || item.label || ''); + return { + ...item, + label: label.replace(/^元/, unit).replace(/^人民币/, unit), + value: value.replace(/^元/, unit).replace(/^人民币/, unit), + }; + }); +} diff --git a/src/views/business/components/billing-plan-editor.vue b/src/views/business/components/billing-plan-editor.vue index 115798f..19f4b17 100644 --- a/src/views/business/components/billing-plan-editor.vue +++ b/src/views/business/components/billing-plan-editor.vue @@ -171,7 +171,7 @@ :label="item.label" :value="item.value" /> - - + - + @@ -173,20 +182,30 @@ - + + + + 添加 - + { + if (value === undefined || value === null || value === '') { + return callback(new Error('请输入合同金额')); + } + if (Number(value) < 0) { + return callback(new Error('合同金额不能小于0')); + } + callback(); + }, + trigger: 'blur', + }, + ], changeReason: [{ max: 500, message: '变更原因不能超过500个字符', trigger: 'blur' }], }, }; @@ -433,7 +476,7 @@ export default { return this.$route.query.name || '合同变更'; }, changeAttachmentLocationOptions() { - return ['合同文件', '其它附件', '变更材料']; + return ['用印材料', '附件(佐证材料等)', '变更材料']; }, contractAmountDisplay() { const value = this.form.contractAmount; @@ -601,7 +644,7 @@ export default { return []; } }, - // 审核通过后上一次的变更材料归集到「其它附件」,按文件地址/文件名去重,避免重复展示 + // 审核通过后上一次的变更材料归集到「附件(佐证材料等)」,按文件地址/文件名去重,避免重复展示 mergeChangeAttachments(attachments = [], changeAttachments = []) { const key = item => this.attachmentUrl(item) || this.attachmentName(item); const exists = new Set(attachments.map(key)); @@ -655,11 +698,11 @@ export default { previewAttachment(row, rows = this.attachments) { const url = this.attachmentUrl(row); if (!url) { this.$message.warning('附件地址为空,无法预览'); return; } if (this.isAttachmentImage(row)) { this.imagePreviewUrls = (rows || []).filter(item => this.isAttachmentImage(item) && this.attachmentUrl(item)).map(item => this.attachmentUrl(item)); this.imagePreviewIndex = Math.max(this.imagePreviewUrls.indexOf(url), 0); this.imagePreviewVisible = true; return; } this.previewFile = { name: this.attachmentName(row), url, mimeType: row.mimeType || row.contentType || '' }; this.documentPreviewVisible = true; }, handleAttachmentUploadToLocation({ location, rows = [] }) { if (!rows.length) return; - if (location === '合同文件') { + if (isContractSealMaterialLocation(location)) { this.contractFileRows = [...(this.contractFileRows || []), ...rows]; return; } - if (location === '其它附件') { + if (isContractSupportingAttachmentLocation(location)) { this.attachments = [...(this.attachments || []), ...rows]; return; } diff --git a/src/views/business/contract-manage.vue b/src/views/business/contract-manage.vue index 75130e4..f6a7e2d 100644 --- a/src/views/business/contract-manage.vue +++ b/src/views/business/contract-manage.vue @@ -128,7 +128,7 @@ {{ detailValue('partyA') }} {{ detailValue('partyB') }} {{ detailContractPeriod }} - {{ + {{ detailValue('organizationName') }} {{ @@ -140,6 +140,9 @@ {{ detailValue('signDate') }} + {{ + detailValue('applyDate') + }} {{ detailValue('contractFormat') }} @@ -171,14 +174,17 @@ {{ detailElectronicSealText }} - {{ + {{ + detailValue('mainContent') + }} + {{ detailValue('remark') }} @@ -417,7 +429,7 @@ /> - + + + +
- + - + @@ -576,20 +598,30 @@
- + + + + @@ -1026,6 +1061,8 @@ import ChangeRecordDetailDialog from '@/components/change-record-detail-dialog/m import ContractAttachmentSection, { attachmentName as sharedAttachmentName, attachmentUrl as sharedAttachmentUrl, + isContractSealMaterialLocation, + isContractSupportingAttachmentLocation, normalizeContractFileRows, } from './components/contract-attachment-section.vue'; import PdfPreview from '@/components/pdf-preview/main.vue'; @@ -1093,8 +1130,9 @@ const defaultForm = () => ({ partyB: '', handlerUserName: '', signDate: '', + applyDate: '', contractFormat: '', - legalSealFlag: 0, + legalSealFlag: null, copyCount: '', settlementCurrency: '', invoiceCycle: '', @@ -1110,6 +1148,7 @@ const defaultForm = () => ({ effectiveType: '', feeGenerationMode: 'system', archiveStatus: '未归档', + mainContent: '', remark: '', }); // 复制新增:仅保留可编辑字段与明细,剔除主键、编号、审批与流程信息 @@ -1121,6 +1160,7 @@ const createCopiedDetail = (detail = {}) => { return { ...form, contractNo: '', + applyDate: '', archiveStatus: '未归档', feeGenerationMode: detail.feeGenerationMode || '', billingEnabled: detail.billingEnabled, @@ -1215,13 +1255,12 @@ export default { signType: [{ required: true, message: '请选择签约类型', trigger: 'change' }], settlementCurrency: [{ required: true, message: '请选择结算币种', trigger: 'change' }], projectName: [{ required: true, message: '请选择所属项目', trigger: 'change' }], - organizationName: [{ required: true, message: '请选择所属组织', trigger: 'change' }], + organizationName: [{ required: true, message: '请选择合同负责部门', trigger: 'change' }], partyA: [{ required: true, message: '请选择甲方', trigger: 'change' }], partyB: [{ required: true, message: '请选择乙方', trigger: 'change' }], signDate: [{ required: true, message: '请选择签订日期', trigger: 'change' }], settlementMode: [{ required: true, message: '请选择结算方式', trigger: 'change' }], contractFormat: [{ required: true, message: '请选择合同格式', trigger: 'change' }], - legalSealFlag: [{ required: true, message: '请选择是否需要加盖法人章', trigger: 'change' }], invoiceCycle: [ { validator: (rule, value, callback) => { @@ -1236,6 +1275,22 @@ export default { ], startDate: [{ required: true, message: '请选择开始日期', trigger: 'change' }], endDate: [{ required: true, message: '请选择结束日期', trigger: 'change' }], + contractAmount: [ + { required: true, message: '请输入合同金额', trigger: 'blur' }, + { + validator: (rule, value, callback) => { + if (value === undefined || value === null || value === '') { + return callback(new Error('请输入合同金额')); + } + if (Number(value) < 0) { + return callback(new Error('合同金额不能小于0')); + } + callback(); + }, + trigger: 'blur', + }, + ], + mainContent: [{ max: 1000, message: '合同主要内容不能超过1000个字符', trigger: 'blur' }], }, submitAction: '', projectOptions: [], @@ -1345,8 +1400,8 @@ export default { return this.$route.query.attachmentUpload === '1'; }, contractAttachmentLocationOptions() { - if (this.isAttachmentUploadMode) return ['合同文件']; - return ['合同文件', '其它附件']; + if (this.isAttachmentUploadMode) return ['用印材料']; + return ['用印材料', '附件(佐证材料等)']; }, contractAmountDisplay() { const value = this.form.contractAmount; @@ -1413,7 +1468,7 @@ export default { expandTrigger: 'click', }; }, - // 所属组织级联:selectedOrganizationId 存部门 id,级联控件用根到节点的 id 路径, + // 合同负责部门级联:selectedOrganizationId 存部门 id,级联控件用根到节点的 id 路径, // 两者在此双向转换;依赖 organizationOptions,树加载完成后回显自动刷新 organizationCascaderValue: { get() { @@ -1775,6 +1830,7 @@ export default { this.applyDetail(createCopiedDetail(detail)); this.form.handlerUserName = this.userInfo?.realName || this.userInfo?.userName || this.userInfo?.account || ''; + this.refreshPreviewContractNo(); this.$nextTick(() => this.$refs.contractForm?.clearValidate()); }, closeForm() { @@ -1789,9 +1845,10 @@ export default { ['contractName', '合同名称'], ['contractCategory', '合同类型'], ['projectName', '所属项目'], - ['organizationName', '所属组织'], + ['organizationName', '合同负责部门'], ['partyA', '甲方'], ['partyB', '乙方'], + ['contractAmount', '合同金额'], ]; const empty = required.find(([prop]) => { const value = this.form[prop]; @@ -1799,10 +1856,16 @@ export default { }); if (empty) { this.$message.warning( - empty[0] === 'contractName' ? `请输入${empty[1]}` : `请选择${empty[1]}` + ['contractName', 'contractAmount'].includes(empty[0]) + ? `请输入${empty[1]}` + : `请选择${empty[1]}` ); return false; } + if (Number(this.form.contractAmount) < 0) { + this.$message.warning('合同金额不能小于0'); + return false; + } return true; } if (!this.form.contractName?.trim()) { @@ -1825,7 +1888,7 @@ export default { if (action !== 'formal' && action !== 'edit') return true; const required = [ ['projectName', '所属项目'], - ['organizationName', '所属组织'], + ['organizationName', '合同负责部门'], ['partyA', '甲方'], ['partyB', '乙方'], ['startDate', '合同开始日期'], @@ -1833,10 +1896,20 @@ export default { ['signDate', '签订日期'], ['settlementMode', '结算方式'], ['contractFormat', '合同格式'], + ['contractAmount', '合同金额'], ]; - const empty = required.find(([prop]) => !this.form[prop]); + const empty = required.find(([prop]) => { + const value = this.form[prop]; + return value === undefined || value === null || String(value).trim() === ''; + }); if (empty) { - this.$message.warning(`请选择或输入${empty[1]}`); + this.$message.warning( + empty[0] === 'contractAmount' ? `请输入${empty[1]}` : `请选择或输入${empty[1]}` + ); + return false; + } + if (Number(this.form.contractAmount) < 0) { + this.$message.warning('合同金额不能小于0'); return false; } return ( @@ -2026,11 +2099,11 @@ export default { }, handleListAttachmentUploadToLocation({ location, rows = [] }) { if (!rows.length) return; - if (location === '合同文件') { + if (isContractSealMaterialLocation(location)) { this.listUploadContractFileRows = [...(this.listUploadContractFileRows || []), ...rows]; return; } - if (location === '其它附件') { + if (isContractSupportingAttachmentLocation(location)) { this.listUploadAttachmentRows = [...(this.listUploadAttachmentRows || []), ...rows]; } }, @@ -2150,6 +2223,9 @@ export default { column.renderSearch = scope => this.renderOrganizationSearch(scope); } this.syncCurrentOrganizationOption(); + if (this.isFormPage && this.formMode === 'add' && this.form.organizationId) { + this.refreshPreviewContractNo(); + } }) .finally(() => { this.organizationLoading = false; @@ -2212,6 +2288,29 @@ export default { } : { organizationId: '', organizationName: '' } ); + if (this.formMode === 'add') { + this.refreshPreviewContractNo(); + } + }, + refreshPreviewContractNo() { + if (this.formMode !== 'add') return; + const organizationId = this.form.organizationId; + if (!organizationId) { + this.form.contractNo = ''; + return; + } + const requestId = (this.contractNoPreviewRequestId = + (this.contractNoPreviewRequestId || 0) + 1); + api + .nextContractNo(organizationId) + .then(res => { + if (requestId !== this.contractNoPreviewRequestId) return; + this.form.contractNo = res.data?.data || ''; + }) + .catch(() => { + if (requestId !== this.contractNoPreviewRequestId) return; + this.form.contractNo = ''; + }); }, syncCurrentOrganizationOption() { if (!this.form.organizationId || !this.form.organizationName) return; @@ -2225,7 +2324,7 @@ export default { this.organizationFlatOptions.unshift(item); }, renderOrganizationSearch(scope) { - // 搜索区所属组织:单选级联(与新增/编辑表单一致) + // 搜索区合同负责部门:单选级联(与新增/编辑表单一致) // 级联返回 id 路径数组,提交时由 normalizeSearch 取末级 id 转组织名称 return h(ElCascader, { modelValue: Array.isArray(scope.row?.organizationName) ? scope.row.organizationName : [], @@ -2244,7 +2343,7 @@ export default { filterable: true, clearable: true, style: 'width: 100%', - placeholder: '请选择 所属组织', + placeholder: '请选择 合同负责部门', }); }, integerInput(prop, value) { @@ -2289,11 +2388,11 @@ export default { }, handleAttachmentUploadToLocation({ location, rows = [] }) { if (!rows.length) return; - if (location === '合同文件') { + if (isContractSealMaterialLocation(location)) { this.contractFileRows = [...(this.contractFileRows || []), ...rows]; return; } - if (location === '其它附件') { + if (isContractSupportingAttachmentLocation(location)) { this.attachmentRows = [...(this.attachmentRows || []), ...rows]; } }, @@ -2654,15 +2753,16 @@ export default { templateFlag: '是否范本', originalContractNo: '原件合同编号', electronicSealFlag: '是否电子章', - remark: '备注', + mainContent: '合同主要内容', + remark: '其他事项说明', feeGenerationMode: '费用生成模式', billingPlanJson: '计费方案', settlementRuleJson: '结算单规则', preSettlementConfigJson: '预结算配置', formalSettlementConfigJson: '正式结算配置', paymentRatioJson: '付款比例设置', - contractFileJson: '合同文件', - attachmentsJson: '其它附件', + contractFileJson: '用印材料', + attachmentsJson: '附件(佐证材料等)', changeAttachmentsJson: '变更材料', }; return labels[field] || field; diff --git a/src/views/system/dept.vue b/src/views/system/dept.vue index 8e81809..0390616 100644 --- a/src/views/system/dept.vue +++ b/src/views/system/dept.vue @@ -357,6 +357,33 @@ export default { }, ], }, + { + label: '合同编码前缀', + prop: 'contractCodePrefix', + minWidth: 160, + order: 62, + maxlength: 64, + showWordLimit: true, + placeholder: '如 GWZH成都', + tip: '合同编号规则:前缀(年份)三位流水号,流水按本部门独立编号', + rules: [ + { + validator: (rule, value, callback) => { + const prefix = String(value || '').trim(); + if (!prefix) { + callback(); + return; + } + if (prefix.length > 64) { + callback(new Error('合同编码前缀不能超过64个字符')); + return; + } + callback(); + }, + trigger: 'blur', + }, + ], + }, { label: '组织简称', prop: 'shortName', @@ -995,6 +1022,7 @@ export default { row.tenantId = row.tenantId || this.userInfo.tenantId || website.tenantId; row.deptCode = String(row.deptCode || '').trim(); row.kingdeeDeptCode = String(row.kingdeeDeptCode || '').trim(); + row.contractCodePrefix = String(row.contractCodePrefix || '').trim(); row.leaderId = func.join(row.leaderId); this.normalizeTopParent(row); if (!this.prepareDeptSubmitRow(row)) { @@ -1030,6 +1058,7 @@ export default { row.tenantId = row.tenantId || this.userInfo.tenantId || website.tenantId; row.deptCode = String(row.deptCode || '').trim(); row.kingdeeDeptCode = String(row.kingdeeDeptCode || '').trim(); + row.contractCodePrefix = String(row.contractCodePrefix || '').trim(); row.leaderId = func.join(row.leaderId); this.normalizeTopParent(row); if (!this.prepareDeptSubmitRow(row)) { diff --git a/src/views/transportCapacity/vehicle-dispatch.vue b/src/views/transportCapacity/vehicle-dispatch.vue index 851abd1..81559d8 100644 --- a/src/views/transportCapacity/vehicle-dispatch.vue +++ b/src/views/transportCapacity/vehicle-dispatch.vue @@ -428,4 +428,43 @@ export default { .vehicle-dispatch-page :deep(.vehicle-dispatch-form-control) { width: 100%; } +/* 查看弹窗保持与新增/编辑相同的表单样式,仅禁用控件 */ +.vehicle-dispatch-page :deep(.avue--detail .el-form-item) { + background-color: transparent; +} +.vehicle-dispatch-page :deep(.avue--detail .el-input.is-disabled .el-input__wrapper) { + box-shadow: 0 0 0 1px var(--el-disabled-border-color) inset; + background-color: var(--el-disabled-bg-color); +} +.vehicle-dispatch-page :deep(.avue--detail .el-input.is-disabled .el-input__inner), +.vehicle-dispatch-page :deep(.avue--detail .el-textarea.is-disabled .el-textarea__inner), +.vehicle-dispatch-page :deep(.avue--detail .el-range-editor.is-disabled), +.vehicle-dispatch-page :deep(.avue--detail .el-range-editor.is-disabled input) { + color: var(--el-disabled-text-color); + background-color: var(--el-disabled-bg-color); + padding-left: 11px; + box-shadow: 0 0 0 1px var(--el-disabled-border-color) inset; + -webkit-text-fill-color: var(--el-disabled-text-color); +} +.vehicle-dispatch-page :deep(.avue--detail .el-select__wrapper.is-disabled) { + background-color: var(--el-disabled-bg-color); + box-shadow: 0 0 0 1px var(--el-disabled-border-color) inset !important; +} +.vehicle-dispatch-page :deep(.avue--detail .el-select__suffix), +.vehicle-dispatch-page :deep(.avue--detail .el-input__suffix), +.vehicle-dispatch-page :deep(.avue--detail .el-input__prefix) { + display: inline-flex; +} +.vehicle-dispatch-page :deep(.avue--detail .el-input__inner), +.vehicle-dispatch-page :deep(.avue--detail .el-textarea__inner) { + border: none; +} +.vehicle-dispatch-page :deep(.avue--detail .el-cascader) { + width: 100%; +} +.vehicle-dispatch-page :deep(.avue--detail .el-cascader .el-input.is-disabled .el-input__wrapper), +.vehicle-dispatch-page :deep(.avue--detail .el-autocomplete .el-input.is-disabled .el-input__wrapper) { + box-shadow: 0 0 0 1px var(--el-disabled-border-color) inset; + background-color: var(--el-disabled-bg-color); +} diff --git a/src/views/transportCapacity/vehicle.vue b/src/views/transportCapacity/vehicle.vue index 42553a2..4ced43f 100644 --- a/src/views/transportCapacity/vehicle.vue +++ b/src/views/transportCapacity/vehicle.vue @@ -985,7 +985,7 @@ export default { } this.applyDrivingLicenseValidity(inspectionValidity); if (registrationDate) { - this.vehicleForm.registrationDate = this.normalizeDate(registrationDate); + this.vehicleForm.registrationDate = this.normalizeDate(registrationDate, 'first'); } if (registrationNo) { this.vehicleForm.registrationNo = String(registrationNo).replace(/\s/g, ''); @@ -1000,6 +1000,7 @@ export default { 'vehicleType', 'drivingLicenseNo', 'drivingLicenseEndDate', + 'registrationDate', 'registrationNo', 'approvedLoadKg', 'outerLength', @@ -1057,9 +1058,9 @@ export default { this.vehicleForm.roadTransportCertEndDate = ''; return; } - const dateList = value.match(/\d{4}[-/.年]\d{1,2}[-/.月]\d{1,2}日?/g) || []; - if (dateList.length) { - this.vehicleForm.roadTransportCertEndDate = this.normalizeDate(dateList[dateList.length - 1]); + const endDate = this.normalizeDate(value, 'last'); + if (endDate) { + this.vehicleForm.roadTransportCertEndDate = endDate; this.vehicleForm.roadTransportCertLongTerm = 0; } }, @@ -1071,9 +1072,9 @@ export default { this.vehicleForm.drivingLicenseEndDate = ''; return; } - const dateList = value.match(/\d{4}[-/.年]\d{1,2}[-/.月]\d{1,2}日?/g) || []; - if (dateList.length) { - this.vehicleForm.drivingLicenseEndDate = this.normalizeDate(dateList[dateList.length - 1]); + const endDate = this.normalizeDate(value, 'last'); + if (endDate) { + this.vehicleForm.drivingLicenseEndDate = endDate; this.vehicleForm.drivingLicenseLongTerm = 0; } }, @@ -1150,11 +1151,11 @@ export default { this.vehicleForm.drivingLicenseNo = drivingLicenseNo; } if (drivingLicenseEndDate) { - this.vehicleForm.drivingLicenseEndDate = this.normalizeDate(drivingLicenseEndDate); + this.vehicleForm.drivingLicenseEndDate = this.normalizeDate(drivingLicenseEndDate, 'last'); this.vehicleForm.drivingLicenseLongTerm = 0; } if (registrationDate) { - this.vehicleForm.registrationDate = this.normalizeDate(registrationDate); + this.vehicleForm.registrationDate = this.normalizeDate(registrationDate, 'first'); } if (registrationNo) { this.vehicleForm.registrationNo = registrationNo; @@ -1165,18 +1166,60 @@ export default { 'vehicleType', 'drivingLicenseNo', 'drivingLicenseEndDate', + 'registrationDate', 'registrationNo', ].forEach(prop => { this.$refs.vehicleForm?.validateField(prop); }); }); }, - normalizeDate(value = '') { + /** + * OCR 日期常为区间(如 2022-08-17至2024-01-05)或含无效日(如 01-65)。 + * 提取可解析的合法日期;prefer=first 取登记类起始日,prefer=last 取有效期截止日。 + */ + normalizeDate(value = '', prefer = 'first') { const dateValue = String(value || '').trim(); - const match = dateValue.match(/^(\d{4})[-/.年](\d{1,2})[-/.月](\d{1,2})日?$/); - if (!match) return dateValue; - const [, year, month, day] = match; - return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`; + if (!dateValue) return ''; + const segments = dateValue.match(/\d{4}[-/.年]\d{1,2}[-/.月]\d{1,2}日?/g) || []; + const sourceList = segments.length ? segments : [dateValue]; + const candidates = sourceList + .map(item => { + const match = String(item) + .trim() + .match(/^(\d{4})[-/.年](\d{1,2})[-/.月](\d{1,2})日?$/); + if (!match) return ''; + const year = Number(match[1]); + const month = Number(match[2]); + const day = Number(match[3]); + const date = new Date(year, month - 1, day); + if ( + date.getFullYear() !== year || + date.getMonth() !== month - 1 || + date.getDate() !== day + ) { + return ''; + } + return `${String(year).padStart(4, '0')}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`; + }) + .filter(Boolean); + if (!candidates.length) return ''; + return prefer === 'last' ? candidates[candidates.length - 1] : candidates[0]; + }, + normalizeVehicleDateFields(form = {}) { + const dateFields = [ + ['compulsoryScrapDate', 'first'], + ['drivingLicenseStartDate', 'first'], + ['drivingLicenseEndDate', 'last'], + ['roadTransportCertStartDate', 'first'], + ['roadTransportCertEndDate', 'last'], + ['annualReviewEndDate', 'last'], + ['registrationDate', 'first'], + ]; + dateFields.forEach(([prop, prefer]) => { + if (form[prop] === undefined || form[prop] === null || form[prop] === '') return; + form[prop] = this.normalizeDate(form[prop], prefer); + }); + return form; }, normalizeVehicleType(value = '') { const vehicleType = String(value || '').trim(); @@ -1207,12 +1250,14 @@ export default { }, handleSubmit() { this.syncPlateNo(); + this.normalizeVehicleDateFields(this.vehicleForm); this.$refs.vehicleForm.validate(valid => { if (!valid) { return; } this.submitLoading = true; - submit(this.vehicleForm) + const payload = this.normalizeVehicleDateFields({ ...this.vehicleForm }); + submit(payload) .then(() => { this.$message.success('操作成功'); this.vehicleBox = false;