From 55583551b54cb3a2bcbafbff9c8c5eef18b38a02 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Tue, 25 Aug 2026 16:13:58 +0800 Subject: [PATCH] fix bug --- src/views/base/airport-master.vue | 18 +- src/views/base/cargo-type.vue | 4 +- src/views/base/fee-item.vue | 74 +- src/views/base/port-terminal.vue | 23 +- src/views/base/railway-station.vue | 5 +- src/views/business/common-route.vue | 5 +- .../components/billing-plan-editor.vue | 56 +- .../components/master-order-dispatch.vue | 6 +- src/views/business/contract-manage-change.vue | 816 ++++++++++++++++-- src/views/business/contract-manage.vue | 162 ++-- src/views/business/project-apply.vue | 17 +- .../settlement/receivable-payable-detail.vue | 1 - src/views/vehicle/customer-archive.vue | 26 +- vite.config.mjs | 26 +- 14 files changed, 953 insertions(+), 286 deletions(-) diff --git a/src/views/base/airport-master.vue b/src/views/base/airport-master.vue index a6073d1..ae7b61e 100644 --- a/src/views/base/airport-master.vue +++ b/src/views/base/airport-master.vue @@ -190,7 +190,9 @@ export default { } }; const validateIcaoCode = (rule, value, callback) => { - if (!/^[A-Z]{4}$/.test(String(value || '').toUpperCase())) { + if (!value) { + callback(); + } else if (!/^[A-Z]{4}$/.test(String(value).toUpperCase())) { callback(new Error('ICAO代码为4位大写字母')); } else { callback(); @@ -281,10 +283,7 @@ export default { prop: 'icaoCode', minWidth: 100, maxlength: 4, - rules: [ - { required: true, message: '请输入ICAO代码', trigger: 'blur' }, - { validator: validateIcaoCode, trigger: 'blur' }, - ], + rules: [{ validator: validateIcaoCode, trigger: 'blur' }], change: ({ value }) => this.handleIcaoChange(value), blur: ({ value }) => this.handleIcaoChange(value), }, @@ -376,7 +375,10 @@ export default { overHidden: false, maxlength: 255, showWordLimit: true, - rules: [{ max: 255, message: '详细地址不能超过255字', trigger: 'blur' }], + rules: [ + { required: true, message: '请输入详细地址', trigger: 'blur' }, + { max: 255, message: '详细地址不能超过255字', trigger: 'blur' }, + ], }, { label: '经度', @@ -674,6 +676,7 @@ export default { row.districtCode = String(row.districtCode || '').trim(); row.districtName = String(row.districtName || '').trim(); row.detailAddress = String(row.detailAddress || '').trim(); + row.icaoCode = String(row.icaoCode || '').trim() || undefined; row.longitude = normalizeCoordinateInput(row.longitude); row.latitude = normalizeCoordinateInput(row.latitude); if (!row.dataSource) row.dataSource = '手动录入'; @@ -696,6 +699,9 @@ export default { validateUnique(row) { const rowId = String(row.id || ''); const hasDuplicate = (params, prop) => { + if (!row[prop]) { + return Promise.resolve(false); + } return getList(1, 10, params).then(res => { const records = res.data.data.records || []; return records.some(item => item[prop] === row[prop] && String(item.id || '') !== rowId); diff --git a/src/views/base/cargo-type.vue b/src/views/base/cargo-type.vue index 41c1beb..2978a01 100644 --- a/src/views/base/cargo-type.vue +++ b/src/views/base/cargo-type.vue @@ -213,9 +213,9 @@ export const decorateCargoTypeFailDetail = async workbook => { } } reasonCell.value = decorated.join(';'); - // 仅操作 G 列:黑色字体、无背景填充,其余列保持后端原样 + // 仅失败原因列使用红色字体,其余列保持普通字体 reasonCell.fill = { type: 'pattern', pattern: 'none' }; - reasonCell.font = { color: { argb: 'FF000000' } }; + reasonCell.font = { color: { argb: 'FFFF0000' } }; reasonCell.alignment = { wrapText: true, vertical: 'top' }; }); // 加宽各列,避免内容拥挤(仅改列宽,不改动单元格样式) diff --git a/src/views/base/fee-item.vue b/src/views/base/fee-item.vue index 0fad1fe..de2b752 100644 --- a/src/views/base/fee-item.vue +++ b/src/views/base/fee-item.vue @@ -21,12 +21,6 @@ @on-load="onLoad" > - - - - - - @@ -99,24 +83,20 @@ import { exportBlob } from '@/api/common'; import { getDictionary } from '@/api/system/dictbiz'; import { getDeptTree } from '@/api/system/dept'; import { downloadXls } from '@/utils/util'; -import { openImportDialog } from '@/utils/import-excel'; import { getToken } from '@/utils/auth'; -import { getUploadHeaders } from '@/utils/upload'; -import { Upload, Download } from '@element-plus/icons-vue'; +import { Download } from '@element-plus/icons-vue'; import { mapGetters } from 'vuex'; import NProgress from 'nprogress'; import 'nprogress/nprogress.css'; export default { - components: { Upload, Download }, + components: { Download }, data() { return { form: {}, query: {}, loading: true, data: [], - excelBox: false, - excelForm: {}, dialogType: 'add', feeCategoryOptions: [], page: { @@ -184,6 +164,17 @@ export default { maxlength: 50, rules: [{ required: true, message: '请输入费用项', trigger: 'blur' }], }, + { + label: '备注', + prop: 'remark', + type: 'textarea', + minRows: 2, + span: 24, + hide: true, + maxlength: 200, + showWordLimit: true, + rules: [{ max: 200, message: '备注不能超过200个字', trigger: 'blur' }], + }, { label: '组织', prop: 'createDeptName', @@ -259,32 +250,6 @@ export default { }, ], }, - excelOption: { - submitBtn: false, - emptyBtn: false, - column: [ - { - label: '模板上传', - prop: 'excelFile', - type: 'upload', - drag: true, - loadText: '模板上传中,请稍等', - span: 24, - propsHttp: { - res: 'data', - }, - headers: getUploadHeaders(), - tip: '请上传 .xls,.xlsx 标准格式文件', - action: '/blade-system/fee-item/import-fee-item', - }, - { - label: '模板下载', - prop: 'excelTemplate', - formslot: true, - span: 24, - }, - ], - }, }; }, created() { @@ -364,6 +329,7 @@ export default { values.feeCategory = String(values.feeCategory || '').trim(); values.name = String(values.name || '').trim(); values.englishName = prefix ? `${prefix}${code}` : code; + values.remark = String(values.remark || '').trim() || undefined; if (!values.status) values.status = 1; return values; }, @@ -464,9 +430,6 @@ export default { refreshChange() { this.onLoad(this.page, this.query); }, - handleImport() { - openImportDialog(this, '费用项'); - }, buildExportParams() { return { ...this.query, @@ -492,15 +455,6 @@ export default { }); }); }, - handleTemplate() { - exportBlob( - `/blade-system/fee-item/export-template?${this.website.tokenHeader}=${getToken()}`, - undefined, - { feedback: true } - ).then(res => { - downloadXls(res.data, '费用项模板.xlsx'); - }); - }, onLoad(page, params = {}) { this.loading = true; getList(page.currentPage, page.pageSize, Object.assign(params, this.query)).then(res => { diff --git a/src/views/base/port-terminal.vue b/src/views/base/port-terminal.vue index 1dc5b06..c7cfc99 100644 --- a/src/views/base/port-terminal.vue +++ b/src/views/base/port-terminal.vue @@ -393,7 +393,7 @@ export default { dicData: [], hide: true, span: 12, - order: 100, + order: 101, placeholder: '请选择', rules: [{ required: false, message: '请选择上级港口', trigger: 'change' }], change: ({ value }) => this.handleParentChange(value), @@ -412,10 +412,11 @@ export default { minWidth: 120, slot: true, span: 12, + order: 101, + disabled: true, addDisplay: false, editDisplay: false, placeholder: '自动带出', - rules: [{ required: false, message: '请选择上级港口', trigger: 'change' }], }, { label: '国家', @@ -537,7 +538,10 @@ export default { placeholder: '请输入', maxlength: 255, showWordLimit: true, - rules: [{ max: 255, message: '详细地址不能超过255字', trigger: 'blur' }], + rules: [ + { required: true, message: '请输入详细地址', trigger: 'blur' }, + { max: 255, message: '详细地址不能超过255字', trigger: 'blur' }, + ], }, { label: '数据来源', @@ -551,9 +555,9 @@ export default { editDisplay: false, dicData: [ { label: '全部', value: '' }, - { label: '初始化导入', value: '初始化导入' }, + { label: '初始化录入', value: '初始化录入' }, { label: '批量导入', value: '批量导入' }, - { label: '手工导入', value: '手工导入' }, + { label: '手动录入', value: '手动录入' }, ], }, { @@ -761,6 +765,7 @@ export default { parentColumn.rules[0].required = required; } this.setColumnDisplay('parentId', required); + this.setColumnDisplay('parentCode', required); if (value === '港口') { this.form.parentId = undefined; this.form.parentCode = ''; @@ -992,7 +997,7 @@ export default { submitRow.parentCode = undefined; submitRow.parentName = undefined; } - if (!submitRow.dataSource) submitRow.dataSource = '手工导入'; + if (!submitRow.dataSource) submitRow.dataSource = '手动录入'; if (!submitRow.status) submitRow.status = 1; delete submitRow.terminalCode; delete submitRow.cityCode; @@ -1021,6 +1026,10 @@ export default { this.$message.warning('请选择区县'); return false; } + if (isBlank(row.detailAddress)) { + this.$message.warning('请输入详细地址'); + return false; + } if (isBlank(row.longitude)) { this.$message.warning('请输入经度'); return false; @@ -1133,7 +1142,7 @@ export default { beforeOpen(done, type) { if (type === 'add') { this.form.category = this.form.category || '港口'; - this.form.dataSource = this.form.dataSource || '手工导入'; + this.form.dataSource = this.form.dataSource || '手动录入'; this.form.status = this.form.status || 1; } this.handleCategoryChange(this.form.category || '港口'); diff --git a/src/views/base/railway-station.vue b/src/views/base/railway-station.vue index d7b4e94..bdfac8a 100644 --- a/src/views/base/railway-station.vue +++ b/src/views/base/railway-station.vue @@ -347,7 +347,10 @@ export default { overHidden: false, maxlength: 255, showWordLimit: true, - rules: [{ max: 255, message: '详细地址不能超过255字', trigger: 'blur' }], + rules: [ + { required: true, message: '请输入详细地址', trigger: 'blur' }, + { max: 255, message: '详细地址不能超过255字', trigger: 'blur' }, + ], }, { label: '经度', diff --git a/src/views/business/common-route.vue b/src/views/business/common-route.vue index ba7e367..e720363 100644 --- a/src/views/business/common-route.vue +++ b/src/views/business/common-route.vue @@ -166,7 +166,7 @@ class="common-route-page__address-dialog" @opened="loadAddressList" > - + - - - {{ displayValue(draft.planName) }} + {{ + transportModeLabel(draft.transportMode) + }} {{ isDefaultPlan(draft) ? '是' : '否' }} @@ -31,10 +34,27 @@ maxlength="100" :disabled="readonly" /> + 默认方案默认方案 > @@ -340,6 +360,7 @@ import { getList as getCargoTypeList } from '@/api/base/cargo-type'; import { getList as getFeeItemList } from '@/api/base/fee-item'; import { getLazyTree as getRegionLazyTree } from '@/api/base/region'; +import { InfoFilled } from '@element-plus/icons-vue'; import { getDictionary } from '@/api/system/dictbiz'; const clone = value => JSON.parse(JSON.stringify(value)); @@ -370,6 +391,7 @@ const defaultRule = () => ({ }); export default { + components: { InfoFilled }, props: { modelValue: Boolean, value: { type: Object, default: () => ({}) }, @@ -417,7 +439,10 @@ export default { cargoFlatOptions: [], cargoLoading: false, cargoRequest: null, - formRules: { planName: [{ required: true, message: '请输入方案名称', trigger: 'blur' }] }, + formRules: { + planName: [{ required: true, message: '请输入方案名称', trigger: 'blur' }], + transportMode: [{ required: true, message: '请选择运输方式', trigger: 'change' }], + }, }; }, computed: { @@ -446,6 +471,7 @@ export default { }, mounted() { this.loadDictionaries(); + this.ensureTransportModes(); }, watch: { value: { @@ -474,7 +500,7 @@ export default { const option = this.transportModeOptions.find( item => String(item.value) === String(value) || String(item.label) === String(value) ); - return this.displayValue(option?.label || value); + return this.displayValue(option?.label || this.draft.transportModeLabel || value); }, isDefaultPlan(plan) { return ( @@ -484,7 +510,14 @@ export default { ); }, normalizePlan(value = {}) { - const plan = { planName: '', defaultPlan: false, remark: '', ...clone(value || {}) }; + const plan = { + planName: '', + transportMode: '', + transportModeLabel: '', + defaultPlan: false, + remark: '', + ...clone(value || {}), + }; plan.rules = (value?.rules?.length ? value.rules : [defaultRule()]).map(rule => this.normalizeRule(rule) ); @@ -818,6 +851,10 @@ export default { this.$refs.formRef.validate(valid => { if (!valid || !this.validateRules()) return; const plan = clone(this.draft); + const transportMode = this.transportModeOptions.find( + item => String(item.value) === String(plan.transportMode) + ); + plan.transportModeLabel = transportMode?.label || plan.transportMode; plan.rules = plan.rules.map(rule => { const ranges = this.canEditLimit(rule) ? this.getRanges(rule) : []; return { @@ -837,6 +874,9 @@ export default { this.visible = false; }); }, + showDefaultPlanTip() { + this.$message.info('同一运输方式仅支持配置一个默认计费方案'); + }, openMatch(row, index) { this.matchIndex = index; this.matchForm = { ...defaultRule().matchCondition, ...(row.matchCondition || {}) }; @@ -1097,6 +1137,12 @@ export default { margin: 12px 0; } +.billing-plan-editor__default-tip { + margin-left: 6px; + color: #909399; + cursor: pointer; +} + .limit-list { display: flex; flex-direction: column; diff --git a/src/views/business/components/master-order-dispatch.vue b/src/views/business/components/master-order-dispatch.vue index 5b4efe4..f257d31 100644 --- a/src/views/business/components/master-order-dispatch.vue +++ b/src/views/business/components/master-order-dispatch.vue @@ -80,7 +80,7 @@ - + @@ -88,7 +88,7 @@ - + @@ -601,8 +601,6 @@ export default { if (goods.some(item => Number(item.dispatchQuantity) > this.availableDispatchQuantity(route, item))) return this.$message.warning('本次数量不能超过剩余数量'); this.syncFreightItems(route); if (!this.validateFreightItems(route)) return; - if (!route.estimatedStartTime) return this.$message.warning(`请选择${this.dateStartLabel(route)}`); - if (!route.estimatedEndTime) return this.$message.warning(`请选择${this.dateEndLabel(route)}`); if (!this.validateRoutePhones(route)) return; if (route.documentType === '运单') { if (this.isRoad(route)) { diff --git a/src/views/business/contract-manage-change.vue b/src/views/business/contract-manage-change.vue index d70b607..e30551f 100644 --- a/src/views/business/contract-manage-change.vue +++ b/src/views/business/contract-manage-change.vue @@ -4,35 +4,176 @@
基本信息
- - - - - - - - - - - - - -
- - + + + + + + + + + + + + + +
+
+ +
合同文件
-
批量下载
- +
+ 批量下载 +
+
-
计费信息
添加
- 系统生成手动生成 - +
+
计费信息
+ 添加 +
+ 系统生成手动生成 +
@@ -41,13 +182,66 @@ -
自动生成结算单开启关闭
- - - - - - +
+ 自动生成结算单开启关闭 +
+ + + + + +
@@ -56,29 +250,135 @@ - - - + + + 添加 - +
-
其它附件
批量下载
- - 上传附件 +
+
其它附件
+ 批量下载 +
+ + 上传附件
-
变更原因
变更材料
上传变更材料
- +
+
变更原因
+ +
变更材料
+ 上传变更材料 +
+ - - + + - + @@ -87,7 +387,13 @@ import * as api from '@/api/business/contract-manage'; import BillingPlanEditor from './components/billing-plan-editor.vue'; import { ElImageViewer } from 'element-plus'; import { OpenFileViewer } from '@open-file-viewer/vue'; -import { fallbackPlugin, imagePlugin, officePlugin, pdfPlugin, textPlugin } from '@open-file-viewer/core'; +import { + fallbackPlugin, + imagePlugin, + officePlugin, + pdfPlugin, + textPlugin, +} from '@open-file-viewer/core'; import '@open-file-viewer/core/style.css'; import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url'; @@ -101,52 +407,396 @@ const viewerPlugins = [ export default { components: { BillingPlanEditor, ElImageViewer, OpenFileViewer }, - data() { return { form: {}, period: [], plans: [], attachments: [], contractFiles: [], contractFileRows: [], selectedContractFiles: [], paymentRatioRows: [], settlementConfigTab: 'pre', changeMaterials: [], imagePreviewVisible: false, imagePreviewUrls: [], imagePreviewIndex: 0, documentPreviewVisible: false, previewFile: {}, viewerPlugins, viewerToolbar: { zoom: true, rotate: true, download: true, fullscreen: true }, feeGenerationMode: 'system', settlementRule: { autoGenerate: 1, settlementType: '月结', billCycleType: '固定截单日', billCutoffDay: 25, cycleDays: '' }, preSettlementConfig: {}, formalSettlementConfig: {}, settlementTypeOptions: ['月结','日结','周结','半月结','固定天数周期结算'], billCycleTypeOptions: ['固定截单日','自然月'], billCutoffDayOptions: Array.from({ length: 31 }, (_, index) => ({ label: `${index + 1}日`, value: index + 1 })), cycleDayOptions: [7,15,30,60].map(value => ({ label: `${value}天`, value })), planDialogVisible: false, planEditor: { planName: '', defaultPlan: false, remark: '', rules: [] }, planEditorIndex: -1, billingElements: ['按重量','按体积','按车辆','按里程','按吨·公里','固定金额(整单一口价)','按数量'], attachmentFileTypes: ['pdf','bmp','jpeg','png','jpg','doc','docx','ppt','pptx','xlsx','xls','eml','msg','zip'], rules: { contractName: [{ required: true, message: '请输入合同名称', trigger: 'blur' }], changeReason: [{ required: true, message: '请输入变更原因', trigger: 'blur' }] } }; }, - computed: { showSettlementBillCycleType() { return this.settlementRule.settlementType === '月结'; }, showSettlementBillCutoffDay() { return this.showSettlementBillCycleType && this.settlementRule.billCycleType === '固定截单日'; }, showSettlementCycleDays() { return this.settlementRule.settlementType === '固定天数周期结算'; } }, - 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) }; } }, + data() { + return { + form: {}, + period: [], + plans: [], + attachments: [], + contractFiles: [], + contractFileRows: [], + selectedContractFiles: [], + paymentRatioRows: [], + settlementConfigTab: 'pre', + changeMaterials: [], + imagePreviewVisible: false, + imagePreviewUrls: [], + imagePreviewIndex: 0, + documentPreviewVisible: false, + previewFile: {}, + viewerPlugins, + viewerToolbar: { zoom: true, rotate: true, download: true, fullscreen: true }, + feeGenerationMode: 'system', + settlementRule: { + autoGenerate: 1, + settlementType: '月结', + billCycleType: '固定截单日', + billCutoffDay: 25, + cycleDays: '', + }, + preSettlementConfig: {}, + formalSettlementConfig: {}, + settlementTypeOptions: ['月结', '日结', '周结', '半月结', '固定天数周期结算'], + billCycleTypeOptions: ['固定截单日', '自然月'], + billCutoffDayOptions: Array.from({ length: 31 }, (_, index) => ({ + label: `${index + 1}日`, + value: index + 1, + })), + cycleDayOptions: [7, 15, 30, 60].map(value => ({ label: `${value}天`, value })), + planDialogVisible: false, + planEditor: { planName: '', defaultPlan: false, remark: '', rules: [] }, + planEditorIndex: -1, + billingElements: [ + '按重量', + '按体积', + '按车辆', + '按里程', + '按吨·公里', + '固定金额(整单一口价)', + '按数量', + ], + attachmentFileTypes: [ + 'pdf', + 'bmp', + 'jpeg', + 'png', + 'jpg', + 'doc', + 'docx', + 'ppt', + 'pptx', + 'xlsx', + 'xls', + 'eml', + 'msg', + 'zip', + ], + rules: { + contractName: [{ required: true, message: '请输入合同名称', trigger: 'blur' }], + changeReason: [{ required: true, message: '请输入变更原因', trigger: 'blur' }], + }, + }; + }, + computed: { + showSettlementBillCycleType() { + return this.settlementRule.settlementType === '月结'; + }, + showSettlementBillCutoffDay() { + return this.showSettlementBillCycleType && this.settlementRule.billCycleType === '固定截单日'; + }, + showSettlementCycleDays() { + return this.settlementRule.settlementType === '固定天数周期结算'; + }, + }, + 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, changeType: '合同信息变更' }; 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); }, - parse(value) { try { const result = JSON.parse(value || '[]'); return Array.isArray(result) ? result : []; } catch { return []; } }, - 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 }; } }, - addPlan() { this.planEditorIndex = -1; this.planEditor = { planName: `计费方案${this.plans.length + 1}`, defaultPlan: !this.plans.length, remark: '', rules: [{}] }; this.planDialogVisible = true; }, - editPlan(row, index) { this.planEditorIndex = index; this.planEditor = JSON.parse(JSON.stringify(row)); this.planDialogVisible = true; }, - toggleDefaultPlan(value) { if (value) this.plans.forEach(item => { item.defaultPlan = false; }); }, - savePlan(value, index) { if (index < 0) this.plans.push(value); else this.plans.splice(index, 1, value); if (value.defaultPlan) this.plans.forEach((item, current) => { if (current !== (index < 0 ? this.plans.length - 1 : index)) item.defaultPlan = false; }); }, - handleSettlementTypeChange(value) { if (value !== '月结') { this.settlementRule.billCycleType = ''; this.settlementRule.billCutoffDay = ''; } else if (!this.settlementRule.billCycleType) this.settlementRule.billCycleType = '固定截单日'; if (value !== '固定天数周期结算') this.settlementRule.cycleDays = ''; }, - handleCycleTypeChange(value) { if (value === '固定截单日' && !this.settlementRule.billCutoffDay) this.settlementRule.billCutoffDay = 25; if (value !== '固定截单日') this.settlementRule.billCutoffDay = ''; }, - handleAttachment(event) { const raw = event.raw; if (raw) this.attachments.push({ name: raw.name, size: `${Math.ceil(raw.size / 1024)}KB`, uploadTime: this.$dayjs().format('YYYY-MM-DD HH:mm:ss') }); }, - handleContractFileChange(list) { this.contractFileRows = (list || []).map(item => ({ ...item, uploadTime: item.uploadTime || this.$dayjs().format('YYYY-MM-DD HH:mm:ss') })); }, - attachmentUrl(row = {}) { return row.url || row.link || row.fileUrl || row.downloadUrl || row.domain || ''; }, - attachmentName(row = {}) { return row.originalName || row.name || row.fileName || '附件'; }, - attachmentExtension(row = {}) { const source = String(this.attachmentName(row) || this.attachmentUrl(row)).split('?')[0]; const index = source.lastIndexOf('.'); return index > -1 ? source.slice(index + 1).toLowerCase() : ''; }, - isAttachmentImage(row) { return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(this.attachmentExtension(row)); }, - 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; }, - handlePreviewUnsupported() { this.$message.warning('当前文件暂不支持在线预览'); }, - handlePreviewError() { this.$message.error('附件预览失败'); }, - removeContractFile(index) { this.contractFileRows.splice(index, 1); }, - handleContractFileBatchDownload() { (this.selectedContractFiles.length ? this.selectedContractFiles : this.contractFileRows).forEach(row => { if (row.url) window.open(row.url, '_blank'); }); }, - formatFileSize(value) { const size = Number(value || 0); return size > 1024 * 1024 ? `${(size / 1024 / 1024).toFixed(2)}MB` : `${Math.max(1, Math.ceil(size / 1024))}KB`; }, - handleChangeMaterial(event) { if (event.raw) this.changeMaterials.push(event.raw); }, - addPaymentRatioRow() { this.paymentRatioRows.push({ paymentTerm: `第${this.paymentRatioRows.length + 1}笔`, ratioLimit: '', remark: '' }); }, - async submit() { await this.$refs.formRef.validate(); const total = this.paymentRatioRows.reduce((sum, row) => sum + Number(row.ratioLimit || 0), 0); if (this.paymentRatioRows.length && Math.abs(total - 100) > 0.0001) { this.$message.warning('付款比例上限合计必须等于100%'); return; } const settlementRule = { preSettlementConfig: this.preSettlementConfig, formalSettlementConfig: this.formalSettlementConfig }; await api.submitChange({ ...this.form, startDate: this.period[0], endDate: this.period[1], feeGenerationMode: this.feeGenerationMode, billingEnabled: this.feeGenerationMode === 'system' ? 1 : 0, billingPlanJson: JSON.stringify(this.plans), settlementRuleJson: JSON.stringify(settlementRule), preSettlementConfigJson: JSON.stringify(this.preSettlementConfig), formalSettlementConfigJson: JSON.stringify(this.formalSettlementConfig), paymentRatioJson: JSON.stringify(this.paymentRatioRows), contractFileJson: JSON.stringify(this.contractFileRows), attachmentsJson: JSON.stringify(this.attachments), changeContent: this.form.changeReason, changeReason: this.form.changeReason }); this.$message.success('变更已提交'); this.$router.back(); }, + 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, changeType: '合同信息变更' }; + 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); + }, + parse(value) { + try { + const result = JSON.parse(value || '[]'); + return Array.isArray(result) ? result : []; + } catch { + return []; + } + }, + 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, + }; + } + }, + addPlan() { + this.planEditorIndex = -1; + this.planEditor = { + planName: `计费方案${this.plans.length + 1}`, + defaultPlan: !this.plans.length, + remark: '', + rules: [{}], + }; + this.planDialogVisible = true; + }, + editPlan(row, index) { + this.planEditorIndex = index; + this.planEditor = JSON.parse(JSON.stringify(row)); + this.planDialogVisible = true; + }, + toggleDefaultPlan(value) { + if (value) + this.plans.forEach(item => { + item.defaultPlan = false; + }); + }, + savePlan(value, index) { + if (index < 0) this.plans.push(value); + else this.plans.splice(index, 1, value); + if (value.defaultPlan) + this.plans.forEach((item, current) => { + if (current !== (index < 0 ? this.plans.length - 1 : index)) item.defaultPlan = false; + }); + }, + handleSettlementTypeChange(value) { + if (value !== '月结') { + this.settlementRule.billCycleType = ''; + this.settlementRule.billCutoffDay = ''; + } else if (!this.settlementRule.billCycleType) + this.settlementRule.billCycleType = '固定截单日'; + if (value !== '固定天数周期结算') this.settlementRule.cycleDays = ''; + }, + handleCycleTypeChange(value) { + if (value === '固定截单日' && !this.settlementRule.billCutoffDay) + this.settlementRule.billCutoffDay = 25; + if (value !== '固定截单日') this.settlementRule.billCutoffDay = ''; + }, + handleAttachment(event) { + const raw = event.raw; + if (raw) + this.attachments.push({ + name: raw.name, + size: `${Math.ceil(raw.size / 1024)}KB`, + uploadTime: this.$dayjs().format('YYYY-MM-DD HH:mm:ss'), + }); + }, + handleContractFileChange(list) { + this.contractFileRows = (list || []).map(item => ({ + ...item, + uploadTime: item.uploadTime || this.$dayjs().format('YYYY-MM-DD HH:mm:ss'), + })); + }, + attachmentUrl(row = {}) { + return row.url || row.link || row.fileUrl || row.downloadUrl || row.domain || ''; + }, + attachmentName(row = {}) { + return row.originalName || row.name || row.fileName || '附件'; + }, + attachmentExtension(row = {}) { + const source = String(this.attachmentName(row) || this.attachmentUrl(row)).split('?')[0]; + const index = source.lastIndexOf('.'); + return index > -1 ? source.slice(index + 1).toLowerCase() : ''; + }, + isAttachmentImage(row) { + return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(this.attachmentExtension(row)); + }, + 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; + }, + handlePreviewUnsupported() { + this.$message.warning('当前文件暂不支持在线预览'); + }, + handlePreviewError() { + this.$message.error('附件预览失败'); + }, + removeContractFile(index) { + this.contractFileRows.splice(index, 1); + }, + handleContractFileBatchDownload() { + (this.selectedContractFiles.length + ? this.selectedContractFiles + : this.contractFileRows + ).forEach(row => { + if (row.url) window.open(row.url, '_blank'); + }); + }, + formatFileSize(value) { + const size = Number(value || 0); + return size > 1024 * 1024 + ? `${(size / 1024 / 1024).toFixed(2)}MB` + : `${Math.max(1, Math.ceil(size / 1024))}KB`; + }, + handleChangeMaterial(event) { + if (event.raw) this.changeMaterials.push(event.raw); + }, + addPaymentRatioRow() { + this.paymentRatioRows.push({ + paymentTerm: `第${this.paymentRatioRows.length + 1}笔`, + ratioLimit: '', + remark: '', + }); + }, + ratioInput(row, value) { + let nextValue = String(value ?? '').replace(/[^\d.]/g, ''); + const parts = nextValue.split('.'); + if (parts.length > 2) nextValue = `${parts.shift()}.${parts.join('')}`; + const [integerPart, decimalPart] = nextValue.split('.'); + nextValue = + decimalPart === undefined ? integerPart : `${integerPart}.${decimalPart.slice(0, 2)}`; + if (Number(nextValue) > 100) nextValue = '100'; + row.ratioLimit = nextValue; + }, + async submit() { + await this.$refs.formRef.validate(); + const total = this.paymentRatioRows.reduce( + (sum, row) => sum + Number(row.ratioLimit || 0), + 0 + ); + if (this.paymentRatioRows.length && Math.abs(total - 100) > 0.0001) { + this.$message.warning('付款比例上限合计必须等于100%'); + return; + } + const settlementRule = { + preSettlementConfig: this.preSettlementConfig, + formalSettlementConfig: this.formalSettlementConfig, + }; + await api.submitChange({ + ...this.form, + startDate: this.period[0], + endDate: this.period[1], + feeGenerationMode: this.feeGenerationMode, + billingEnabled: this.feeGenerationMode === 'system' ? 1 : 0, + billingPlanJson: JSON.stringify(this.plans), + settlementRuleJson: JSON.stringify(settlementRule), + preSettlementConfigJson: JSON.stringify(this.preSettlementConfig), + formalSettlementConfigJson: JSON.stringify(this.formalSettlementConfig), + paymentRatioJson: JSON.stringify(this.paymentRatioRows), + contractFileJson: JSON.stringify(this.contractFileRows), + attachmentsJson: JSON.stringify(this.attachments), + changeContent: this.form.changeReason, + changeReason: this.form.changeReason, + }); + this.$message.success('变更已提交'); + this.$router.back(); + }, }, }; diff --git a/src/views/business/contract-manage.vue b/src/views/business/contract-manage.vue index 2b998d9..8e3b303 100644 --- a/src/views/business/contract-manage.vue +++ b/src/views/business/contract-manage.vue @@ -177,7 +177,7 @@ filterable clearable :loading="contractPartyLoading" - :disabled="!form.projectId" + @visible-change="visible => visible && loadContractPartyOptions()" > - - + + - - - + + -