From f78e61b43394cd3b9c4778b4c564c9808d25733a Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Wed, 9 Sep 2026 14:43:38 +0800 Subject: [PATCH] =?UTF-8?q?1=E3=80=81=E8=B0=83=E6=95=B4=E5=AF=BC=E5=85=A5?= =?UTF-8?q?=E8=BF=90=E5=8D=95=202=E3=80=81=E8=B0=83=E6=95=B4=E5=AF=B9?= =?UTF-8?q?=E8=B4=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/option/business/contract-manage.js | 8 +- src/option/business/temporary-credit-limit.js | 8 +- src/option/business/transport-plan.js | 10 +- .../components/shipping-template-page.vue | 233 ++++++++++++++++-- .../components/transport-plan-page.vue | 132 +++++++--- .../components/pre-settlement-editor.vue | 109 ++++++++ .../transport-reconciliation-editor.vue | 208 +++++++++++++++- 7 files changed, 639 insertions(+), 69 deletions(-) diff --git a/src/option/business/contract-manage.js b/src/option/business/contract-manage.js index b416f07..06342c0 100644 --- a/src/option/business/contract-manage.js +++ b/src/option/business/contract-manage.js @@ -27,6 +27,12 @@ const listDicFormatter = res => { return []; }; +const projectNameDicFormatter = res => { + const list = listDicFormatter(res); + // 过滤掉 projectName 为空的记录 + return list.filter(item => item && item.projectName && String(item.projectName).trim()); +}; + export const config = { title: '合同管理', permission: 'contract_manage', @@ -266,7 +272,7 @@ export const option = createCrudOption([ order: 165, dicUrl: '/blade-transport/project-apply/list?current=1&size=9999&approvalStatuses=approved,change_approved', - dicFormatter: listDicFormatter, + dicFormatter: projectNameDicFormatter, props: { label: 'projectName', value: 'projectName', diff --git a/src/option/business/temporary-credit-limit.js b/src/option/business/temporary-credit-limit.js index 4e8befd..6981a71 100644 --- a/src/option/business/temporary-credit-limit.js +++ b/src/option/business/temporary-credit-limit.js @@ -15,6 +15,12 @@ const listDicFormatter = res => { return []; }; +const projectNameDicFormatter = res => { + const list = listDicFormatter(res); + // 过滤掉 projectName 为空的记录 + return list.filter(item => item && item.projectName && String(item.projectName).trim()); +}; + const userDicFormatter = res => listDicFormatter(res).map(user => ({ ...user, @@ -137,7 +143,7 @@ export const option = { searchType: 'select', formslot: true, dicUrl: '/blade-transport/project-apply/list?current=1&size=9999', - dicFormatter: listDicFormatter, + dicFormatter: projectNameDicFormatter, props: { label: 'projectName', value: 'projectName', diff --git a/src/option/business/transport-plan.js b/src/option/business/transport-plan.js index d15c736..45b9154 100644 --- a/src/option/business/transport-plan.js +++ b/src/option/business/transport-plan.js @@ -23,6 +23,12 @@ const listDicFormatter = res => { return []; }; +const projectNameDicFormatter = res => { + const list = listDicFormatter(res); + // 过滤掉 projectName 为空的记录 + return list.filter(item => item && item.projectName && String(item.projectName).trim()); +}; + const parseGoodsRows = value => { if (!value) return []; if (Array.isArray(value)) return value; @@ -209,7 +215,7 @@ export const config = { statusProp: 'businessStatus', statusTextProp: 'businessStatusName', deleteStatus: ['draft', 'waiting_dispatch'], - editStatus: ['draft', 'waiting_dispatch'], + editStatus: ['draft', 'waiting_dispatch', 'dispatching'], detailButton: true, detailSections: [ { @@ -364,7 +370,7 @@ export const option = { span: 6, order: 400, dicUrl: '/blade-transport/project-apply/list?current=1&size=9999', - dicFormatter: listDicFormatter, + dicFormatter: projectNameDicFormatter, props: { label: 'projectName', value: 'projectName', diff --git a/src/views/business/components/shipping-template-page.vue b/src/views/business/components/shipping-template-page.vue index 5d84d89..7fa8619 100644 --- a/src/views/business/components/shipping-template-page.vue +++ b/src/views/business/components/shipping-template-page.vue @@ -1257,6 +1257,7 @@ export default { dialogReadonly: false, crudDialogType: '', standaloneFormKey: '', + copyingRow: null, detailBox: false, detailLoading: false, detailRow: {}, @@ -1427,7 +1428,7 @@ export default { String(item.value).toUpperCase() === String(this.shippingTemplateFreight.currency || '').toUpperCase() ); - return this.currencyName(option?.label || this.shippingTemplateFreight.currency); + return this.currencyUnit(option?.label || this.shippingTemplateFreight.currency); }, roadFreightTotal() { const total = this.shippingTemplateFreight.freightItems.reduce( @@ -1498,7 +1499,8 @@ export default { if ( this.isStandaloneFormPage && (to.query.mode !== from?.query?.mode || - String(to.query.id || '') !== String(from?.query.id || '')) + String(to.query.id || '') !== String(from?.query.id || '') || + to.query.copy !== from?.query?.copy) ) this.$nextTick(() => this.initStandaloneFormPage()); }, @@ -1608,13 +1610,21 @@ export default { }, openBusinessForm(mode, row = {}) { if (this.isStandaloneBusinessPage) { + const query = { + mode, + ...(mode === 'edit' && row.id ? { id: row.id } : {}), + name: `${mode === 'edit' ? '编辑' : '新增'}${this.config.title}`, + }; + // 如果是复制模式,添加标识 + console.log('openBusinessForm, mode:', mode, 'copyingRow:', this.copyingRow); + if (mode === 'add' && this.copyingRow) { + query.copy = Date.now().toString(); + console.log('添加 copy 参数:', query.copy); + } + console.log('最终的 query:', query); this.$router.push({ path: '/business/shipping-template/form', - query: { - mode, - ...(mode === 'edit' && row.id ? { id: row.id } : {}), - name: `${mode === 'edit' ? '编辑' : '新增'}${this.config.title}`, - }, + query, }); return; } @@ -1624,10 +1634,29 @@ export default { if (!this.isStandaloneFormPage) return; const mode = this.$route.query.mode; const id = this.$route.query.id || ''; - const key = `${mode}:${id}`; + const copy = this.$route.query.copy || ''; + const key = `${mode}:${id}:${copy}`; + console.log('initStandaloneFormPage 被调用, key:', key, 'standaloneFormKey:', this.standaloneFormKey); if (this.standaloneFormKey === key) return; this.standaloneFormKey = key; + + // 检查是否是复制模式 + if (mode === 'add' && copy) { + const copyData = sessionStorage.getItem('shipping-template-copy-data'); + console.log('复制模式检测到,从 sessionStorage 读取数据:', copyData); + if (copyData) { + try { + this.copyingRow = JSON.parse(copyData); + console.log('解析后的 copyingRow:', this.copyingRow); + sessionStorage.removeItem('shipping-template-copy-data'); + } catch (e) { + console.error('解析复制数据失败', e); + } + } + } + if (mode === 'edit') this.form = { id }; + console.log('调用 beforeOpen 前,copyingRow:', this.copyingRow); this.beforeOpen(() => {}, mode); }, parseJsonArray(value) { @@ -1752,9 +1781,37 @@ export default { .catch(() => loading?.()); }, beforeOpen(done, type) { + console.log('beforeOpen 被调用,type:', type, 'copyingRow:', this.copyingRow); this.crudDialogType = type; this.dialogReadonly = type === 'view'; if (type === 'add') { + // 检查是否是复制模式 + if (this.copyingRow) { + console.log('进入复制模式分支'); + const copyData = { ...this.copyingRow }; + this.copyingRow = null; + this.form = { ...(this.config.defaultForm || {}), ...copyData }; + console.log('合并后的 form:', this.form); + // 删除模板编号,等待系统生成 + delete this.form.templateCode; + this.applyFormDetail(this.form); + console.log('applyFormDetail 调用完成'); + // 生成新的模板编号 + if (typeof this.api.nextCode === 'function') { + this.api + .nextCode() + .then(res => { + this.form.templateCode = res?.data?.data || ''; + console.log('生成的新模板编号:', this.form.templateCode); + }) + .finally(() => done?.()); + } else { + done?.(); + } + return; + } + console.log('进入正常新增模式分支'); + // 正常新增模式 this.form = { ...(this.config.defaultForm || {}) }; this.selectedProjectId = ''; this.contractOptions = []; @@ -1791,9 +1848,12 @@ export default { this.normalizeCargo(item) ); if (!this.transportCargoRows.length) this.transportCargoRows = [defaultCargo()]; - this.restoreCargoTypePaths(); - // 编辑时先加载货物类型树,保证已有货物类型可以回显为级联路径。 - this.ensureCargoTypeOptions().catch(() => {}); + // 先加载货物类型树,保证已有货物类型可以回显为级联路径,然后再恢复路径。 + this.ensureCargoTypeOptions() + .then(() => { + this.restoreCargoTypePaths(); + }) + .catch(() => {}); this.attachmentRows = this.parseJsonArray(this.form.attachmentsJson); this.shippingTemplateFreight = { ...defaultFreight(), @@ -1937,11 +1997,42 @@ export default { }); }, handleCopy(row) { - if (typeof this.api.copy !== 'function') return; - this.api.copy(row.id).then(() => { - this.$message.success('复制成功'); - this.onLoad(this.page, this.query); - }); + this.api + .getDetail(row.id) + .then(res => { + const detail = res?.data?.data || {}; + console.log('获取到的详情数据:', detail); + // 删除不需要复制的字段 + delete detail.id; + delete detail.templateCode; // 系统自动生成 + delete detail.createTime; + delete detail.createUser; + delete detail.createUserName; + delete detail.createDept; + delete detail.updateTime; + delete detail.updateUser; + delete detail.updateUserName; + delete detail.status; + + console.log('处理后的数据:', detail); + console.log('isStandaloneBusinessPage:', this.isStandaloneBusinessPage); + + // 存储到组件状态(用于判断是否是复制模式) + this.copyingRow = detail; + console.log('已存储到 copyingRow'); + + // 如果是独立表单页模式,同时存储到 sessionStorage + if (this.isStandaloneBusinessPage) { + sessionStorage.setItem('shipping-template-copy-data', JSON.stringify(detail)); + console.log('已存储到 sessionStorage'); + } + + // 打开新增表单 + this.openBusinessForm('add', {}); + }) + .catch(() => { + this.$message.error('获取数据失败'); + }); }, handleCustomOperation(operation, row) { if (operation.action === 'createFromTemplate') { @@ -2100,6 +2191,45 @@ export default { }[text.toUpperCase()] || text ); }, + currencyUnit(value) { + const text = String(value || ''); + // 如果格式是 "CNY - 人民币",提取货币代码部分 + let code = text; + if (text.includes(' - ')) { + code = text.split(' - ')[0].trim(); + } + + const upperCode = code.toUpperCase(); + const unitMap = { + CNY: '元', + RMB: '元', + USD: '美元', + EUR: '欧元', + JPY: '日元', + GBP: '英镑', + }; + + // 如果是已知的货币代码,返回对应的单位 + if (unitMap[upperCode]) { + return unitMap[upperCode]; + } + + // 如果是中文货币名称,转换为单位 + const nameMap = { + '人民币': '元', + '美元': '美元', + '欧元': '欧元', + '日元': '日元', + '英镑': '英镑', + }; + + if (nameMap[code]) { + return nameMap[code]; + } + + // 默认返回原值 + return text; + }, loadFeeUnitOptions() { getDictionary({ code: 'unit_fee' }).then(res => { this.feeUnitOptions = this.normalizeDictOptions(res?.data?.data || []); @@ -2644,7 +2774,20 @@ export default { const pathMatch = this.cargoTypeFlatOptions.find(item => item.path?.join('/') === pathKey); if (pathMatch) return pathMatch; } - const codeCandidates = [row.secondCargoTypeCode, row.cargoTypeCode, row.firstCargoTypeCode] + + // 优先通过二级类型编码查找(最准确) + if (row.secondCargoTypeCode) { + const secondMatch = this.cargoTypeFlatOptions.find( + item => String(item.cargoCode || item.code || '').trim() === String(row.secondCargoTypeCode).trim() + ); + if (secondMatch) { + console.log('通过 secondCargoTypeCode 找到货物类型:', secondMatch); + return secondMatch; + } + } + + // 其他候选编码 + const codeCandidates = [row.cargoTypeCode, row.firstCargoTypeCode] .map(value => String(value || '').trim()) .filter(Boolean); const nameCandidates = [ @@ -2655,6 +2798,7 @@ export default { ] .map(value => String(value || '').trim()) .filter(Boolean); + return ( this.cargoTypeFlatOptions.find(item => codeCandidates.some( @@ -2671,13 +2815,19 @@ export default { return matched?.path ? this.cargoPath(matched.path) : []; }, restoreCargoTypePaths() { + console.log('restoreCargoTypePaths 被调用'); + console.log('transportCargoRows:', this.transportCargoRows); + console.log('cargoTypeFlatOptions 长度:', this.cargoTypeFlatOptions.length); if (!this.transportCargoRows.length || !this.cargoTypeFlatOptions.length) return; - this.transportCargoRows.forEach(row => { + this.transportCargoRows.forEach((row, index) => { + console.log(`处理第 ${index + 1} 行货物:`, row); const path = this.resolveCargoTypePath(row); + console.log(`解析出的 path:`, path); if (!path.length) return; const item = this.cargoTypeFlatOptions.find( option => option.path?.join('/') === path.join('/') ); + console.log(`匹配到的 item:`, item); row.cargoTypePath = path; row.cargoType = item?.cargoName || @@ -2691,12 +2841,22 @@ export default { row.secondCargoTypeCode || row.firstCargoTypeCode || ''; + console.log(`设置后的 row.cargoTypePath:`, row.cargoTypePath); }); }, normalizeCargo(row = {}) { - const cargoTypePath = this.resolveCargoTypePath(row); + console.log('normalizeCargo 输入:', row); + // 如果原始数据中有完整的 cargoTypePath 数组,直接使用 + let cargoTypePath = []; + if (Array.isArray(row.cargoTypePath) && row.cargoTypePath.length > 0) { + cargoTypePath = this.cargoPath(row.cargoTypePath); + console.log('使用原始的 cargoTypePath:', cargoTypePath); + } else { + cargoTypePath = this.resolveCargoTypePath(row); + console.log('解析的 cargoTypePath:', cargoTypePath); + } const cargoType = this.findCargoTypeOption({ ...row, cargoTypePath }); - return { + const result = { ...defaultCargo(), ...row, cargoTypePath, @@ -2714,6 +2874,8 @@ export default { '', cargoName: row.cargoName || row.goodsName || '', }; + console.log('normalizeCargo 输出:', result); + return result; }, handleCargoTypeChange(row, value) { const path = this.cargoPath(value); @@ -2740,26 +2902,47 @@ export default { .catch(() => callback([])); }, async selectCargoSuggestion(row, item = {}) { - const cargoTypeOption = this.findCargoTypeOption(item); + console.log('selectCargoSuggestion 被调用,item:', item); + const selectedCargo = { ...row, ...item, cargoName: item.cargoName || item.name || item.label || item.value || row.cargoName || '', }; + + // 确保货物类型树已加载 try { await this.ensureCargoTypeOptions(); + console.log('货物类型树已加载,共', this.cargoTypeFlatOptions.length, '个选项'); } catch (error) { + console.warn('货物类型树加载失败', error); // 货物类型树加载失败时仍保留货物名称,提交时会给出货物类型必填提示。 } - // 货物名称返回了类型信息时,以货物自身的类型覆盖当前行原有类型。 - // 这样从一个类型切换到另一个类型时,级联选择器不会继续显示旧值。 - const matchedCargoType = this.findCargoTypeOption(item) || cargoTypeOption; + + // 从常用货物返回的数据中查找货物类型 + const matchedCargoType = this.findCargoTypeOption(item); + console.log('匹配到的货物类型:', matchedCargoType); + if (matchedCargoType?.path?.length) { - selectedCargo.cargoTypePath = this.cargoPath(matchedCargoType.path); + // 找到匹配的货物类型,回填到当前行 + const fullPath = matchedCargoType.path; + selectedCargo.cargoTypePath = this.cargoPath(fullPath); selectedCargo.cargoType = matchedCargoType.cargoName || selectedCargo.cargoType; selectedCargo.cargoTypeCode = matchedCargoType.cargoCode || matchedCargoType.code || matchedCargoType.id || ''; + + console.log('货物名称选择后反选货物类型:', { + cargoName: selectedCargo.cargoName, + cargoType: selectedCargo.cargoType, + cargoTypePath: selectedCargo.cargoTypePath, + fullPath: fullPath, + matchedCargoType: matchedCargoType, + }); + } else { + console.warn('未找到匹配的货物类型,item数据:', item); + console.warn('当前 cargoTypeFlatOptions 前5项:', this.cargoTypeFlatOptions.slice(0, 5)); } + Object.assign(row, this.normalizeCargo(selectedCargo)); row.value = undefined; this.syncFreightItems(); diff --git a/src/views/business/components/transport-plan-page.vue b/src/views/business/components/transport-plan-page.vue index 7d1634d..d069039 100644 --- a/src/views/business/components/transport-plan-page.vue +++ b/src/views/business/components/transport-plan-page.vue @@ -316,6 +316,25 @@ empty-text="暂无货物信息" > + + + 货物类型 + + + visible && ensureBillingCargoTypeOptions()" + @change="value => handleTransportCargoTypeChange(row, value)" + /> + + {{ formatBillingCargoTypeLabel(item) }} - {{ item.cargoCode }} - - - 货物类型 - - - visible && ensureBillingCargoTypeOptions()" - @change="value => handleTransportCargoTypeChange(row, value)" - /> - - 联系人 - + 联系方式 - + @@ -1520,10 +1521,12 @@ v-model="dispatchItemForm.arrivalName" :placeholder="dispatchStationNamePlaceholder" :suffix-icon="dispatchStationMode ? Search : undefined" + disabled /> 联系人 - + 联系方式 - + @@ -4849,10 +4852,7 @@ export default { fetchTaskCargoSuggestions(queryString, callback, path = []) { const keyword = String(queryString || '').trim(); const normalizedPath = this.normalizeBillingCargoTypePath(path); - if (!keyword && !normalizedPath.length) { - callback([]); - return; - } + // 移除空查询时返回空数组的限制,允许点击时显示货物列表 let loadingKey = ''; this.ensureBillingCargoTypeOptions() .then(() => { @@ -4884,11 +4884,42 @@ export default { if (loadingKey) this.setTaskCargoLoadingByKey(loadingKey, false); }); }, - handleTaskCargoRowSelect(row, item = {}) { + async handleTaskCargoRowSelect(row, item = {}) { const value = this.formatBillingCargoTypeLabel(item); row.cargoName = value; row.specification = item.specification || item.spec || row.specification || ''; row.model = item.model || item.modelName || row.model || ''; + + // 确保货物类型树已加载 + try { + await this.ensureBillingCargoTypeOptions(); + } catch (error) { + console.warn('货物类型树加载失败', error); + this.syncTransportCargoJson(); + return; + } + + // 从常用货物返回的数据中查找货物类型并反选 + const matchedCargoType = this.findBillingCargoTypeOption(item); + + if (matchedCargoType?.path?.length) { + // 找到匹配的货物类型,回填到当前行 + const fullPath = this.normalizeBillingCargoTypePath(matchedCargoType.path); + row.cargoTypePath = fullPath; + row.cargoType = matchedCargoType.cargoName || row.cargoType || ''; + row.cargoTypeCode = + matchedCargoType.cargoCode || matchedCargoType.code || matchedCargoType.id || ''; + + console.log('货物名称选择后反选货物类型:', { + cargoName: row.cargoName, + cargoType: row.cargoType, + cargoTypePath: row.cargoTypePath, + matchedCargoType: matchedCargoType, + }); + } else { + console.warn('未找到匹配的货物类型,item数据:', item); + } + this.syncTransportCargoJson(); }, loadTaskQuantityUnitOptions() { @@ -6179,6 +6210,49 @@ export default { null ); }, + findBillingCargoTypeOption(row = {}) { + const path = this.normalizeBillingCargoTypePath(row.cargoTypePath); + if (path.length) { + const pathKey = path.join('/'); + const pathMatch = this.billingCargoTypeFlatOptions.find(item => item.path?.join('/') === pathKey); + if (pathMatch) return pathMatch; + } + + // 优先通过二级类型编码查找(最准确) + if (row.secondCargoTypeCode) { + const secondMatch = this.billingCargoTypeFlatOptions.find( + item => String(item.cargoCode || item.code || '').trim() === String(row.secondCargoTypeCode).trim() + ); + if (secondMatch) { + console.log('通过 secondCargoTypeCode 找到货物类型:', secondMatch); + return secondMatch; + } + } + + // 其他候选编码 + const codeCandidates = [row.cargoTypeCode, row.firstCargoTypeCode] + .map(value => String(value || '').trim()) + .filter(Boolean); + const nameCandidates = [ + row.secondCargoTypeName, + row.cargoType, + row.goodsType, + row.firstCargoTypeName, + ] + .map(value => String(value || '').trim()) + .filter(Boolean); + + return ( + this.billingCargoTypeFlatOptions.find(item => + codeCandidates.some( + code => code === String(item.cargoCode || item.code || item.id || '').trim() + ) + ) || + this.billingCargoTypeFlatOptions.find(item => + nameCandidates.some(name => name === String(item.cargoName || '').trim()) + ) + ); + }, resolveCommonCargoTypePath(row = {}) { const path = this.normalizeBillingCargoTypePath(row.cargoTypePath); if (path.length) return path; diff --git a/src/views/settlement/components/pre-settlement-editor.vue b/src/views/settlement/components/pre-settlement-editor.vue index 9aabde1..838419d 100644 --- a/src/views/settlement/components/pre-settlement-editor.vue +++ b/src/views/settlement/components/pre-settlement-editor.vue @@ -247,6 +247,9 @@ > {{ row[column.prop] }} + + {{ formatUnitPrice(row) }} + {{ formatMoney(row[column.prop], row.currency || form.currency) }} @@ -259,6 +262,12 @@ {{ transportTypeName(row[column.prop]) }} + + {{ formatCargoNames(row) }} + + + {{ formatCargoTypes(row) }} + {{ displayValue(row[column.prop]) }} @@ -1078,6 +1087,7 @@ export default { adjustRows: [], pendingAdjustments: {}, sourceFeeSnapshots: {}, + detailFeeCache: {}, // 缓存每个明细的费用明细数据(用于拼接货物名称和类型) adjustChangeRecordVisible: false, adjustChangeRecord: null, adjustChangeRecordDetailRows: [], @@ -1169,6 +1179,19 @@ export default { }); return Array.from(names); }, + // 按 preSettlementDetailId 分组的明细数据(用于拼接货物名称和类型) + groupedDetails() { + const groups = {}; + this.details.forEach(detail => { + // 优先使用 sourceDetailId,如果没有则使用 preSettlementDetailId + const groupKey = detail.sourceDetailId || detail.preSettlementDetailId || detail.id; + if (!groups[groupKey]) { + groups[groupKey] = []; + } + groups[groupKey].push(detail); + }); + return groups; + }, filteredDetails() { return this.details.filter(row => Object.entries(this.appliedDetailQuery).every(([field, keyword]) => { @@ -1322,6 +1345,7 @@ export default { this.adjustRows = []; this.pendingAdjustments = {}; this.sourceFeeSnapshots = {}; + this.detailFeeCache = {}; this.adjustDialog.activeTab = 'adjust'; this.adjustDialog.detailId = ''; this.adjustDialog.sourceDetailId = ''; @@ -1371,6 +1395,8 @@ export default { partyB: detail.payeeName, }); } + // 预加载所有明细的费用明细数据,用于拼接货物名称和类型 + await this.loadAllDetailFees(); } finally { this.loading = false; } @@ -1917,6 +1943,30 @@ export default { this.normalizeAdjustRow(item, data.feeItemNames || []) ); }, + // 批量加载所有明细的费用明细数据,用于拼接货物名称和类型 + async loadAllDetailFees() { + if (!this.details.length) return; + + // 并发加载所有明细的费用数据 + await Promise.all( + this.details.map(async detail => { + if (!detail.id) return; + + try { + const response = await getDetailFees(detail.id); + const feeRows = response.data?.data || response.data || []; + + // 缓存费用明细数据 + if (Array.isArray(feeRows) && feeRows.length > 0) { + this.detailFeeCache[detail.id] = feeRows; + } + } catch (error) { + // 加载失败时使用明细本身的数据 + console.warn(`加载明细 ${detail.id} 的费用数据失败:`, error); + } + }) + ); + }, // 明细列表只有汇总金额,费用项级别的原始金额需要按来源明细单独拉取并缓存为调整基准。 async ensureSourceFeeSnapshots() { const targets = this.details.filter(row => { @@ -2461,6 +2511,65 @@ export default { if (!Number.isFinite(quantity)) return '-'; return `${quantity.toFixed(2)}${row.quantityUnit ? ` ${row.quantityUnit}` : ''}`; }, + // 拼接同一运单的货物名称 + formatCargoNames(row) { + const detailId = row.id; + const feeRows = this.detailFeeCache[detailId] || []; + + // 如果没有缓存数据或只有一条,返回原始值 + if (feeRows.length <= 1) { + return this.displayValue(row.cargoName); + } + + // 收集所有货物名称并去重 + const names = feeRows + .map(item => item.cargoName) + .filter(name => name && String(name).trim()) + .filter((name, index, arr) => arr.indexOf(name) === index); + + return names.length > 0 ? names.join('、') : this.displayValue(row.cargoName); + }, + // 拼接同一运单的货物类型 + formatCargoTypes(row) { + const detailId = row.id; + const feeRows = this.detailFeeCache[detailId] || []; + + // 如果没有缓存数据或只有一条,返回原始值 + if (feeRows.length <= 1) { + return this.displayValue(row.cargoType); + } + + // 收集所有货物类型并去重 + const types = feeRows + .map(item => item.cargoType) + .filter(type => type && String(type).trim()) + .filter((type, index, arr) => arr.indexOf(type) === index); + + return types.length > 0 ? types.join('、') : this.displayValue(row.cargoType); + }, + // 格式化运输单价,如果多个货物的单价不一样则显示'-' + formatUnitPrice(row) { + const detailId = row.id; + const feeRows = this.detailFeeCache[detailId] || []; + + // 如果没有缓存数据或只有一条,返回原始值 + if (feeRows.length <= 1) { + return this.formatMoney(row.unitPrice, row.currency || this.form.currency); + } + + // 检查所有货物的运输单价是否一致 + const prices = feeRows + .map(item => Number(item.unitPrice || 0)) + .filter((price, index, arr) => arr.indexOf(price) === index); + + // 如果单价不一致,返回'-' + if (prices.length > 1) { + return '-'; + } + + // 单价一致,返回该单价 + return this.formatMoney(prices[0], row.currency || this.form.currency); + }, displayValue(value) { return value === undefined || value === null || value === '' ? '-' : value; }, diff --git a/src/views/settlement/components/transport-reconciliation-editor.vue b/src/views/settlement/components/transport-reconciliation-editor.vue index 3cec2c4..c70cec1 100644 --- a/src/views/settlement/components/transport-reconciliation-editor.vue +++ b/src/views/settlement/components/transport-reconciliation-editor.vue @@ -523,23 +523,62 @@ 补款 扣款 - + - + + + {{ row.name }} + + {{ formatCompletionMoney(row.originalAmount) }} - {{ + + + {{ formatCompletionMoney(row.adjustAmount) - }} + }} + - {{ formatCompletionMoney(row.afterAmount) }} + + {{ + formatCompletionMoney(row.afterAmount) + }} + + + + + + {{ displayValue(row.remark) }} + + + + + 删除 + - @@ -565,6 +604,7 @@ import * as api from '@/api/settlement/transportReconciliation'; import * as formalSettlementApi from '@/api/settlement/formalSettlement'; import { getDictionary } from '@/api/system/dictbiz'; import { Search } from '@element-plus/icons-vue'; +import { h } from 'vue'; import { downloadXls } from '@/utils/util'; import { transportReconciliationFormFields } from '@/option/settlement/transportReconciliationForm'; import { @@ -610,6 +650,7 @@ export default { adjustDialog: { visible: false, saving: false, rows: [], reason: '' }, manualMatchDialog: { visible: false, loading: false, row: null }, completionDialog: { visible: false, loading: false }, + completionSummaryRowsData: null, externalEditing: {}, externalEditSnapshots: {}, matchingStarted: false, @@ -728,9 +769,17 @@ export default { return [...names]; }, duplicateRows() { - return this.externalDetails.filter( - row => row.suspectedDuplicate || row.matchStatus === 'suspected_duplicate' + const duplicateKeys = new Set(); + const duplicateRows = []; + this.externalDetails.forEach(row => { + const key = this.externalDuplicateKey(row); + if (duplicateKeys.has(key)) duplicateRows.push(row); + else duplicateKeys.add(key); + }); + const duplicateKeySet = new Set( + duplicateRows.map(row => this.externalDuplicateKey(row)) ); + return this.externalDetails.filter(row => duplicateKeySet.has(this.externalDuplicateKey(row))); }, canStartMatch() { return Boolean(this.form.formalSettlementId && this.externalDetails.length); @@ -767,6 +816,11 @@ export default { return this.form.reconciliationModeName || labels[this.form.reconciliationMode] || '-'; }, completionSummaryRows() { + if (Array.isArray(this.completionSummaryRowsData)) return this.completionSummaryRowsData; + return this.buildCompletionSummaryRows; + }, + + buildCompletionSummaryRows() { const source = this.form.feeSummary || this.form.feeSummaries || @@ -796,10 +850,14 @@ export default { : originalAmount; return { name, + feeType: '', + feeItem: name, originalAmount, adjustAmount: afterAmount - originalAmount, afterAmount, remark: '', + manualFlag: 0, + _feeKind: '', _index: index, }; }); @@ -851,6 +909,7 @@ export default { this.externalDetails = []; this.externalEditing = {}; this.externalEditSnapshots = {}; + this.completionSummaryRowsData = null; this.matchingStarted = false; this.externalTab = 'all'; this.internalQuery = { documentNo: '', vehicleNo: '', batchNo: '', cargoName: '' }; @@ -880,6 +939,7 @@ export default { : data.payeeName) || '', }; + this.completionSummaryRowsData = null; const internalRows = data.internalDetails || data.internalBillDetails || data.internals || []; this.internalDetails = internalRows.map(row => this.normalizeInternalRow(row)); const externalRows = data.externalDetails || data.externalBillDetails || data.externals || []; @@ -908,6 +968,52 @@ export default { return {}; } }, + externalDuplicateKey(row) { + const feeItems = row.feeItems || this.parseFeeItems(row.feeItemsJson); + const normalized = value => { + if (value === null || value === undefined || value === '') return ''; + if (typeof value === 'number') return Number(value).toString(); + if (typeof value === 'object') { + return JSON.stringify( + Object.keys(value) + .sort() + .reduce((result, key) => { + result[key] = normalized(value[key]); + return result; + }, {}) + ); + } + const text = String(value).trim().replace(/\s+/g, '').toLowerCase(); + return /^-?\d+(\.\d+)?$/.test(text) ? Number(text).toString() : text; + }; + const normalizeDate = value => { + if (!value) return ''; + const date = this.$dayjs(value); + return date.isValid() ? date.format('YYYY-MM-DDTHH:mm:ss') : value; + }; + return [ + row.vehicleNo, + row.departureAddress, + row.arrivalAddress, + normalizeDate(row.actualDepartureTime), + normalizeDate(row.actualCompletionTime), + row.transportType, + row.cargoName, + row.cargoType, + row.specification, + row.model, + row.transportQuantity, + row.quantityUnit, + row.mileage, + row.batchNo, + row.unitPrice, + row.freightAmount, + feeItems, + row.settlementAmount, + ] + .map(normalized) + .join('|'); + }, normalizeInternalRow(row) { const feeItems = row.feeItems && typeof row.feeItems === 'object' && Object.keys(row.feeItems).length @@ -1027,6 +1133,7 @@ export default { if (formalSettlementChanged) { this.externalDetails = []; this.matchingStarted = false; + this.completionSummaryRowsData = null; } await this.loadFormalInternalPreview(selected.id); this.formalDialog.visible = false; @@ -1174,6 +1281,7 @@ export default { this.externalDetails = (data.externalDetails || []).map((row, index) => this.normalizeExternalRow(row, index) ); + this.completionSummaryRowsData = null; this.matchingStarted = true; this.$message.success('匹配完成'); } finally { @@ -1206,10 +1314,22 @@ export default { ) { return this.$message.warning('差异单数、差异货量和差异金额必须全部为0才可完成对账'); } + this.completionSummaryRowsData = this.buildCompletionSummaryRows.map(row => ({ ...row })); this.completionDialog.visible = true; }, async confirmCompletion() { if (this.completionDialog.loading) return; + for (const row of this.completionSummaryRows) { + const adjustAmount = Number(row.adjustAmount || 0); + if (row.manualFlag === 1 && row._feeKind === '补款' && adjustAmount <= 0) { + return this.$message.warning('补款调整金额必须大于0'); + } + if (row.manualFlag === 1 && row._feeKind === '扣款' && adjustAmount >= 0) { + return this.$message.warning('扣款调整金额必须小于0'); + } + if (String(row.remark || '').length > 200) return this.$message.warning('备注不能超过200个字'); + this.recalculateCompletionFee(row); + } this.completionDialog.loading = true; try { this.actionLoading = true; @@ -1220,6 +1340,16 @@ export default { remark: this.form.remark, internalDetails: this.serializeInternalRows(), externalDetails: this.serializeExternalRows(), + feeSummary: this.completionSummaryRows.map(row => ({ + id: row.id || undefined, + feeType: row.feeType, + feeItem: row.feeItem || row.name, + originalAmount: Number(row.originalAmount || 0), + adjustAmount: Number(row.adjustAmount || 0), + settlementAmount: Number(row.afterAmount || 0), + remark: row.remark || '', + manualFlag: Number(row.manualFlag || 0), + })), }) ); this.form = { ...this.form, ...data }; @@ -1237,7 +1367,55 @@ export default { } }, openFeeAdjustment(type) { - this.$message.info(`${type}请在结算明细调整中维护`); + if (!Array.isArray(this.completionSummaryRowsData)) { + this.completionSummaryRowsData = this.buildCompletionSummaryRows.map(row => ({ ...row })); + } + this.completionSummaryRowsData.push({ + id: null, + name: type, + feeType: '其他费用', + feeItem: type, + originalAmount: 0, + adjustAmount: 0, + afterAmount: 0, + remark: '', + manualFlag: 1, + _feeKind: type, + _index: this.completionSummaryRowsData.length, + }); + }, + removeCompletionFee(row) { + if (row.manualFlag !== 1 || !Array.isArray(this.completionSummaryRowsData)) return; + const index = this.completionSummaryRowsData.indexOf(row); + if (index >= 0) this.completionSummaryRowsData.splice(index, 1); + }, + recalculateCompletionFee(row) { + row.originalAmount = row.manualFlag === 1 ? 0 : Number(row.originalAmount || 0); + row.afterAmount = Number(row.originalAmount || 0) + Number(row.adjustAmount || 0); + }, + getCompletionSummarySums({ columns, data }) { + return columns.map((column, index) => { + if (index === 0) return '合计'; + const amountPropertyByIndex = { + 2: 'originalAmount', + 3: 'adjustAmount', + 4: 'afterAmount', + }; + const property = column.property || amountPropertyByIndex[index]; + if (property === 'originalAmount' || property === 'adjustAmount' || property === 'afterAmount') { + const total = data.reduce((sum, row) => { + const value = property === 'afterAmount' ? row.afterAmount : row[property]; + return sum + Number(value || 0); + }, 0); + const text = this.formatCompletionMoney(total); + const negativeAmount = + (property === 'adjustAmount' || property === 'afterAmount') && total < 0; + return negativeAmount + ? h('span', { class: 'completion-dialog__negative', style: { color: '#f56c6c' } }, text) + : text; + } + return ''; + }); }, sumFeeAmount(rows, name) { return rows.reduce((sum, row) => { @@ -1254,11 +1432,16 @@ export default { item.afterAmount ?? item.adjustedAmount ?? item.settlementAmount ?? item.totalAmount ?? originalAmount ); return { + id: item.id, + feeType: item.feeType || '', + feeItem: item.feeItem || item.name || item.feeItemName || '', name: item.name || item.feeItem || item.feeItemName || `费用${index + 1}`, originalAmount, adjustAmount: Number(item.adjustAmount ?? item.adjustmentAmount ?? afterAmount - originalAmount), afterAmount, remark: item.remark || item.adjustRemark || '', + manualFlag: Number(item.manualFlag || 0), + _feeKind: item.feeItem === '补款' || item.feeItem === '扣款' ? item.feeItem : '', _index: index, }; }, @@ -1781,6 +1964,9 @@ export default { .completion-dialog__negative { color: #f56c6c; } +.completion-dialog__negative-input :deep(input) { + color: #f56c6c; +} .completion-dialog__footer { display: flex; justify-content: center;