Merge remote-tracking branch 'origin/master'

This commit is contained in:
2026-10-20 19:08:26 +08:00
10 changed files with 511 additions and 200 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 } }); request({ url: `${baseUrl}/list`, method: 'get', params: { current, size, ...params } });
export const getDetail = id => request({ url: `${baseUrl}/detail`, method: 'get', params: { id } }); export const getDetail = id => request({ url: `${baseUrl}/detail`, method: 'get', params: { id } });
export const getExpiryCounts = () => request({ url: `${baseUrl}/expiry-counts`, method: 'get' }); 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({ request({
url: `${baseUrl}/available-options`, url: `${baseUrl}/available-options`,
method: 'get', 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({ request({
url: `${baseUrl}/available-page`, url: `${baseUrl}/available-page`,
method: 'get', 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 submit = data => request({ url: `${baseUrl}/submit`, method: 'post', data });
export const remove = id => request({ url: `${baseUrl}/remove`, method: 'post', params: { id } }); 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: 'freightAmount', minWidth: 120, money: true },
{ label: '调整金额', prop: 'adjustAmount', minWidth: 120, money: true }, { label: '调整金额', prop: 'adjustAmount', minWidth: 120, money: true },
{ label: '结算金额(含税)', prop: 'settlementAmountTax', minWidth: 155, money: true }, { label: '结算金额(含税)', prop: 'settlementAmountTax', minWidth: 155, money: true },
{ label: '结算金额(不含税)', prop: 'settlementAmountNoTax', minWidth: 170, money: true },
{ label: '备注', prop: 'remark', minWidth: 180 }, { label: '备注', prop: 'remark', minWidth: 180 },
]; ];
@@ -96,22 +96,12 @@
prop="carrierName" prop="carrierName"
label="承运商" label="承运商"
min-width="140" min-width="140"
/><el-table-column
v-if="columnVisible.carrierType"
prop="carrierType"
label="承运类型"
min-width="120"
/> />
<el-table-column <el-table-column
v-if="columnVisible.waybillCount" v-if="columnVisible.waybillCount"
prop="waybillCount" prop="waybillCount"
label="运单数" label="运单数"
width="90" width="90"
/> <el-table-column
v-if="columnVisible.importTypeName"
prop="importTypeName"
label="导入方式"
width="100"
/> />
<el-table-column <el-table-column
v-if="columnVisible.statusName" v-if="columnVisible.statusName"
@@ -119,6 +109,12 @@
label="状态" label="状态"
width="100" width="100"
/> />
<el-table-column
v-if="columnVisible.settlementStatusName"
prop="settlementStatusName"
label="结算状态"
width="110"
/>
<el-table-column <el-table-column
v-if="columnVisible.createUserName" v-if="columnVisible.createUserName"
prop="createUserName" prop="createUserName"
@@ -159,7 +155,7 @@
>编辑</el-link >编辑</el-link
> >
<el-link <el-link
v-if="row.importStatus === 'draft'" v-if="canRemoveBatch(row)"
type="danger" type="danger"
@click="removeBatch(row)" @click="removeBatch(row)"
>删除</el-link >删除</el-link
@@ -641,10 +637,9 @@ const columnOptions = [
{ prop: 'batchNo', label: '运单批次号' }, { prop: 'batchNo', label: '运单批次号' },
{ prop: 'projectName', label: '项目名称' }, { prop: 'projectName', label: '项目名称' },
{ prop: 'carrierName', label: '承运商' }, { prop: 'carrierName', label: '承运商' },
{ prop: 'carrierType', label: '承运类型' },
{ prop: 'waybillCount', label: '运单数' }, { prop: 'waybillCount', label: '运单数' },
{ prop: 'importTypeName', label: '导入方式' },
{ prop: 'statusName', label: '状态' }, { prop: 'statusName', label: '状态' },
{ prop: 'settlementStatusName', label: '结算状态' },
{ prop: 'createUserName', label: '创建人' }, { prop: 'createUserName', label: '创建人' },
{ prop: 'createTime', label: '创建时间' }, { prop: 'createTime', label: '创建时间' },
{ prop: 'updateTime', label: '更新时间' }, { prop: 'updateTime', label: '更新时间' },
@@ -1266,26 +1261,46 @@ const fillEditForm = async row => {
if (carrierValue) carrierChange(carrierValue); if (carrierValue) carrierChange(carrierValue);
await loadBatchRows(snapshot.id); await loadBatchRows(snapshot.id);
}; };
/** 与行操作一致:仅草稿可删除 */ /** 与后端一致:仅未结算批次可整批删除 */
const canRemoveBatch = row => String(row?.importStatus || '') === 'draft'; const canRemoveBatch = row => String(row?.settlementStatus || 'unsettled') === 'unsettled';
const removeBatches = () => { const removeBatches = async () => {
if (!selected.value.length) { if (!selected.value.length) {
ElMessage.warning('请选择至少一条数据'); ElMessage.warning('请选择至少一条数据');
return; return;
} }
// 选中含不可删条目时整批拦截(含已完成/进行中),不静默跳过 // 选中含部分结算/已结算时整批拦截,不静默跳过
if (selected.value.some(row => !canRemoveBatch(row))) { if (selected.value.some(row => !canRemoveBatch(row))) {
ElMessage.warning('已完成运单不可删除'); ElMessage.warning('仅未结算的运单批次可以删除');
return; return;
} }
return api.removeImportBatches(selected.value.map(item => item.id).join(',')).then(loadBatches); try {
await ElMessageBox.confirm('确认删除选中的运单批次?删除后不可恢复。', '提示', {
type: 'warning',
});
} catch {
return;
}
await api.removeImportBatches(selected.value.map(item => item.id).join(','));
ElMessage.success('删除成功');
return loadBatches();
}; };
const removeBatch = row => { const removeBatch = async row => {
if (!canRemoveBatch(row)) { if (!canRemoveBatch(row)) {
ElMessage.warning('已完成运单不可删除'); ElMessage.warning('仅未结算的运单批次可以删除');
return; return;
} }
return api.removeImportBatches(row.id).then(loadBatches); try {
await ElMessageBox.confirm(
`确认删除运单批次「${row.batchNo || ''}」?删除后不可恢复。`,
'提示',
{ type: 'warning' }
);
} catch {
return;
}
await api.removeImportBatches(row.id);
ElMessage.success('删除成功');
return loadBatches();
}; };
const projectChange = async id => { const projectChange = async id => {
const requestId = ++projectRequestId; const requestId = ++projectRequestId;
+117 -43
View File
@@ -44,7 +44,8 @@
reserve-keyword reserve-keyword
:remote-method="searchCustomers" :remote-method="searchCustomers"
:loading="customerLoading" :loading="customerLoading"
placeholder="请选择客商基础档案" placeholder="请选择"
@change="syncFeeBearer"
> >
<el-option <el-option
v-for="item in customerOptions" v-for="item in customerOptions"
@@ -63,6 +64,7 @@
allow-create allow-create
default-first-option default-first-option
placeholder="请选择或输入" placeholder="请选择或输入"
@change="syncFeeBearer"
> >
<el-option <el-option
v-for="item in receiverOptions" v-for="item in receiverOptions"
@@ -74,7 +76,7 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="6"> <el-col :span="6">
<el-form-item label="票面金额" prop="faceAmount"> <el-form-item label="票面金额" prop="faceAmount" required>
<el-input-number <el-input-number
v-model="form.faceAmount" v-model="form.faceAmount"
:min="0" :min="0"
@@ -111,17 +113,15 @@
</el-col> </el-col>
<el-col :span="6"> <el-col :span="6">
<el-form-item label="可用部门" prop="availableDeptIds"> <el-form-item label="可用部门" prop="availableDeptIds">
<el-tree-select <el-cascader
v-model="form.availableDeptIds" v-model="availableDeptCascaderValue"
:data="deptTree" :options="businessDeptTreeOptions"
:props="deptProps" :props="deptCascaderProps"
node-key="id" placeholder="请选择"
multiple filterable
show-checkbox clearable
collapse-tags collapse-tags
collapse-tags-tooltip collapse-tags-tooltip
placeholder="请选择,默认全部"
@change="handleDeptChange"
/> />
</el-form-item> </el-form-item>
</el-col> </el-col>
@@ -140,17 +140,9 @@
</el-col> </el-col>
<el-col :span="6"> <el-col :span="6">
<el-form-item label="费用承担方" prop="feeBearerId"> <el-form-item label="费用承担方" prop="feeBearerId">
<el-select <el-select v-model="form.feeBearerId" filterable clearable placeholder="请选择">
v-model="form.feeBearerId"
filterable
remote
reserve-keyword
:remote-method="searchCustomers"
:loading="customerLoading"
placeholder="请选择客商基础档案"
>
<el-option <el-option
v-for="item in customerOptions" v-for="item in feeBearerOptions"
:key="item.id" :key="item.id"
:label="customerName(item)" :label="customerName(item)"
:value="item.id" :value="item.id"
@@ -245,13 +237,13 @@ const emptyForm = () => ({
billNo: '', billNo: '',
issuerId: null, issuerId: null,
receiverName: '', receiverName: '',
billType: 'issued', billType: '',
faceAmount: null, faceAmount: null,
availableBalance: 0, availableBalance: 0,
issueDate: '', issueDate: '',
maturityDate: '', maturityDate: '',
availableDeptIds: ['all'], availableDeptIds: [],
availableDeptNames: '全部', availableDeptNames: '',
feeBearerId: null, feeBearerId: null,
confirmedDiscountRate: null, confirmedDiscountRate: null,
issuingBank: '', issuingBank: '',
@@ -272,9 +264,17 @@ export default {
receiverOptions: [], receiverOptions: [],
customerLoading: false, customerLoading: false,
saving: false, saving: false,
deptTree: [], businessDeptTreeOptions: [],
deptMap: new Map(), 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: [ bankOptions: [
'中国工商银行', '中国工商银行',
'中国农业银行', '中国农业银行',
@@ -309,7 +309,10 @@ export default {
issuerId: [{ required: true, message: '请选择出票单位', trigger: 'change' }], issuerId: [{ required: true, message: '请选择出票单位', trigger: 'change' }],
receiverName: [{ required: true, message: '请选择或输入收票单位', trigger: 'change' }], receiverName: [{ required: true, message: '请选择或输入收票单位', trigger: 'change' }],
billType: [{ 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' }], issueDate: [{ required: true, message: '请选择出票日期', trigger: 'change' }],
maturityDate: [{ required: true, validator: this.validateMaturityDate, trigger: 'change' }], maturityDate: [{ required: true, validator: this.validateMaturityDate, trigger: 'change' }],
availableDeptIds: [{ validator: this.validateDepartments, 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 (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() { created() {
this.initialize(); this.initialize();
@@ -352,7 +391,6 @@ export default {
async initialize() { async initialize() {
await Promise.all([this.loadDepartments(), this.searchCustomers('')]); await Promise.all([this.loadDepartments(), this.searchCustomers('')]);
if (!this.recordId) { if (!this.recordId) {
this.form.issueDate = this.$dayjs().format('YYYY-MM-DD');
return; return;
} }
const data = this.unwrapData(await api.getDetail(this.recordId)); const data = this.unwrapData(await api.getDetail(this.recordId));
@@ -361,14 +399,20 @@ export default {
...data, ...data,
confirmedDiscountRate: this.normalizeDiscountRate(data.confirmedDiscountRate), confirmedDiscountRate: this.normalizeDiscountRate(data.confirmedDiscountRate),
bankDiscountReferenceRate: this.normalizeDiscountRate(data.bankDiscountReferenceRate), 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), attachments: this.parse(data.attachmentsJson),
}; };
this.usageRecords = data.usageRecords || []; this.usageRecords = data.usageRecords || [];
this.receiverOptions = data.receiverName ? [data.receiverName] : []; this.receiverOptions = data.receiverName ? [data.receiverName] : [];
this.ensureCustomerOption(data.issuerId, data.issuerName); this.ensureCustomerOption(data.issuerId, data.issuerName);
this.ensureCustomerOption(data.feeBearerId, data.feeBearerName); 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) { parse(value) {
if (!value) return []; if (!value) return [];
@@ -381,22 +425,52 @@ export default {
}, },
async loadDepartments() { async loadDepartments() {
const response = await getDeptTree(this.userInfo?.tenantId); const response = await getDeptTree(this.userInfo?.tenantId);
const rows = this.unwrapData(response) || []; const rows = this.excludeExternalOrganization(this.unwrapData(response) || []);
this.deptTree = [{ id: 'all', title: '全部', children: rows }]; this.businessDeptTreeOptions = this.toDeptCascaderOptions(rows);
this.deptMap = new Map([['all', '全部']]); this.deptMap = new Map();
this.flattenDepartments(rows).forEach(item => this.deptMap.set(String(item.id), item.title)); 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) { flattenDepartments(rows) {
return (rows || []).flatMap(item => [item, ...this.flattenDepartments(item.children || [])]); return (rows || []).flatMap(item => [item, ...this.flattenDepartments(item.children || [])]);
}, },
handleDeptChange(values) { findDeptPath(id, tree = [], parents = []) {
const ids = values || []; const target = Array.isArray(id) ? id.at(-1) : id;
this.form.availableDeptNames = ids.some(item => String(item) === 'all') if (target === undefined || target === null || target === '') return null;
? '全部' for (const node of tree || []) {
: ids const path = [...parents, node.value];
.map(item => this.deptMap.get(String(item))) if (String(node.value) === String(target)) return path;
.filter(Boolean) const matched = this.findDeptPath(target, node.children, path);
.join('、'); 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) { async searchCustomers(keyword) {
this.customerLoading = true; this.customerLoading = true;
@@ -545,7 +619,7 @@ export default {
.bill-ledger-form-page__form :deep(.el-select), .bill-ledger-form-page__form :deep(.el-select),
.bill-ledger-form-page__form :deep(.el-input-number), .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-date-editor),
.bill-ledger-form-page__form :deep(.el-tree-select) { .bill-ledger-form-page__form :deep(.el-cascader) {
width: 100%; width: 100%;
} }
.bill-ledger-form-page__form :deep(.el-input-number .el-input__inner) { .bill-ledger-form-page__form :deep(.el-input-number .el-input__inner) {
+27 -7
View File
@@ -173,7 +173,7 @@
<el-input <el-input
v-model="settlementDialog.keyword" v-model="settlementDialog.keyword"
clearable clearable
placeholder="3结算单号、项目或合同" placeholder="结算单号、项目或合同"
@keyup.enter="loadSettlementCandidates" @keyup.enter="loadSettlementCandidates"
/> />
<el-button type="primary" @click="loadSettlementCandidates">查询</el-button> <el-button type="primary" @click="loadSettlementCandidates">查询</el-button>
@@ -182,10 +182,11 @@
ref="settlementTable" ref="settlementTable"
v-loading="settlementDialog.loading" v-loading="settlementDialog.loading"
:data="settlementDialog.rows" :data="settlementDialog.rows"
row-key="id"
border border
@selection-change="settlementDialog.selection = $event" @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="formalSettlementNo" label="结算单号" min-width="170" />
<el-table-column prop="projectName" label="所属项目" min-width="150" /> <el-table-column prop="projectName" label="所属项目" min-width="150" />
<el-table-column prop="deptName" label="所属组织" min-width="150" /> <el-table-column prop="deptName" label="所属组织" min-width="150" />
@@ -206,7 +207,9 @@
</el-table> </el-table>
<template #footer> <template #footer>
<el-button @click="settlementDialog.visible = false">取消</el-button> <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> </template>
</el-dialog> </el-dialog>
</basic-container> </basic-container>
@@ -437,7 +440,15 @@ export default {
}, },
async openSettlementDialog() { async openSettlementDialog() {
this.settlementDialog.visible = true; this.settlementDialog.visible = true;
this.settlementDialog.selection = [];
await this.loadSettlementCandidates(); 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() { async loadSettlementCandidates() {
this.settlementDialog.loading = true; this.settlementDialog.loading = true;
@@ -468,11 +479,21 @@ export default {
this.$message.warning('关联结算单必须属于同一项目、组织及收付款方'); this.$message.warning('关联结算单必须属于同一项目、组织及收付款方');
return; return;
} }
const previousAmount = new Map(
this.form.settlements.map(item => [
String(item.formalSettlementId || item.settlementId),
item.allocatedInvoiceAmount,
])
);
this.form.settlements = rows.map(item => ({ this.form.settlements = rows.map(item => ({
...item, ...item,
formalSettlementId: item.id, formalSettlementId: item.id,
settlementId: 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.projectName = first.projectName;
this.form.deptName = first.deptName; this.form.deptName = first.deptName;
@@ -482,9 +503,8 @@ export default {
await this.loadReferenceInformation(); await this.loadReferenceInformation();
this.$refs.formRef?.validateField('settlementIds').catch(() => {}); this.$refs.formRef?.validateField('settlementIds').catch(() => {});
}, },
async selectSettlement(row) { selectSettlement(row) {
this.settlementDialog.selection = [row]; this.$refs.settlementTable?.toggleRowSelection(row);
await this.confirmSettlements();
}, },
async loadReferenceInformation() { async loadReferenceInformation() {
const data = this.unwrapData(await api.getReferenceInformation(this.settlementIds.join(','))); const data = this.unwrapData(await api.getReferenceInformation(this.settlementIds.join(',')));
+91 -22
View File
@@ -852,6 +852,8 @@ export default {
billOptions: [], billOptions: [],
billLoading: false, billLoading: false,
billDialogVisible: false, billDialogVisible: false,
settlementPayerName: '',
settlementPayeeName: '',
billQuery: { billQuery: {
keyword: '', keyword: '',
}, },
@@ -1221,8 +1223,12 @@ export default {
callback(); callback();
return; return;
} }
if (selected.billType && selected.billType !== 'received') { if (
callback(new Error('汇票付款仅可选择收票类型的汇票')); selected.billType &&
selected.billType !== 'received' &&
!this.billMatchesSettlementParties(selected)
) {
callback(new Error('汇票付款仅可选择收票类型,或出票单位与收票单位均为当前结算收付款方的汇票'));
return; return;
} }
if (selected.maturityDate) { if (selected.maturityDate) {
@@ -1580,6 +1586,7 @@ export default {
this.createInvoiceRows(item.invoices || [], item.formalSettlementNo || item.settlementNo) this.createInvoiceRows(item.invoices || [], item.formalSettlementNo || item.settlementNo)
), ),
}); });
this.rememberSettlementParties(first.payerName, first.payeeName);
this.paymentRecords = rows.flatMap(item => this.paymentRecords = rows.flatMap(item =>
(item.paymentRecords || item.paymentApplications || []).map(record => ({ (item.paymentRecords || item.paymentApplications || []).map(record => ({
...record, ...record,
@@ -1667,7 +1674,11 @@ export default {
? 0 ? 0
: Number(row.payableAmount || 0); : Number(row.payableAmount || 0);
return { 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 || '', settlementNo: row.preSettlementNo || row.formalSettlementNo || row.settlementNo || '',
referenceType: isPre ? 'pre' : 'formal', referenceType: isPre ? 'pre' : 'formal',
settlementAmount: Number(Number(settlementAmount || 0).toFixed(2)), settlementAmount: Number(Number(settlementAmount || 0).toFixed(2)),
@@ -1868,6 +1879,7 @@ export default {
paymentRatio, paymentRatio,
appliedAmount: headerAppliedAmount, appliedAmount: headerAppliedAmount,
}); });
this.rememberSettlementParties(first.payerName, first.payeeName);
this.paymentRecords = []; this.paymentRecords = [];
this.clearReceiptAccount(); this.clearReceiptAccount();
await this.fillBillTypeFromContract(first.contractId, true); await this.fillBillTypeFromContract(first.contractId, true);
@@ -2063,12 +2075,31 @@ export default {
}, },
resolveAttachmentMaterial(file, materials = Object.values(ATTACHMENT_MATERIALS)) { resolveAttachmentMaterial(file, materials = Object.values(ATTACHMENT_MATERIALS)) {
if (!this.attachmentFileUrl(file)) return null; if (!this.attachmentFileUrl(file)) return null;
const typeMatched = (materials || []).find( const typeText = String(file.attachmentType || file.type || '')
material => file.attachmentType === MATERIAL_TYPE_MAP[material.key] .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); 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( const existingUrls = new Set(
(this.form.attachments || []).map(file => this.attachmentFileUrl(file)).filter(Boolean) (this.form.attachments || []).map(file => this.attachmentFileUrl(file)).filter(Boolean)
); );
@@ -2077,16 +2108,20 @@ export default {
file, file,
material: this.resolveAttachmentMaterial(file, materials) || defaultMaterial, 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 }) => { .map(({ file, material }) => {
existingUrls.add(this.attachmentFileUrl(file)); const url = this.attachmentFileUrl(file);
existingUrls.add(url);
return { return {
...file, ...file,
originalName: this.attachmentFileName(file), originalName: this.attachmentFileName(file),
name: this.attachmentFileName(file), name: this.attachmentFileName(file),
url: this.attachmentFileUrl(file), url,
link: file.link || this.attachmentFileUrl(file), link: file.link || url,
attachmentType: MATERIAL_TYPE_MAP[material.key], attachmentType: material ? MATERIAL_TYPE_MAP[material.key] : 'other',
description: file.description || `自动取自${sourceName}`, description: file.description || `自动取自${sourceName}`,
sourceImported: true, sourceImported: true,
sourceKey: sourceKey || undefined, sourceKey: sourceKey || undefined,
@@ -2119,11 +2154,7 @@ export default {
}, },
/** 按当前结算单附件整批同步:先清旧关联,再写入新结算单上传的附件 */ /** 按当前结算单附件整批同步:先清旧关联,再写入新结算单上传的附件 */
syncSettlementAttachments(settlementRows = []) { syncSettlementAttachments(settlementRows = []) {
const settlementMaterials = [ const settlementMaterials = Object.values(ATTACHMENT_MATERIALS);
ATTACHMENT_MATERIALS.weighingSlip,
ATTACHMENT_MATERIALS.entrustOrder,
ATTACHMENT_MATERIALS.settlement,
];
const settlementFiles = (settlementRows || []).flatMap(row => this.parse(row.attachmentsJson)); const settlementFiles = (settlementRows || []).flatMap(row => this.parse(row.attachmentsJson));
const nextUrlSet = new Set( const nextUrlSet = new Set(
settlementFiles.map(file => this.attachmentFileUrl(file)).filter(Boolean) settlementFiles.map(file => this.attachmentFileUrl(file)).filter(Boolean)
@@ -2140,7 +2171,8 @@ export default {
settlementMaterials, settlementMaterials,
'结算单', '结算单',
null, null,
'settlement' 'settlement',
true
); );
this.linkedSettlementAttachmentUrls = imported this.linkedSettlementAttachmentUrls = imported
.map(file => this.attachmentFileUrl(file)) .map(file => this.attachmentFileUrl(file))
@@ -2547,7 +2579,13 @@ export default {
try { try {
this.billOptions = this.billOptions =
this.unwrapData( this.unwrapData(
await billLedgerApi.getAvailableOptions(keyword, this.form.deptId, selectedId) await billLedgerApi.getAvailableOptions(
keyword,
this.form.deptId,
selectedId,
this.settlementPayerName,
this.settlementPayeeName
)
) || []; ) || [];
} finally { } finally {
this.billLoading = false; this.billLoading = false;
@@ -2569,7 +2607,9 @@ export default {
this.billPage.size, this.billPage.size,
String(this.billQuery.keyword || '').trim() || undefined, String(this.billQuery.keyword || '').trim() || undefined,
this.form.deptId, this.form.deptId,
this.form.billLedgerId this.form.billLedgerId,
this.settlementPayerName,
this.settlementPayeeName
); );
const data = this.unwrapData(response) || {}; const data = this.unwrapData(response) || {};
this.billRows = data.records || []; this.billRows = data.records || [];
@@ -2590,9 +2630,27 @@ export default {
this.billPage.current = 1; this.billPage.current = 1;
this.loadBillPage(); 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) { selectBill(row) {
if (row?.billType && row.billType !== 'received') { if (
ElMessage.warning('汇票付款仅可选择收票类型的汇票'); row?.billType &&
row.billType !== 'received' &&
!this.billMatchesSettlementParties(row)
) {
ElMessage.warning('汇票付款仅可选择收票类型,或出票单位与收票单位均为当前结算收付款方的汇票');
return; return;
} }
if (row?.maturityDate) { if (row?.maturityDate) {
@@ -2848,6 +2906,7 @@ export default {
payerName: row.payerName, payerName: row.payerName,
payeeName: '', payeeName: '',
}); });
this.rememberSettlementParties(row.payerName, row.payeeName);
this.referenceVisible = false; this.referenceVisible = false;
this.refreshReferenceValidation(); this.refreshReferenceValidation();
const detail = this.unwrapData(detailResponse); const detail = this.unwrapData(detailResponse);
@@ -2930,6 +2989,7 @@ export default {
payerName: row.payerName, payerName: row.payerName,
payeeName: '', payeeName: '',
}); });
this.rememberSettlementParties(row.payerName, row.payeeName);
this.referenceVisible = false; this.referenceVisible = false;
this.refreshReferenceValidation(); this.refreshReferenceValidation();
const detail = this.unwrapData(detailResponse); const detail = this.unwrapData(detailResponse);
@@ -3147,6 +3207,14 @@ export default {
: resolvedPreSettlementId : resolvedPreSettlementId
? [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 { return {
id, id,
paymentType, paymentType,
@@ -3154,6 +3222,7 @@ export default {
settlementIds: usePreSettlementOnly ? [] : settlementIds, settlementIds: usePreSettlementOnly ? [] : settlementIds,
preSettlementId: usePreSettlementOnly ? resolvedPreSettlementId : preSettlementId, preSettlementId: usePreSettlementOnly ? resolvedPreSettlementId : preSettlementId,
preSettlementIds: usePreSettlementOnly ? resolvedPreSettlementIds : preSettlementIds, preSettlementIds: usePreSettlementOnly ? resolvedPreSettlementIds : preSettlementIds,
preSettlementNo,
projectId, projectId,
projectName, projectName,
deptId, deptId,
@@ -53,7 +53,7 @@
<el-option <el-option
v-for="item in contracts" v-for="item in contracts"
:key="item.id" :key="item.id"
:label="formatContractOptionLabel(item)" :label="item.contractName || ''"
:value="item.id" :value="item.id"
/> />
</el-select> </el-select>
@@ -1070,7 +1070,6 @@ import {
import { getDictionary } from '@/api/system/dictbiz'; import { getDictionary } from '@/api/system/dictbiz';
import { h } from 'vue'; import { h } from 'vue';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { formatContractOptionLabel } from '@/utils/contract';
import { isTierUnitPrice, isUnitPriceAdjustable } from '@/utils/settlement-adjust-rule'; import { isTierUnitPrice, isUnitPriceAdjustable } from '@/utils/settlement-adjust-rule';
import { normalizeCurrencyCode } from '@/utils/currency'; import { normalizeCurrencyCode } from '@/utils/currency';
import { import {
@@ -1389,7 +1388,6 @@ export default {
methods: { methods: {
isTierUnitPrice, isTierUnitPrice,
isUnitPriceAdjustable, isUnitPriceAdjustable,
formatContractOptionLabel,
hasPermission(code) { hasPermission(code) {
if (this.isAdmin) return true; if (this.isAdmin) return true;
const action = String(code || '').replace( const action = String(code || '').replace(
@@ -251,29 +251,32 @@
:key="item.key" :key="item.key"
:label="item.kind === 'fee' ? item.name : item.column.label" :label="item.kind === 'fee' ? item.name : item.column.label"
:prop="item.kind === 'fee' ? undefined : item.column.prop" :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" align="center"
show-overflow-tooltip show-overflow-tooltip
> >
<template #default="{ row }"> <template #header>
<span v-if="item.kind === 'fee'" class="pre-settlement-editor__billing-rule-cell"> <span class="pre-settlement-editor__billing-rule-cell">
<span> <span>{{ item.kind === 'fee' ? item.name : item.column.label }}</span>
{{
formatMoney(
parseFeeItems(row.feeItemsJson)[item.name],
row.currency || form.currency
)
}}
</span>
<el-icon <el-icon
v-if="hasDetailBillingRule(row)" v-if="showDetailColumnRuleIcon(item)"
class="pre-settlement-editor__billing-rule-icon" class="pre-settlement-editor__billing-rule-icon"
title="查看计费规则详情" title="查看计费规则详情"
@click.stop="openDetailBillingRule(row, item.name)" @click.stop="openDetailColumnRule(item)"
> >
<InfoFilled /> <InfoFilled />
</el-icon> </el-icon>
</span> </span>
</template>
<template #default="{ row }">
<span v-if="item.kind === 'fee'">
{{
formatMoney(
parseFeeItems(row.feeItemsJson)[item.name],
row.currency || form.currency
)
}}
</span>
<el-link <el-link
v-else-if="item.column.link && row[item.column.prop] && !publicMode" v-else-if="item.column.link && row[item.column.prop] && !publicMode"
type="primary" type="primary"
@@ -284,19 +287,8 @@
<span v-else-if="item.column.money && item.column.prop === 'unitPrice'"> <span v-else-if="item.column.money && item.column.prop === 'unitPrice'">
{{ formatUnitPrice(row) }} {{ formatUnitPrice(row) }}
</span> </span>
<span <span v-else-if="item.column.prop === 'freightAmount'">
v-else-if="item.column.prop === 'freightAmount'" {{ formatMoney(row[item.column.prop], row.currency || form.currency) }}
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> </span>
<span v-else-if="item.column.money"> <span v-else-if="item.column.money">
{{ formatMoney(row[item.column.prop], row.currency || form.currency) }} {{ formatMoney(row[item.column.prop], row.currency || form.currency) }}
@@ -499,6 +491,11 @@
<span v-else>{{ displayValue(row[column.prop]) }}</span> <span v-else>{{ displayValue(row[column.prop]) }}</span>
</template> </template>
</el-table-column> </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> </el-table>
</section-card> </section-card>
</div> </div>
@@ -824,25 +821,28 @@
min-width="160" min-width="160"
align="center" align="center"
> >
<template #default="{ row }"> <template #header>
<div class="pre-settlement-editor__billing-rule-cell"> <span class="pre-settlement-editor__billing-rule-cell">
<el-input-number <span>{{ feeItem }}</span>
v-model="row.feeItems[feeItem]"
:min="0"
:precision="2"
:controls="false"
:disabled="adjustDialog.readonly"
@change="recalculateAdjustRow(row, feeItem)"
/>
<el-icon <el-icon
v-if="hasBillingRule(row)" v-if="showAdjustFeeRuleIcon(feeItem)"
class="pre-settlement-editor__billing-rule-icon" class="pre-settlement-editor__billing-rule-icon"
title="查看计费规则详情" title="查看计费规则详情"
@click="openBillingRuleInfo(row, feeItem)" @click.stop="openAdjustFeeRule(feeItem)"
> >
<InfoFilled /> <InfoFilled />
</el-icon> </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> </template>
</el-table-column> </el-table-column>
<el-table-column label="结算金额(含税)" min-width="170" align="center"> <el-table-column label="结算金额(含税)" min-width="170" align="center">
@@ -1522,7 +1522,9 @@ export default {
availableWriteOffAmount: Number(row.availableWriteOffAmount || 0), availableWriteOffAmount: Number(row.availableWriteOffAmount || 0),
createTime: row.paymentCreateTime || row.createTime, createTime: row.paymentCreateTime || row.createTime,
})); }));
this.changeRecords = detail.changeRecords || []; this.changeRecords = (detail.changeRecords || []).filter(
row => row.changeContent !== '预结算审批通过'
);
this.attachments = this.parseAttachments(detail.attachmentsJson); this.attachments = this.parseAttachments(detail.attachmentsJson);
this.sortAttachments(); this.sortAttachments();
this.selectedAttachmentRows = []; this.selectedAttachmentRows = [];
@@ -2307,6 +2309,17 @@ export default {
}, },
formatAdjustChangeValue(value) { formatAdjustChangeValue(value) {
if (value === undefined || value === null || value === '') return '空'; 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); if (typeof value === 'object') return JSON.stringify(value);
return String(value); return String(value);
}, },
@@ -2369,12 +2382,60 @@ export default {
ruleFeeName(rule = {}) { ruleFeeName(rule = {}) {
return String(rule.feeItem || rule.feeItemName || rule.name || '').trim(); return String(rule.feeItem || rule.feeItemName || rule.name || '').trim();
}, },
isHandlingFeeItem(name) {
return String(name || '').includes('装卸');
},
feeNamesMatch(left, right) { feeNamesMatch(left, right) {
const source = String(left || '').trim(); const source = String(left || '').trim();
const target = String(right || '').trim(); const target = String(right || '').trim();
if (!source || !target) return false; if (!source || !target) return false;
if (source === target) return true; 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 = {}) { collectBillingRules(row = {}) {
const source = row || {}; const source = row || {};
@@ -604,6 +604,38 @@ const dedupeColumns = columns => {
}); });
}; };
const settlementAmountColumn = {
prop: 'settlementAmount',
label: '结算金额',
editor: 'number',
minWidth: 120,
};
/** 模板已去掉结算金额;明细仍展示,并插在费用项之后、日期列之前 */
const ensureSettlementAmountColumn = columns => {
if (columns.some(column => column.prop === 'settlementAmount')) {
return columns;
}
const result = [...columns];
let insertAt = result.findIndex(column =>
['actualStartDate', 'actualEndDate', 'planStartDate', 'planEndDate', 'remark', 'waybillIdentifier'].includes(
column.prop
)
);
if (insertAt < 0) {
const lastFeeIndex = [...result].map((column, index) => (column.isFeeItem ? index : -1)).filter(index => index >= 0).pop();
const transportFeeIndex = result.findIndex(column => column.prop === 'transportFee');
insertAt =
lastFeeIndex !== undefined
? lastFeeIndex + 1
: transportFeeIndex >= 0
? transportFeeIndex + 1
: result.length;
}
result.splice(insertAt, 0, { ...settlementAmountColumn });
return result;
};
const detailColumns = computed(() => { const detailColumns = computed(() => {
const historyColumns = const historyColumns =
form.waybillMode === 'history' form.waybillMode === 'history'
@@ -619,16 +651,13 @@ const detailColumns = computed(() => {
const fallback = [ const fallback = [
...columnsBeforeFee, ...columnsBeforeFee,
...feeItemNames.value.map(name => feeColumn(name)), ...feeItemNames.value.map(name => feeColumn(name)),
{ { ...settlementAmountColumn },
prop: 'settlementAmount',
label: '结算金额',
editor: 'number',
minWidth: 120,
},
...columnsAfterFee, ...columnsAfterFee,
]; ];
const uploaded = parsedHeaders.value.map(columnFromHeader).filter(Boolean); const uploaded = parsedHeaders.value.map(columnFromHeader).filter(Boolean);
const columns = dedupeColumns(uploaded.length ? uploaded : fallback); const columns = ensureSettlementAmountColumn(
dedupeColumns(uploaded.length ? uploaded : fallback)
);
return [...historyColumns, ...columns]; return [...historyColumns, ...columns];
}); });
@@ -717,6 +746,7 @@ const formatCell = (row, column) => {
return value ?? ''; return value ?? '';
}; };
/** 导入解析后自动合计结算金额;编辑运输费/费用项时不再回算 */
const recalcSettlementAmount = row => { const recalcSettlementAmount = row => {
const transportFee = toNumber(row.transportFee ?? row.freight); const transportFee = toNumber(row.transportFee ?? row.freight);
row.transportFee = transportFee; row.transportFee = transportFee;
@@ -735,6 +765,18 @@ const recalcSettlementAmount = row => {
row.settlementAmount = settlementAmount; row.settlementAmount = settlementAmount;
row.otherFeeTotal = Number(feeSum.toFixed(2)); row.otherFeeTotal = Number(feeSum.toFixed(2));
row.freightTotal = settlementAmount; row.freightTotal = settlementAmount;
row['运费合计'] = settlementAmount;
};
const syncOtherFeeTotal = row => {
if (!row.feeItems || typeof row.feeItems !== 'object') {
row.feeItems = {};
}
let feeSum = 0;
feeItemNames.value.forEach(name => {
feeSum += toNumber(row.feeItems[name] ?? row[name]);
});
row.otherFeeTotal = Number(feeSum.toFixed(2));
}; };
const updateNumberCell = (row, column, value) => { const updateNumberCell = (row, column, value) => {
@@ -744,17 +786,16 @@ const updateNumberCell = (row, column, value) => {
if (!row.feeItems) row.feeItems = {}; if (!row.feeItems) row.feeItems = {};
row.feeItems[column.prop] = ''; row.feeItems[column.prop] = '';
row[column.prop] = ''; row[column.prop] = '';
} else { syncOtherFeeTotal(row);
row[column.prop] = column.editor === 'optionalNumber' ? '' : '';
if (column.prop === 'transportFee') row.freight = '';
if (column.prop === 'unitPrice') row.transportUnitPrice = '';
}
if (column.prop === 'settlementAmount') {
row.settlementAmount = '';
row.freightTotal = '';
return; return;
} }
recalcSettlementAmount(row); row[column.prop] = '';
if (column.prop === 'transportFee') row.freight = '';
if (column.prop === 'unitPrice') row.transportUnitPrice = '';
if (column.prop === 'settlementAmount') {
row.freightTotal = '';
row['运费合计'] = '';
}
return; return;
} }
const [integer = '', decimal = ''] = raw.replace(/[^\d.]/g, '').split('.'); const [integer = '', decimal = ''] = raw.replace(/[^\d.]/g, '').split('.');
@@ -764,21 +805,20 @@ const updateNumberCell = (row, column, value) => {
if (!row.feeItems) row.feeItems = {}; if (!row.feeItems) row.feeItems = {};
row.feeItems[column.prop] = normalized; row.feeItems[column.prop] = normalized;
row[column.prop] = normalized; row[column.prop] = normalized;
} else { syncOtherFeeTotal(row);
row[column.prop] = normalized; return;
if (column.prop === 'transportFee') { }
row.freight = normalized; row[column.prop] = normalized;
} if (column.prop === 'transportFee') {
if (column.prop === 'unitPrice') { row.freight = normalized;
row.transportUnitPrice = normalized; }
} if (column.prop === 'unitPrice') {
if (column.prop === 'settlementAmount') { row.transportUnitPrice = normalized;
row.freightTotal = normalized; }
row['运费合计'] = normalized; if (column.prop === 'settlementAmount') {
return; row.freightTotal = normalized;
} row['运费合计'] = normalized;
} }
recalcSettlementAmount(row);
}; };
const importFieldText = value => String(value ?? '').trim(); const importFieldText = value => String(value ?? '').trim();
@@ -854,11 +894,8 @@ const normalizeParsedRow = (item, index) => {
applyVehicleNo(row, vehicleNo); applyVehicleNo(row, vehicleNo);
row.transportUnitPrice = row.unitPrice; row.transportUnitPrice = row.unitPrice;
row.freight = row.transportFee; row.freight = row.transportFee;
if (row.settlementAmount === '' || row.settlementAmount === null || row.settlementAmount === undefined) { // 模板无结算金额列:导入后按运输费+费用项自动合计
recalcSettlementAmount(row); recalcSettlementAmount(row);
} else {
row.freightTotal = row.settlementAmount;
}
return row; return row;
}; };
@@ -1341,22 +1378,18 @@ const confirmImport = async () => {
const loadTransportTypeOptions = async () => { const loadTransportTypeOptions = async () => {
try { try {
// 运输方式取业务字典 transport_type 的字典名称(dictValue),不写死枚举
const response = await getDictionary({ code: 'transport_type' }); const response = await getDictionary({ code: 'transport_type' });
const list = unwrapData(response) || []; const list = unwrapData(response) || [];
transportTypeOptions.value = (Array.isArray(list) ? list : []) transportTypeOptions.value = (Array.isArray(list) ? list : [])
.map(item => { .map(item => {
const label = item.dictValue || item.label || item.name || ''; const label = String(item.dictValue || item.label || item.name || '').trim();
const value = item.dictValue || item.dictKey || item.value || label; if (!label || String(item.dictKey) === '-1') return null;
return label ? { label, value } : null; return { label, value: label };
}) })
.filter(Boolean); .filter(Boolean);
} catch (error) { } catch (error) {
transportTypeOptions.value = [ transportTypeOptions.value = [];
{ label: '公路运输', value: '公路运输' },
{ label: '铁路运输', value: '铁路运输' },
{ label: '水路运输', value: '水路运输' },
{ label: '航空运输', value: '航空运输' },
];
} }
}; };
@@ -241,7 +241,12 @@
<div <div
class="settlement-detail-page__detail-panel-body settlement-detail-page__adjust-panel-body" 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 type="index" label="序号" width="64" align="center" />
<el-table-column label="来源" width="110" align="center"> <el-table-column label="来源" width="110" align="center">
<template #default="{ row }">{{ feeSourceLabel(row.dataSource) }}</template> <template #default="{ row }">{{ feeSourceLabel(row.dataSource) }}</template>
@@ -1115,6 +1120,8 @@ export default {
page: { current: 1, size: 10, total: 0 }, page: { current: 1, size: 10, total: 0 },
// 底部详情/调整面板打开时压缩主表高度,露出表格滚动条 // 底部详情/调整面板打开时压缩主表高度,露出表格滚动条
listTableHeight: undefined, listTableHeight: undefined,
savedListTableHeight: null,
listTableLayoutToken: 0,
detailDialog: { detailDialog: {
visible: false, visible: false,
title: '费用明细', title: '费用明细',
@@ -1129,6 +1136,8 @@ export default {
rule: null, rule: null,
}, },
adjustDialog: { visible: false, loading: false, submitting: false, row: null }, adjustDialog: { visible: false, loading: false, submitting: false, row: null },
// 表头 + 最多 3 行费用 + 横向滚动条,超出后表体上下滚动
adjustTableMaxHeight: 40 + 49 * 3 + 16,
adjustRows: [], adjustRows: [],
adjustDynamicFeeColumns: [], adjustDynamicFeeColumns: [],
adjustTextProps: ['specification', 'model', 'billingFactor', 'billingType'], adjustTextProps: ['specification', 'model', 'billingFactor', 'billingType'],
@@ -1330,8 +1339,11 @@ export default {
'$route.fullPath'() { '$route.fullPath'() {
this.openRouteDetail(); this.openRouteDetail();
}, },
isBottomPanelOpen() { isBottomPanelOpen: {
this.updateListTableHeight(); flush: 'post',
handler() {
this.updateListTableHeight();
},
}, },
searchVisible() { searchVisible() {
this.updateListTableHeight(); this.updateListTableHeight();
@@ -1396,36 +1408,62 @@ export default {
}, },
/** 表格和分页撑满剩余可视区域;单据详情展开时贴住详情面板,关闭后恢复 */ /** 表格和分页撑满剩余可视区域;单据详情展开时贴住详情面板,关闭后恢复 */
updateListTableHeight() { updateListTableHeight() {
this.listTableLayoutToken = (this.listTableLayoutToken || 0) + 1;
const token = this.listTableLayoutToken;
this.$nextTick(() => { 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; const tableElement = this.$refs.mainTableRef?.$el;
if (!tableElement) return; 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 paginationHeight = pagination?.offsetHeight || 0;
const tableTop = tableElement.getBoundingClientRect().top; const tableTop = tableElement.getBoundingClientRect().top;
const view = this.$el?.closest?.('#avue-view'); const view = this.$el?.closest?.('#avue-view');
const viewRect = view?.getBoundingClientRect?.(); 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 const panelRect = document
.querySelector('.settlement-detail-page__detail-panel') .querySelector('.settlement-detail-page__detail-panel')
?.getBoundingClientRect?.(); ?.getBoundingClientRect?.();
if (panelRect && panelRect.height > 0) { if (panelRect && panelRect.height > 0) {
bottomEdge = panelRect.top; bottomEdge = Math.min(bottomEdge, panelRect.top);
this.listTableHeightRetry = 0; this.listTableHeightRetry = 0;
} else if ((this.listTableHeightRetry || 0) < 5) { } else if ((this.listTableHeightRetry || 0) < 5) {
this.listTableHeightRetry = (this.listTableHeightRetry || 0) + 1; this.listTableHeightRetry = (this.listTableHeightRetry || 0) + 1;
window.requestAnimationFrame(() => this.applyListTableHeight()); window.requestAnimationFrame(() => this.applyListTableHeight(token));
return; return;
} }
} else { } else {
this.listTableHeightRetry = 0; this.listTableHeightRetry = 0;
} }
const gap = this.isBottomPanelOpen ? 8 : 0; const gap = panelOpen ? 8 : 0;
this.listTableHeight = Math.max(180, Math.floor(bottomEdge - tableTop - paginationHeight - gap)); 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?.()); this.$nextTick(() => this.$refs.mainTableRef?.doLayout?.());
}, },
contractCategoryName(value) { contractCategoryName(value) {
@@ -1858,11 +1896,13 @@ export default {
this.changeRecordsDialog.visible = false; this.changeRecordsDialog.visible = false;
this.billingRuleDialog.visible = false; this.billingRuleDialog.visible = false;
this.billingRuleDialog.rule = null; this.billingRuleDialog.rule = null;
this.updateListTableHeight();
}, },
closeAdjustPanel() { closeAdjustPanel() {
this.clearAdjustCalculations(); this.clearAdjustCalculations();
this.adjustDialog.visible = false; this.adjustDialog.visible = false;
this.adjustDialog.row = null; this.adjustDialog.row = null;
this.updateListTableHeight();
}, },
handleTableLink(row, column) { handleTableLink(row, column) {
if (column.prop === 'waybillNo') { if (column.prop === 'waybillNo') {
@@ -2446,7 +2486,8 @@ export default {
return; return;
} }
const options = this.syncBillingPlanOptions(contractId); 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 || ''; this.updateFeeForm.billingPlanId = selected?.id || '';
}, },
async confirmUpdateFee() { async confirmUpdateFee() {
@@ -3333,6 +3374,7 @@ export default {
.settlement-detail-page__adjust-panel-body { .settlement-detail-page__adjust-panel-body {
padding-top: 16px; padding-top: 16px;
overflow: hidden;
} }
.settlement-detail-page__billing-rule-cell { .settlement-detail-page__billing-rule-cell {