🔀 合并 feature/20261106-fix禅道-web,解决付款申请与正式结算单冲突

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-11-10 16:54:33 +08:00
co-authored by Cursor
5 changed files with 77 additions and 100 deletions
@@ -23,6 +23,7 @@ export const searchFields = [
{ label: '预付结算单号', prop: 'preSettlementNo', type: 'input' },
{ label: '正式结算单号', prop: 'formalSettlementNo', type: 'input' },
{ label: '批次号', prop: 'batchNo', type: 'input' },
{ label: '运单号', prop: 'waybillNo', type: 'input' },
{ label: '车号', prop: 'vehicleNo', type: 'input' },
];
+36 -31
View File
@@ -97,7 +97,7 @@
<el-option
v-for="item in contractOptions"
:key="item.id"
:label="formatContractOptionLabel(item)"
:label="item.contractName || item.contractNo"
:value="item.id"
/>
</el-select>
@@ -427,7 +427,8 @@
><el-input
v-model="row.description"
:disabled="readonly"
maxlength="200" /></template></el-table-column
maxlength="200"
placeholder="请输入" /></template></el-table-column
><el-table-column label="文件大小" width="120"
><template #default="{ row }">{{
formatAttachmentSize(row)
@@ -744,7 +745,6 @@ import {
getList as getCustomerArchiveList,
} from '@/api/vehicle/customer-archive';
import { readSettlementTransfer, removeSettlementTransfer } from '@/utils/settlement-transfer';
import { formatContractOptionLabel } from '@/utils/contract';
import { downloadFile, downloadFileByUrl } from '@/utils/util';
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
import {
@@ -1155,7 +1155,6 @@ export default {
this.initialize();
},
methods: {
formatContractOptionLabel,
hasPermission(code) {
return this.permission?.[code] !== false;
},
@@ -2126,10 +2125,10 @@ export default {
mergeAutomaticAttachments(
files,
materials,
sourceName,
defaultMaterial = null,
sourceKey = '',
keepUnmatched = false
keepUnmatched = false,
insertIndex = -1
) {
const existingUrls = new Set(
(this.form.attachments || []).map(file => this.attachmentFileUrl(file)).filter(Boolean)
@@ -2153,35 +2152,39 @@ export default {
url,
link: file.link || url,
attachmentType: material ? MATERIAL_TYPE_MAP[material.key] : 'other',
description: file.description || `自动取自${sourceName}`,
description: file.description || '',
sourceImported: true,
sourceKey: sourceKey || undefined,
};
});
if (imported.length) this.form.attachments = [...this.form.attachments, ...imported];
if (imported.length) {
// 回填到被替换附件的原起始位置,避免回显/切换时把用户新传附件顶到前面
const attachments = [...this.form.attachments];
const insertAt =
insertIndex >= 0 ? Math.min(insertIndex, attachments.length) : attachments.length;
attachments.splice(insertAt, 0, ...imported);
this.form.attachments = attachments;
}
return imported;
},
/** 移除指定来源的自动关联附件(切换结算单/合同时整批替换) */
removeAutomaticAttachments(sourceKey, sourceName = '') {
/** 移除指定来源的自动关联附件(切换结算单/合同时整批替换),返回首个被移除的位置 */
removeAutomaticAttachments(sourceKey) {
const previousUrls = new Set(
sourceKey === 'settlement' ? this.linkedSettlementAttachmentUrls || [] : []
);
this.form.attachments = (this.form.attachments || []).filter(file => {
let firstRemovedIndex = -1;
this.form.attachments = (this.form.attachments || []).filter((file, index) => {
const url = this.attachmentFileUrl(file);
if (sourceKey && file.sourceKey === sourceKey) return false;
if (
sourceName &&
file.sourceImported &&
String(file.description || '').includes(`自动取自${sourceName}`)
) {
return false;
}
if (previousUrls.size && url && previousUrls.has(url)) return false;
return true;
const keep =
!(sourceKey && file.sourceKey === sourceKey) &&
!(previousUrls.size && url && previousUrls.has(url));
if (!keep && firstRemovedIndex === -1) firstRemovedIndex = index;
return keep;
});
if (sourceKey === 'settlement') {
this.linkedSettlementAttachmentUrls = [];
}
return firstRemovedIndex;
},
/** 按当前结算单附件整批同步:先清旧关联,再写入新结算单上传的附件 */
syncSettlementAttachments(settlementRows = []) {
@@ -2196,14 +2199,14 @@ export default {
.map(file => this.attachmentFileUrl(file))
.filter(url => url && nextUrlSet.has(url));
}
this.removeAutomaticAttachments('settlement', '结算单');
const removedAt = this.removeAutomaticAttachments('settlement');
const imported = this.mergeAutomaticAttachments(
settlementFiles,
settlementMaterials,
'结算单',
null,
'settlement',
true
true,
removedAt
);
this.linkedSettlementAttachmentUrls = imported
.map(file => this.attachmentFileUrl(file))
@@ -2254,7 +2257,7 @@ export default {
this.syncSettlementAttachments(settlementRows);
if (!this.form.contractId) {
this.firstContractPayment = false;
this.removeAutomaticAttachments('contract', '合同');
this.removeAutomaticAttachments('contract');
return;
}
const contractResponse = await getContractDetail(this.form.contractId);
@@ -2266,13 +2269,14 @@ export default {
this.contractSignatureFileAvailable = contractFiles.some(file =>
Boolean(this.attachmentFileUrl(file))
);
this.removeAutomaticAttachments('contract', '合同');
const contractRemovedAt = this.removeAutomaticAttachments('contract');
this.mergeAutomaticAttachments(
contractFiles,
[ATTACHMENT_MATERIALS.contract],
'合同',
ATTACHMENT_MATERIALS.contract,
'contract'
'contract',
false,
contractRemovedAt
);
return;
}
@@ -2286,14 +2290,15 @@ export default {
String(item.id || '') !== String(this.form.id || '') &&
item.approvalStatus !== 'voided'
);
this.removeAutomaticAttachments('contract', '合同');
const contractRemovedAt = this.removeAutomaticAttachments('contract');
if (this.firstContractPayment) {
this.mergeAutomaticAttachments(
this.parse(contract.contractFileJson),
[ATTACHMENT_MATERIALS.contract],
'合同',
ATTACHMENT_MATERIALS.contract,
'contract'
'contract',
false,
contractRemovedAt
);
}
} catch (error) {
@@ -100,7 +100,12 @@
>
{{ form.formalSettlementNo || (form.id ? '-' : '系统自动生成') }}
</span>
<span v-else-if="field.money">{{ formatMoney(form[field.prop]) }}</span>
<span v-else-if="field.money">{{
formatMoney(
form[field.prop],
field.prop === 'settlementAmount' ? settlementCurrencyCode : 'RMB'
)
}}</span>
<span v-else-if="field.type === 'select' || field.type === 'multi-select'">{{
formSelectLabel(field, form[field.prop])
}}</span>
@@ -196,7 +201,7 @@
'formal-editor__negative-amount': isNegativeAdjustedAmount(row, column.prop),
}"
>
{{ formatMoney(row[column.prop]) }}
{{ formatMoney(row[column.prop], settlementCurrencyCode) }}
</span>
<span v-else-if="column.prop === 'feeType'">{{ feeCategoryName(row.feeType) }}</span>
<span v-else>{{ displayValue(row[column.prop]) }}</span>
@@ -217,7 +222,7 @@
class="formal-editor__settle-summary"
>
<el-descriptions-item label="结算合计(原金额)">
{{ formatMoney(form.settlementAmount) }}
{{ formatMoney(form.settlementAmount, settlementCurrencyCode) }}
</el-descriptions-item>
<el-descriptions-item label="已付金额(含预付、核销)">
{{ formatMoney(settlePaidTotal) }}
@@ -322,7 +327,7 @@
</el-link>
<span v-else-if="item.column.money">{{ formatMoney(row[item.column.prop]) }}</span>
<span v-else-if="item.column.prop === 'transportQuantity'">
{{ formatDetailTransportQuantity(row[item.column.prop]) }}
{{ formatDetailTransportQuantity(row) }}
</span>
<span v-else-if="item.column.prop === 'mileage'">
{{ formatDetailMileage(row[item.column.prop]) }}
@@ -378,7 +383,6 @@
v-model="row.type"
filterable
placeholder="请选择类型"
@change="sortAttachments"
>
<el-option
v-for="item in attachmentTypeOptions"
@@ -1392,6 +1396,10 @@ export default {
isLocalCurrencySettlement() {
return normalizeCurrencyCode(this.form.currency) === 'RMB';
},
// 结算单据币种:本位币结算固定 RMB,外币结算取归一化后的合同结算币种
settlementCurrencyCode() {
return this.isLocalCurrencySettlement ? 'RMB' : normalizeCurrencyCode(this.form.currency);
},
visibleFields() {
if (!this.isLocalCurrencySettlement) return this.fields;
return this.fields.filter(field => !LOCAL_CURRENCY_HIDDEN_FIELDS.includes(field.prop));
@@ -1549,7 +1557,6 @@ export default {
this.selectedAttachmentRows = [];
this.invoices = data.invoices || [];
this.issuedInvoices = data.issuedInvoices || [];
this.sortAttachments();
this.contracts = this.filterContracts(this.allContracts, this.form.projectId);
},
async initialize() {
@@ -1968,12 +1975,13 @@ export default {
});
this.refreshFormalSettlementNo();
}
this.form.settlementAmount = this.sources.reduce(
// 来源预结算金额为本位币;外币结算时结算金额按汇率反算,本位币合计保留本位币汇总
const sourceTotal = this.sources.reduce(
(sum, item) => sum + Number(item.settlementAmount || 0),
0
);
this.form.localSettlementAmount =
this.form.settlementAmount * Number(this.form.exchangeRate || 1);
this.form.settlementAmount = this.convertLocalToSettlementAmount(sourceTotal);
this.form.localSettlementAmount = Number(sourceTotal).toFixed(2);
this.candidate.visible = false;
// 预结算转正式:带出来源预结算已保存的项目预付核销行(后端保存时转移源行,额度单份占用)
const newIds = this.sources
@@ -2123,7 +2131,7 @@ export default {
if (index === 0) return '合计';
if (!sumProps.includes(column.property)) return '';
const total = data.reduce((sum, row) => sum + Number(row[column.property] || 0), 0);
const totalText = this.formatMoney(total);
const totalText = this.formatMoney(total, this.settlementCurrencyCode);
if (column.property === 'settlementAmount' && hasNegativeAdjustment) {
return h('span', { style: { color: 'var(--el-color-danger)' } }, totalText);
}
@@ -3198,16 +3206,16 @@ export default {
const body = response?.data || response || {};
return body?.data || body;
},
formatMoney(value) {
return `${Number(value || 0).toFixed(2)} RMB`;
formatMoney(value, currency = 'RMB') {
return `${Number(value || 0).toFixed(2)} ${currency}`;
},
formatQuantity(value) {
if (value === undefined || value === null || value === '') return '-';
formatDetailTransportQuantity(row) {
const value = row.transportQuantity;
if (this.readonly && Number(value) === -1) return '-';
const quantity = Number(value);
return Number.isFinite(quantity) ? quantity.toFixed(2) : '-';
},
formatDetailTransportQuantity(value) {
return this.readonly && Number(value) === -1 ? '-' : this.formatQuantity(value);
if (!Number.isFinite(quantity)) return '-';
// 与预结算编辑器 formatQuantity 保持一致,拼接计量单位
return `${quantity.toFixed(2)}${row.quantityUnit ? ` ${row.quantityUnit}` : ''}`;
},
formatDetailMileage(value) {
return this.readonly && Number(value) === -1 ? '-' : this.displayValue(value);
@@ -3267,7 +3275,6 @@ export default {
label: item.dictValue,
value: item.dictValue,
}));
this.sortAttachments();
},
resolveAttachmentType(fileName) {
const normalizedName = String(fileName || '').toLocaleLowerCase();
@@ -3291,25 +3298,6 @@ export default {
const typeText = String(item?.label || item?.value || '').trim();
return typeText === '其他' || typeText === '其他附件';
},
getAttachmentTypeOrder(type) {
const normalizedType = String(type || '').trim();
const index = this.attachmentTypeOptions.findIndex(
item =>
String(item.value || '').trim() === normalizedType ||
String(item.label || '').trim() === normalizedType
);
return index === -1 ? Number.MAX_SAFE_INTEGER : index;
},
sortAttachments() {
this.attachments = (this.attachments || [])
.map((file, index) => ({ file, index }))
.sort((a, b) => {
const orderDifference =
this.getAttachmentTypeOrder(a.file.type) - this.getAttachmentTypeOrder(b.file.type);
return orderDifference || a.index - b.index;
})
.map(item => item.file);
},
getMissingAttachmentTypes(files = this.attachments) {
const uploadedTypes = new Set(
(files || [])
@@ -3339,7 +3327,6 @@ export default {
size: this.formatAttachmentSize(file.size),
url: file.url || file.link || '',
});
this.sortAttachments();
},
handleAttachmentSelectionChange(rows) {
this.selectedAttachmentRows = rows || [];
@@ -390,7 +390,6 @@
v-model="row.type"
filterable
placeholder="请选择"
@change="sortAttachments"
>
<el-option
v-for="item in attachmentTypeOptions"
@@ -1602,7 +1601,6 @@ export default {
row => !isApprovalSettlementChange(row.changeContent)
);
this.attachments = this.parseAttachments(detail.attachmentsJson);
this.sortAttachments();
this.selectedAttachmentRows = [];
if (detail.contractId) {
await this.ensureContractPartyOptions(detail.contractId, {
@@ -3135,7 +3133,6 @@ export default {
uploadTime: existing?.uploadTime || file.uploadTime || uploadTime,
};
});
this.sortAttachments();
this.selectedAttachmentRows = [];
},
async loadAttachmentTypeOptions() {
@@ -3151,7 +3148,6 @@ export default {
} catch (error) {
this.attachmentTypeOptions = [];
}
this.sortAttachments();
},
resolveAttachmentType(fileName) {
const normalizedName = String(fileName || '').toLocaleLowerCase();
@@ -3179,25 +3175,6 @@ export default {
typeText === '其它附件'
);
},
getAttachmentTypeOrder(type) {
const normalizedType = String(type || '').trim();
const index = this.attachmentTypeOptions.findIndex(
item =>
String(item.value || '').trim() === normalizedType ||
String(item.label || '').trim() === normalizedType
);
return index === -1 ? Number.MAX_SAFE_INTEGER : index;
},
sortAttachments() {
this.attachments = (this.attachments || [])
.map((file, index) => ({ file, index }))
.sort((a, b) => {
const orderDifference =
this.getAttachmentTypeOrder(a.file.type) - this.getAttachmentTypeOrder(b.file.type);
return orderDifference || a.index - b.index;
})
.map(item => item.file);
},
attachmentTypeName(type) {
const option = this.attachmentTypeOptions.find(
item => String(item.value) === String(type) || String(item.label) === String(type)
@@ -136,7 +136,7 @@
<div class="settlement-detail-page__detail-panel-body">
<el-tabs v-model="detailDialog.activeTab" @tab-change="handleDetailTabChange">
<el-tab-pane label="费用明细" name="fee">
<el-table v-loading="detailDialog.loading" :data="feeRows" border>
<el-table v-loading="detailDialog.loading" :data="feeRows" :max-height="detailTableMaxHeight" border>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column
v-for="column in feeDetailColumns"
@@ -168,7 +168,7 @@
</el-table>
</el-tab-pane>
<el-tab-pane label="变更记录" name="change">
<el-table v-loading="latestChangeLoading" :data="latestChangeRows" border>
<el-table v-loading="latestChangeLoading" :data="latestChangeRows" :max-height="detailTableMaxHeight" border>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column
v-for="column in changeRecordColumns"
@@ -1063,7 +1063,6 @@ import { getContractOptions as getSettlementContractOptions } from '@/api/settle
import { getList as getCargoTypeList } from '@/api/base/cargo-type';
import { exportBlob } from '@/api/common';
import { getDictionary } from '@/api/system/dictbiz';
import { formatContractOptionLabel } from '@/utils/contract';
import { isTierUnitPrice, isUnitPriceAdjustable } from '@/utils/settlement-adjust-rule';
import { downloadXls } from '@/utils/util';
import { createSettlementTransfer } from '@/utils/settlement-transfer';
@@ -1145,8 +1144,6 @@ export default {
rule: null,
},
adjustDialog: { visible: false, loading: false, submitting: false, row: null },
// 表头 + 最多 3 行费用 + 横向滚动条,超出后表体上下滚动
adjustTableMaxHeight: 40 + 49 * 3 + 16,
adjustRows: [],
adjustDynamicFeeColumns: [],
adjustTextProps: ['specification', 'model', 'billingFactor', 'billingType'],
@@ -1224,18 +1221,28 @@ export default {
};
},
computed: {
adjustTableMaxHeight() {
// 面板满高 min(60vh,560) 减去头部(49)、底部(58)、内边距(32);费用行多时表体内部滚动,表头固定
if (!this.adjustDialog.visible) return 40 + 49 * 3 + 16;
return Math.round(Math.min(window.innerHeight * 0.6, 560)) - 138;
},
detailTableMaxHeight() {
// 同上,另减 body 下内边距(16) 和 tabs 头(55)
if (!this.detailDialog.visible) return 40 + 49 * 3 + 16;
return Math.round(Math.min(window.innerHeight * 0.6, 560)) - 178;
},
generateContractName() {
const contract = this.contractOptions.find(
item => String(item.id) === String(this.generateQuery.contractId)
);
return formatContractOptionLabel(contract);
return contract?.contractName || '';
},
updateContractName() {
const contractId = this.updateFeeForm.contractId;
const contract = [...this.updateContractOptions, ...this.contractOptions].find(
item => String(item.id) === String(contractId)
);
return formatContractOptionLabel(contract);
return contract?.contractName || '';
},
updateBillingPlanName() {
const billingPlanId = this.updateFeeForm.billingPlanId;