🐛 修复预付驳回重提、预结算变更记录与收票多选结算单

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-20 01:50:54 +08:00
parent 31794bf26a
commit 4c9bbf1eb7
8 changed files with 397 additions and 134 deletions
+4 -4
View File
@@ -6,17 +6,17 @@ export const getList = (current, size, params) =>
request({ url: `${baseUrl}/list`, method: 'get', params: { current, size, ...params } });
export const getDetail = id => request({ url: `${baseUrl}/detail`, method: 'get', params: { id } });
export const getExpiryCounts = () => request({ url: `${baseUrl}/expiry-counts`, method: 'get' });
export const getAvailableOptions = (keyword, deptId, selectedId) =>
export const getAvailableOptions = (keyword, deptId, selectedId, payerName, payeeName) =>
request({
url: `${baseUrl}/available-options`,
method: 'get',
params: { keyword, deptId, selectedId },
params: { keyword, deptId, selectedId, payerName, payeeName },
});
export const getAvailablePage = (current, size, keyword, deptId, selectedId) =>
export const getAvailablePage = (current, size, keyword, deptId, selectedId, payerName, payeeName) =>
request({
url: `${baseUrl}/available-page`,
method: 'get',
params: { current, size, keyword, deptId, selectedId },
params: { current, size, keyword, deptId, selectedId, payerName, payeeName },
});
export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post', data });
export const remove = id => request({ url: `${baseUrl}/remove`, method: 'post', params: { id } });
@@ -55,7 +55,6 @@ export const detailColumns = [
{ label: '运费', prop: 'freightAmount', minWidth: 120, money: true },
{ label: '调整金额', prop: 'adjustAmount', minWidth: 120, money: true },
{ label: '结算金额(含税)', prop: 'settlementAmountTax', minWidth: 155, money: true },
{ label: '结算金额(不含税)', prop: 'settlementAmountNoTax', minWidth: 170, money: true },
{ label: '备注', prop: 'remark', minWidth: 180 },
];
+117 -43
View File
@@ -44,7 +44,8 @@
reserve-keyword
:remote-method="searchCustomers"
:loading="customerLoading"
placeholder="请选择客商基础档案"
placeholder="请选择"
@change="syncFeeBearer"
>
<el-option
v-for="item in customerOptions"
@@ -63,6 +64,7 @@
allow-create
default-first-option
placeholder="请选择或输入"
@change="syncFeeBearer"
>
<el-option
v-for="item in receiverOptions"
@@ -74,7 +76,7 @@
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="票面金额" prop="faceAmount">
<el-form-item label="票面金额" prop="faceAmount" required>
<el-input-number
v-model="form.faceAmount"
:min="0"
@@ -111,17 +113,15 @@
</el-col>
<el-col :span="6">
<el-form-item label="可用部门" prop="availableDeptIds">
<el-tree-select
v-model="form.availableDeptIds"
:data="deptTree"
:props="deptProps"
node-key="id"
multiple
show-checkbox
<el-cascader
v-model="availableDeptCascaderValue"
:options="businessDeptTreeOptions"
:props="deptCascaderProps"
placeholder="请选择"
filterable
clearable
collapse-tags
collapse-tags-tooltip
placeholder="请选择,默认全部"
@change="handleDeptChange"
/>
</el-form-item>
</el-col>
@@ -140,17 +140,9 @@
</el-col>
<el-col :span="6">
<el-form-item label="费用承担方" prop="feeBearerId">
<el-select
v-model="form.feeBearerId"
filterable
remote
reserve-keyword
:remote-method="searchCustomers"
:loading="customerLoading"
placeholder="请选择客商基础档案"
>
<el-select v-model="form.feeBearerId" filterable clearable placeholder="请选择">
<el-option
v-for="item in customerOptions"
v-for="item in feeBearerOptions"
:key="item.id"
:label="customerName(item)"
:value="item.id"
@@ -245,13 +237,13 @@ const emptyForm = () => ({
billNo: '',
issuerId: null,
receiverName: '',
billType: 'issued',
billType: '',
faceAmount: null,
availableBalance: 0,
issueDate: '',
maturityDate: '',
availableDeptIds: ['all'],
availableDeptNames: '全部',
availableDeptIds: [],
availableDeptNames: '',
feeBearerId: null,
confirmedDiscountRate: null,
issuingBank: '',
@@ -272,9 +264,17 @@ export default {
receiverOptions: [],
customerLoading: false,
saving: false,
deptTree: [],
businessDeptTreeOptions: [],
deptMap: new Map(),
deptProps: { label: 'title', value: 'id', children: 'children' },
deptCascaderProps: {
label: 'label',
value: 'value',
children: 'children',
multiple: true,
checkStrictly: true,
emitPath: true,
expandTrigger: 'click',
},
bankOptions: [
'中国工商银行',
'中国农业银行',
@@ -309,7 +309,10 @@ export default {
issuerId: [{ required: true, message: '请选择出票单位', trigger: 'change' }],
receiverName: [{ required: true, message: '请选择或输入收票单位', trigger: 'change' }],
billType: [{ required: true, message: '请选择汇票类型', trigger: 'change' }],
faceAmount: [{ validator: this.validateFaceAmount, trigger: 'change' }],
faceAmount: [
{ required: true, message: '请输入票面金额', trigger: 'change' },
{ validator: this.validateFaceAmount, trigger: 'change' },
],
issueDate: [{ required: true, message: '请选择出票日期', trigger: 'change' }],
maturityDate: [{ required: true, validator: this.validateMaturityDate, trigger: 'change' }],
availableDeptIds: [{ validator: this.validateDepartments, trigger: 'change' }],
@@ -340,6 +343,42 @@ export default {
(Number(this.form.faceAmount || 0) * Number(this.form.confirmedDiscountRate || 0)) / 100
);
},
// 可用部门:表单存部门 id,级联多选回显用根到节点的路径
availableDeptCascaderValue: {
get() {
return (this.form.availableDeptIds || [])
.filter(id => id !== undefined && id !== null && String(id) !== 'all' && String(id) !== '')
.map(id => this.findDeptPath(id, this.businessDeptTreeOptions) || [String(id)]);
},
set(paths) {
const ids = [
...new Set(
(paths || [])
.map(path => {
const nodes = Array.isArray(path) ? path.filter(item => item !== undefined && item !== null && item !== '') : [];
return nodes.length ? String(nodes.at(-1)) : '';
})
.filter(Boolean)
),
];
this.form.availableDeptIds = ids;
this.form.availableDeptNames = ids
.map(id => this.deptMap.get(String(id)))
.filter(Boolean)
.join('、');
},
},
feeBearerOptions() {
const issuer = this.customerOptions.find(item => String(item.id) === String(this.form.issuerId));
const receiverName = String(this.form.receiverName || '').trim();
const receiver = receiverName
? this.customerOptions.find(item => this.customerName(item) === receiverName)
: null;
return [issuer, receiver].filter(
(item, index, rows) =>
item && rows.findIndex(row => row && String(row.id) === String(item.id)) === index
);
},
},
created() {
this.initialize();
@@ -352,7 +391,6 @@ export default {
async initialize() {
await Promise.all([this.loadDepartments(), this.searchCustomers('')]);
if (!this.recordId) {
this.form.issueDate = this.$dayjs().format('YYYY-MM-DD');
return;
}
const data = this.unwrapData(await api.getDetail(this.recordId));
@@ -361,14 +399,20 @@ export default {
...data,
confirmedDiscountRate: this.normalizeDiscountRate(data.confirmedDiscountRate),
bankDiscountReferenceRate: this.normalizeDiscountRate(data.bankDiscountReferenceRate),
availableDeptIds: this.parse(data.availableDeptIdsJson),
availableDeptIds: this.parse(data.availableDeptIdsJson)
.filter(id => String(id) !== 'all')
.map(id => String(id)),
attachments: this.parse(data.attachmentsJson),
};
this.usageRecords = data.usageRecords || [];
this.receiverOptions = data.receiverName ? [data.receiverName] : [];
this.ensureCustomerOption(data.issuerId, data.issuerName);
this.ensureCustomerOption(data.feeBearerId, data.feeBearerName);
this.handleDeptChange(this.form.availableDeptIds);
this.form.availableDeptNames = (this.form.availableDeptIds || [])
.map(id => this.deptMap.get(String(id)))
.filter(Boolean)
.join('、');
this.syncFeeBearer();
},
parse(value) {
if (!value) return [];
@@ -381,22 +425,52 @@ export default {
},
async loadDepartments() {
const response = await getDeptTree(this.userInfo?.tenantId);
const rows = this.unwrapData(response) || [];
this.deptTree = [{ id: 'all', title: '全部', children: rows }];
this.deptMap = new Map([['all', '全部']]);
this.flattenDepartments(rows).forEach(item => this.deptMap.set(String(item.id), item.title));
const rows = this.excludeExternalOrganization(this.unwrapData(response) || []);
this.businessDeptTreeOptions = this.toDeptCascaderOptions(rows);
this.deptMap = new Map();
this.flattenDepartments(rows).forEach(item =>
this.deptMap.set(String(item.id), item.title || item.deptName || item.name)
);
},
excludeExternalOrganization(list = []) {
return list.reduce((result, item) => {
const deptName = item.title || item.deptName || item.name;
if (String(deptName || '').trim() === '外部组织') return result;
result.push({
...item,
children: item.children?.length
? this.excludeExternalOrganization(item.children)
: item.children,
});
return result;
}, []);
},
toDeptCascaderOptions(list = []) {
return list.map(item => ({
label: item.title || item.deptName || item.name,
value: item.id == null || item.id === '' ? '' : String(item.id),
children: item.children?.length ? this.toDeptCascaderOptions(item.children) : undefined,
}));
},
flattenDepartments(rows) {
return (rows || []).flatMap(item => [item, ...this.flattenDepartments(item.children || [])]);
},
handleDeptChange(values) {
const ids = values || [];
this.form.availableDeptNames = ids.some(item => String(item) === 'all')
? '全部'
: ids
.map(item => this.deptMap.get(String(item)))
.filter(Boolean)
.join('、');
findDeptPath(id, tree = [], parents = []) {
const target = Array.isArray(id) ? id.at(-1) : id;
if (target === undefined || target === null || target === '') return null;
for (const node of tree || []) {
const path = [...parents, node.value];
if (String(node.value) === String(target)) return path;
const matched = this.findDeptPath(target, node.children, path);
if (matched) return matched;
}
return null;
},
syncFeeBearer() {
const allowed = this.feeBearerOptions.map(item => String(item.id));
if (this.form.feeBearerId && !allowed.includes(String(this.form.feeBearerId))) {
this.form.feeBearerId = null;
}
},
async searchCustomers(keyword) {
this.customerLoading = true;
@@ -545,7 +619,7 @@ export default {
.bill-ledger-form-page__form :deep(.el-select),
.bill-ledger-form-page__form :deep(.el-input-number),
.bill-ledger-form-page__form :deep(.el-date-editor),
.bill-ledger-form-page__form :deep(.el-tree-select) {
.bill-ledger-form-page__form :deep(.el-cascader) {
width: 100%;
}
.bill-ledger-form-page__form :deep(.el-input-number .el-input__inner) {
+27 -7
View File
@@ -173,7 +173,7 @@
<el-input
v-model="settlementDialog.keyword"
clearable
placeholder="3结算单号、项目或合同"
placeholder="结算单号、项目或合同"
@keyup.enter="loadSettlementCandidates"
/>
<el-button type="primary" @click="loadSettlementCandidates">查询</el-button>
@@ -182,10 +182,11 @@
ref="settlementTable"
v-loading="settlementDialog.loading"
:data="settlementDialog.rows"
row-key="id"
border
@selection-change="settlementDialog.selection = $event"
>
<el-table-column type="selection" width="52" align="center" />
<el-table-column type="selection" width="52" align="center" reserve-selection />
<el-table-column prop="formalSettlementNo" label="结算单号" min-width="170" />
<el-table-column prop="projectName" label="所属项目" min-width="150" />
<el-table-column prop="deptName" label="所属组织" min-width="150" />
@@ -206,7 +207,9 @@
</el-table>
<template #footer>
<el-button @click="settlementDialog.visible = false">取消</el-button>
<el-button type="primary" @click="confirmSettlements">确定</el-button>
<el-button type="primary" @click="confirmSettlements">
确定{{ settlementDialog.selection.length ? `(${settlementDialog.selection.length})` : '' }}
</el-button>
</template>
</el-dialog>
</basic-container>
@@ -437,7 +440,15 @@ export default {
},
async openSettlementDialog() {
this.settlementDialog.visible = true;
this.settlementDialog.selection = [];
await this.loadSettlementCandidates();
await this.$nextTick();
const selectedIds = new Set(this.settlementIds.map(id => String(id)));
this.settlementDialog.rows.forEach(row => {
if (selectedIds.has(String(row.id))) {
this.$refs.settlementTable?.toggleRowSelection(row, true);
}
});
},
async loadSettlementCandidates() {
this.settlementDialog.loading = true;
@@ -468,11 +479,21 @@ export default {
this.$message.warning('关联结算单必须属于同一项目、组织及收付款方');
return;
}
const previousAmount = new Map(
this.form.settlements.map(item => [
String(item.formalSettlementId || item.settlementId),
item.allocatedInvoiceAmount,
])
);
this.form.settlements = rows.map(item => ({
...item,
formalSettlementId: item.id,
settlementId: item.id,
allocatedInvoiceAmount: rows.length === 1 ? Number(item.settlementAmount || 0) : 0,
allocatedInvoiceAmount: previousAmount.has(String(item.id))
? Number(previousAmount.get(String(item.id)) || 0)
: rows.length === 1
? Number(item.settlementAmount || 0)
: 0,
}));
this.form.projectName = first.projectName;
this.form.deptName = first.deptName;
@@ -482,9 +503,8 @@ export default {
await this.loadReferenceInformation();
this.$refs.formRef?.validateField('settlementIds').catch(() => {});
},
async selectSettlement(row) {
this.settlementDialog.selection = [row];
await this.confirmSettlements();
selectSettlement(row) {
this.$refs.settlementTable?.toggleRowSelection(row);
},
async loadReferenceInformation() {
const data = this.unwrapData(await api.getReferenceInformation(this.settlementIds.join(',')));
+91 -22
View File
@@ -852,6 +852,8 @@ export default {
billOptions: [],
billLoading: false,
billDialogVisible: false,
settlementPayerName: '',
settlementPayeeName: '',
billQuery: {
keyword: '',
},
@@ -1221,8 +1223,12 @@ export default {
callback();
return;
}
if (selected.billType && selected.billType !== 'received') {
callback(new Error('汇票付款仅可选择收票类型的汇票'));
if (
selected.billType &&
selected.billType !== 'received' &&
!this.billMatchesSettlementParties(selected)
) {
callback(new Error('汇票付款仅可选择收票类型,或出票单位与收票单位均为当前结算收付款方的汇票'));
return;
}
if (selected.maturityDate) {
@@ -1580,6 +1586,7 @@ export default {
this.createInvoiceRows(item.invoices || [], item.formalSettlementNo || item.settlementNo)
),
});
this.rememberSettlementParties(first.payerName, first.payeeName);
this.paymentRecords = rows.flatMap(item =>
(item.paymentRecords || item.paymentApplications || []).map(record => ({
...record,
@@ -1667,7 +1674,11 @@ export default {
? 0
: Number(row.payableAmount || 0);
return {
id: row.preSettlementId || (isPre ? row.settlementId : null) || row.settlementId || row.id,
id:
row.preSettlementId ||
(isPre ? row.settlementId : null) ||
row.settlementId ||
(row.paymentNo ? null : row.id),
settlementNo: row.preSettlementNo || row.formalSettlementNo || row.settlementNo || '',
referenceType: isPre ? 'pre' : 'formal',
settlementAmount: Number(Number(settlementAmount || 0).toFixed(2)),
@@ -1868,6 +1879,7 @@ export default {
paymentRatio,
appliedAmount: headerAppliedAmount,
});
this.rememberSettlementParties(first.payerName, first.payeeName);
this.paymentRecords = [];
this.clearReceiptAccount();
await this.fillBillTypeFromContract(first.contractId, true);
@@ -2063,12 +2075,31 @@ export default {
},
resolveAttachmentMaterial(file, materials = Object.values(ATTACHMENT_MATERIALS)) {
if (!this.attachmentFileUrl(file)) return null;
const typeMatched = (materials || []).find(
material => file.attachmentType === MATERIAL_TYPE_MAP[material.key]
);
const typeText = String(file.attachmentType || file.type || '')
.trim()
.toLocaleLowerCase();
const typeMatched = (materials || []).find(material => {
if (file.attachmentType === MATERIAL_TYPE_MAP[material.key]) return true;
if (!typeText) return false;
const label = String(material.label || '').toLocaleLowerCase();
return (
typeText === label ||
material.keywords.some(keyword => {
const normalizedKeyword = String(keyword).toLocaleLowerCase();
return normalizedKeyword && typeText.includes(normalizedKeyword);
})
);
});
return typeMatched || this.matchAttachmentMaterialByFileName(file, materials);
},
mergeAutomaticAttachments(files, materials, sourceName, defaultMaterial = null, sourceKey = '') {
mergeAutomaticAttachments(
files,
materials,
sourceName,
defaultMaterial = null,
sourceKey = '',
keepUnmatched = false
) {
const existingUrls = new Set(
(this.form.attachments || []).map(file => this.attachmentFileUrl(file)).filter(Boolean)
);
@@ -2077,16 +2108,20 @@ export default {
file,
material: this.resolveAttachmentMaterial(file, materials) || defaultMaterial,
}))
.filter(item => item.material && !existingUrls.has(this.attachmentFileUrl(item.file)))
.filter(item => {
const url = this.attachmentFileUrl(item.file);
return Boolean(url) && !existingUrls.has(url) && (item.material || keepUnmatched);
})
.map(({ file, material }) => {
existingUrls.add(this.attachmentFileUrl(file));
const url = this.attachmentFileUrl(file);
existingUrls.add(url);
return {
...file,
originalName: this.attachmentFileName(file),
name: this.attachmentFileName(file),
url: this.attachmentFileUrl(file),
link: file.link || this.attachmentFileUrl(file),
attachmentType: MATERIAL_TYPE_MAP[material.key],
url,
link: file.link || url,
attachmentType: material ? MATERIAL_TYPE_MAP[material.key] : 'other',
description: file.description || `自动取自${sourceName}`,
sourceImported: true,
sourceKey: sourceKey || undefined,
@@ -2119,11 +2154,7 @@ export default {
},
/** 按当前结算单附件整批同步:先清旧关联,再写入新结算单上传的附件 */
syncSettlementAttachments(settlementRows = []) {
const settlementMaterials = [
ATTACHMENT_MATERIALS.weighingSlip,
ATTACHMENT_MATERIALS.entrustOrder,
ATTACHMENT_MATERIALS.settlement,
];
const settlementMaterials = Object.values(ATTACHMENT_MATERIALS);
const settlementFiles = (settlementRows || []).flatMap(row => this.parse(row.attachmentsJson));
const nextUrlSet = new Set(
settlementFiles.map(file => this.attachmentFileUrl(file)).filter(Boolean)
@@ -2140,7 +2171,8 @@ export default {
settlementMaterials,
'结算单',
null,
'settlement'
'settlement',
true
);
this.linkedSettlementAttachmentUrls = imported
.map(file => this.attachmentFileUrl(file))
@@ -2547,7 +2579,13 @@ export default {
try {
this.billOptions =
this.unwrapData(
await billLedgerApi.getAvailableOptions(keyword, this.form.deptId, selectedId)
await billLedgerApi.getAvailableOptions(
keyword,
this.form.deptId,
selectedId,
this.settlementPayerName,
this.settlementPayeeName
)
) || [];
} finally {
this.billLoading = false;
@@ -2569,7 +2607,9 @@ export default {
this.billPage.size,
String(this.billQuery.keyword || '').trim() || undefined,
this.form.deptId,
this.form.billLedgerId
this.form.billLedgerId,
this.settlementPayerName,
this.settlementPayeeName
);
const data = this.unwrapData(response) || {};
this.billRows = data.records || [];
@@ -2590,9 +2630,27 @@ export default {
this.billPage.current = 1;
this.loadBillPage();
},
rememberSettlementParties(payerName, payeeName) {
this.settlementPayerName = String(payerName || '').trim();
this.settlementPayeeName = String(payeeName || '').trim();
},
billMatchesSettlementParties(row) {
const issuer = String(row?.issuerName || '').trim();
const receiver = String(row?.receiverName || '').trim();
const payer = this.settlementPayerName;
const payee = this.settlementPayeeName;
if (!issuer || !receiver || !payer || !payee) return false;
return (
(issuer === payer && receiver === payee) || (issuer === payee && receiver === payer)
);
},
selectBill(row) {
if (row?.billType && row.billType !== 'received') {
ElMessage.warning('汇票付款仅可选择收票类型的汇票');
if (
row?.billType &&
row.billType !== 'received' &&
!this.billMatchesSettlementParties(row)
) {
ElMessage.warning('汇票付款仅可选择收票类型,或出票单位与收票单位均为当前结算收付款方的汇票');
return;
}
if (row?.maturityDate) {
@@ -2848,6 +2906,7 @@ export default {
payerName: row.payerName,
payeeName: '',
});
this.rememberSettlementParties(row.payerName, row.payeeName);
this.referenceVisible = false;
this.refreshReferenceValidation();
const detail = this.unwrapData(detailResponse);
@@ -2930,6 +2989,7 @@ export default {
payerName: row.payerName,
payeeName: '',
});
this.rememberSettlementParties(row.payerName, row.payeeName);
this.referenceVisible = false;
this.refreshReferenceValidation();
const detail = this.unwrapData(detailResponse);
@@ -3147,6 +3207,14 @@ export default {
: resolvedPreSettlementId
? [resolvedPreSettlementId]
: [];
const preSettlementNo = usePreSettlementOnly
? this.form.preSettlementNo ||
(this.referenceRows || [])
.filter(item => item.referenceType === 'pre')
.map(item => item.settlementNo)
.filter(Boolean)
.join('、')
: this.form.preSettlementNo;
return {
id,
paymentType,
@@ -3154,6 +3222,7 @@ export default {
settlementIds: usePreSettlementOnly ? [] : settlementIds,
preSettlementId: usePreSettlementOnly ? resolvedPreSettlementId : preSettlementId,
preSettlementIds: usePreSettlementOnly ? resolvedPreSettlementIds : preSettlementIds,
preSettlementNo,
projectId,
projectName,
deptId,
@@ -53,7 +53,7 @@
<el-option
v-for="item in contracts"
:key="item.id"
:label="formatContractOptionLabel(item)"
:label="item.contractName || ''"
:value="item.id"
/>
</el-select>
@@ -1070,7 +1070,6 @@ import {
import { getDictionary } from '@/api/system/dictbiz';
import { h } from 'vue';
import { mapGetters } from 'vuex';
import { formatContractOptionLabel } from '@/utils/contract';
import { isTierUnitPrice, isUnitPriceAdjustable } from '@/utils/settlement-adjust-rule';
import { normalizeCurrencyCode } from '@/utils/currency';
import {
@@ -1389,7 +1388,6 @@ export default {
methods: {
isTierUnitPrice,
isUnitPriceAdjustable,
formatContractOptionLabel,
hasPermission(code) {
if (this.isAdmin) return true;
const action = String(code || '').replace(
@@ -251,29 +251,32 @@
:key="item.key"
:label="item.kind === 'fee' ? item.name : item.column.label"
:prop="item.kind === 'fee' ? undefined : item.column.prop"
:min-width="item.kind === 'fee' ? 130 : item.column.minWidth"
:min-width="item.kind === 'fee' ? 150 : item.column.minWidth"
align="center"
show-overflow-tooltip
>
<template #default="{ row }">
<span v-if="item.kind === 'fee'" class="pre-settlement-editor__billing-rule-cell">
<span>
{{
formatMoney(
parseFeeItems(row.feeItemsJson)[item.name],
row.currency || form.currency
)
}}
</span>
<template #header>
<span class="pre-settlement-editor__billing-rule-cell">
<span>{{ item.kind === 'fee' ? item.name : item.column.label }}</span>
<el-icon
v-if="hasDetailBillingRule(row)"
v-if="showDetailColumnRuleIcon(item)"
class="pre-settlement-editor__billing-rule-icon"
title="查看计费规则详情"
@click.stop="openDetailBillingRule(row, item.name)"
@click.stop="openDetailColumnRule(item)"
>
<InfoFilled />
</el-icon>
</span>
</template>
<template #default="{ row }">
<span v-if="item.kind === 'fee'">
{{
formatMoney(
parseFeeItems(row.feeItemsJson)[item.name],
row.currency || form.currency
)
}}
</span>
<el-link
v-else-if="item.column.link && row[item.column.prop] && !publicMode"
type="primary"
@@ -284,19 +287,8 @@
<span v-else-if="item.column.money && item.column.prop === 'unitPrice'">
{{ formatUnitPrice(row) }}
</span>
<span
v-else-if="item.column.prop === 'freightAmount'"
class="pre-settlement-editor__billing-rule-cell"
>
<span>{{ formatMoney(row[item.column.prop], row.currency || form.currency) }}</span>
<el-icon
v-if="hasDetailBillingRule(row)"
class="pre-settlement-editor__billing-rule-icon"
title="查看计费规则详情"
@click.stop="openDetailBillingRule(row, '运输费')"
>
<InfoFilled />
</el-icon>
<span v-else-if="item.column.prop === 'freightAmount'">
{{ formatMoney(row[item.column.prop], row.currency || form.currency) }}
</span>
<span v-else-if="item.column.money">
{{ formatMoney(row[item.column.prop], row.currency || form.currency) }}
@@ -499,6 +491,11 @@
<span v-else>{{ displayValue(row[column.prop]) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="100" align="center" fixed="right">
<template #default="{ row }">
<el-link type="primary" @click="openAdjustChangeRecord(row)">查看</el-link>
</template>
</el-table-column>
</el-table>
</section-card>
</div>
@@ -824,25 +821,28 @@
min-width="160"
align="center"
>
<template #default="{ row }">
<div class="pre-settlement-editor__billing-rule-cell">
<el-input-number
v-model="row.feeItems[feeItem]"
:min="0"
:precision="2"
:controls="false"
:disabled="adjustDialog.readonly"
@change="recalculateAdjustRow(row, feeItem)"
/>
<template #header>
<span class="pre-settlement-editor__billing-rule-cell">
<span>{{ feeItem }}</span>
<el-icon
v-if="hasBillingRule(row)"
v-if="showAdjustFeeRuleIcon(feeItem)"
class="pre-settlement-editor__billing-rule-icon"
title="查看计费规则详情"
@click="openBillingRuleInfo(row, feeItem)"
@click.stop="openAdjustFeeRule(feeItem)"
>
<InfoFilled />
</el-icon>
</div>
</span>
</template>
<template #default="{ row }">
<el-input-number
v-model="row.feeItems[feeItem]"
:min="0"
:precision="2"
:controls="false"
:disabled="adjustDialog.readonly"
@change="recalculateAdjustRow(row, feeItem)"
/>
</template>
</el-table-column>
<el-table-column label="结算金额(含税)" min-width="170" align="center">
@@ -1522,7 +1522,9 @@ export default {
availableWriteOffAmount: Number(row.availableWriteOffAmount || 0),
createTime: row.paymentCreateTime || row.createTime,
}));
this.changeRecords = detail.changeRecords || [];
this.changeRecords = (detail.changeRecords || []).filter(
row => row.changeContent !== '预结算审批通过'
);
this.attachments = this.parseAttachments(detail.attachmentsJson);
this.sortAttachments();
this.selectedAttachmentRows = [];
@@ -2307,6 +2309,17 @@ export default {
},
formatAdjustChangeValue(value) {
if (value === undefined || value === null || value === '') return '空';
if (Array.isArray(value)) {
if (!value.length) return '空';
return value
.map(item => {
if (!item || typeof item !== 'object') return String(item);
return Object.entries(item)
.map(([key, itemValue]) => `${key}:${this.formatAdjustChangeValue(itemValue)}`)
.join(',');
})
.join('\n');
}
if (typeof value === 'object') return JSON.stringify(value);
return String(value);
},
@@ -2369,12 +2382,60 @@ export default {
ruleFeeName(rule = {}) {
return String(rule.feeItem || rule.feeItemName || rule.name || '').trim();
},
isHandlingFeeItem(name) {
return String(name || '').includes('装卸');
},
feeNamesMatch(left, right) {
const source = String(left || '').trim();
const target = String(right || '').trim();
if (!source || !target) return false;
if (source === target) return true;
return this.isFreightFeeItem(source) && this.isFreightFeeItem(target);
if (this.isFreightFeeItem(source) && this.isFreightFeeItem(target)) return true;
return this.isHandlingFeeItem(source) && this.isHandlingFeeItem(target);
},
detailColumnFeeName(item = {}) {
if (item.kind === 'fee') return item.name || '';
if (item.column?.prop === 'freightAmount') return '运输费';
return '';
},
showDetailColumnRuleIcon(item) {
const feeName = this.detailColumnFeeName(item);
if (!feeName) return false;
return this.details.some(row => this.rowHasNamedBillingRule(row, feeName));
},
openDetailColumnRule(item) {
const feeName = this.detailColumnFeeName(item);
const row =
this.details.find(detail => this.rowHasNamedBillingRule(detail, feeName)) ||
this.details[0];
if (row) this.openDetailBillingRule(row, feeName);
},
showAdjustFeeRuleIcon(feeName) {
return (this.adjustRows || []).some(row => this.sourceHasNamedBillingRule(row, feeName));
},
openAdjustFeeRule(feeName) {
const row =
(this.adjustRows || []).find(item => this.sourceHasNamedBillingRule(item, feeName)) ||
this.adjustRows[0];
if (row) this.openBillingRuleInfo(row, feeName);
},
rowHasNamedBillingRule(row, feeName) {
return [row, ...this.detailFeeRows(row)].some(source =>
this.sourceHasNamedBillingRule(source, feeName)
);
},
sourceHasNamedBillingRule(source = {}, feeName) {
const rules = [
...this.collectBillingRules(source),
...(Array.isArray(source.billingRule?.rules) ? source.billingRule.rules : []),
];
if (rules.some(rule => this.feeNamesMatch(this.ruleFeeName(rule), feeName))) return true;
if (!this.hasBillingRule(source)) return false;
const named = String(
source.billingFeeItem || source.feeItem || this.ruleFeeName(source.billingRule) || ''
).trim();
if (!named) return true;
return this.feeNamesMatch(named, feeName);
},
collectBillingRules(row = {}) {
const source = row || {};
@@ -241,7 +241,12 @@
<div
class="settlement-detail-page__detail-panel-body settlement-detail-page__adjust-panel-body"
>
<el-table v-loading="adjustDialog.loading" :data="adjustRows" border>
<el-table
v-loading="adjustDialog.loading"
:data="adjustRows"
:max-height="adjustTableMaxHeight"
border
>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column label="来源" width="110" align="center">
<template #default="{ row }">{{ feeSourceLabel(row.dataSource) }}</template>
@@ -1115,6 +1120,8 @@ export default {
page: { current: 1, size: 10, total: 0 },
// 底部详情/调整面板打开时压缩主表高度,露出表格滚动条
listTableHeight: undefined,
savedListTableHeight: null,
listTableLayoutToken: 0,
detailDialog: {
visible: false,
title: '费用明细',
@@ -1129,6 +1136,8 @@ 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'],
@@ -1330,8 +1339,11 @@ export default {
'$route.fullPath'() {
this.openRouteDetail();
},
isBottomPanelOpen() {
this.updateListTableHeight();
isBottomPanelOpen: {
flush: 'post',
handler() {
this.updateListTableHeight();
},
},
searchVisible() {
this.updateListTableHeight();
@@ -1396,36 +1408,62 @@ export default {
},
/** 表格和分页撑满剩余可视区域;单据详情展开时贴住详情面板,关闭后恢复 */
updateListTableHeight() {
this.listTableLayoutToken = (this.listTableLayoutToken || 0) + 1;
const token = this.listTableLayoutToken;
this.$nextTick(() => {
window.requestAnimationFrame(() => this.applyListTableHeight());
window.requestAnimationFrame(() => {
if (token !== this.listTableLayoutToken) return;
this.applyListTableHeight(token);
window.requestAnimationFrame(() => {
if (token !== this.listTableLayoutToken) return;
this.applyListTableHeight(token);
});
});
});
},
applyListTableHeight() {
applyListTableHeight(token = this.listTableLayoutToken) {
if (token !== this.listTableLayoutToken) return;
const tableElement = this.$refs.mainTableRef?.$el;
if (!tableElement) return;
const pagination = this.$el?.querySelector?.('.settlement-detail-page__pagination');
const pagination = this.$el?.querySelector?.(
'.settlement-detail-page__table-panel .settlement-detail-page__pagination'
);
const paginationHeight = pagination?.offsetHeight || 0;
const tableTop = tableElement.getBoundingClientRect().top;
const view = this.$el?.closest?.('#avue-view');
const viewRect = view?.getBoundingClientRect?.();
let bottomEdge = viewRect ? viewRect.bottom - 12 : window.innerHeight;
if (this.isBottomPanelOpen) {
// 用滚动容器的可视高度,避免表格被压矮后按内容底部把高度锁死
let bottomEdge = view
? viewRect.top + view.clientHeight - 12
: window.innerHeight;
const panelOpen = this.isBottomPanelOpen;
if (panelOpen) {
if (!this.savedListTableHeight) {
this.savedListTableHeight = this.listTableHeight || null;
}
const panelRect = document
.querySelector('.settlement-detail-page__detail-panel')
?.getBoundingClientRect?.();
if (panelRect && panelRect.height > 0) {
bottomEdge = panelRect.top;
bottomEdge = Math.min(bottomEdge, panelRect.top);
this.listTableHeightRetry = 0;
} else if ((this.listTableHeightRetry || 0) < 5) {
this.listTableHeightRetry = (this.listTableHeightRetry || 0) + 1;
window.requestAnimationFrame(() => this.applyListTableHeight());
window.requestAnimationFrame(() => this.applyListTableHeight(token));
return;
}
} else {
this.listTableHeightRetry = 0;
}
const gap = this.isBottomPanelOpen ? 8 : 0;
this.listTableHeight = Math.max(180, Math.floor(bottomEdge - tableTop - paginationHeight - gap));
const gap = panelOpen ? 8 : 0;
let nextHeight = Math.max(180, Math.floor(bottomEdge - tableTop - paginationHeight - gap));
if (!panelOpen && this.savedListTableHeight) {
if (nextHeight + 24 < this.savedListTableHeight) {
nextHeight = this.savedListTableHeight;
}
this.savedListTableHeight = null;
}
this.listTableHeight = nextHeight;
this.$nextTick(() => this.$refs.mainTableRef?.doLayout?.());
},
contractCategoryName(value) {
@@ -1858,11 +1896,13 @@ export default {
this.changeRecordsDialog.visible = false;
this.billingRuleDialog.visible = false;
this.billingRuleDialog.rule = null;
this.updateListTableHeight();
},
closeAdjustPanel() {
this.clearAdjustCalculations();
this.adjustDialog.visible = false;
this.adjustDialog.row = null;
this.updateListTableHeight();
},
handleTableLink(row, column) {
if (column.prop === 'waybillNo') {
@@ -2446,7 +2486,8 @@ export default {
return;
}
const options = this.syncBillingPlanOptions(contractId);
const selected = options.find(item => item.defaultPlan) || options[0];
const defaultOptions = options.filter(item => item.defaultPlan);
const selected = defaultOptions[defaultOptions.length - 1] || options[options.length - 1];
this.updateFeeForm.billingPlanId = selected?.id || '';
},
async confirmUpdateFee() {
@@ -3333,6 +3374,7 @@ export default {
.settlement-detail-page__adjust-panel-body {
padding-top: 16px;
overflow: hidden;
}
.settlement-detail-page__billing-rule-cell {