1、调整配载、总单

2、新增收付款模块
This commit is contained in:
2026-08-22 21:11:48 +08:00
parent 1962bee882
commit 5fcb82c0c0
51 changed files with 11663 additions and 1506 deletions
@@ -1,86 +1,82 @@
<template>
<el-dialog
v-model="visible"
:title="dialogTitle"
width="96%"
top="2vh"
append-to-body
destroy-on-close
class="pre-settlement-editor"
<component
:is="editorContainer"
v-bind="editorContainerProps"
@update:model-value="visible = $event"
@closed="handleClosed"
>
<div v-loading="loading" class="pre-settlement-editor__content">
<section-card title="结算基本信息">
<el-form
ref="formRef"
:model="form"
:rules="formRules"
label-position="right"
label-width="auto"
class="pre-settlement-editor__form"
>
<el-row :gutter="24">
<el-col v-for="field in formFields" :key="field.prop" :span="field.span || 8">
<el-form-item :label="field.label" :prop="field.prop">
<el-select
v-if="field.type === 'contract'"
v-model="form.contractId"
filterable
remote
clearable
:remote-method="loadContractOptions"
:loading="contractLoading"
:disabled="!editable || details.length > 0"
placeholder="请选择合同"
@change="handleContractChange"
>
<el-option
v-for="item in contractOptions"
:key="item.id"
:label="`${item.contractName}${item.contractNo || '-'}`"
:value="item.id"
ref="formRef"
:model="form"
:rules="formRules"
label-position="right"
label-width="auto"
class="pre-settlement-editor__form"
>
<el-row :gutter="24">
<el-col v-for="field in formFields" :key="field.prop" :span="field.span || 8">
<el-form-item :label="field.label" :prop="field.prop">
<el-select
v-if="field.type === 'contract'"
v-model="form.contractId"
filterable
remote
clearable
:remote-method="loadContractOptions"
:loading="contractLoading"
:disabled="!editable || details.length > 0"
placeholder="请选择合同"
@change="handleContractChange"
>
<el-option
v-for="item in contractOptions"
:key="item.id"
:label="`${item.contractName}${item.contractNo || '-'}`"
:value="item.id"
/>
</el-select>
<el-date-picker
v-else-if="field.type === 'date'"
v-model="form[field.prop]"
type="date"
value-format="YYYY-MM-DD"
format="YYYY-MM-DD"
:disabled="!editable || form.currency === 'RMB'"
placeholder="请选择"
@change="recalculateLocalAmount"
/>
</el-select>
<el-date-picker
v-else-if="field.type === 'date'"
v-model="form[field.prop]"
type="date"
value-format="YYYY-MM-DD"
format="YYYY-MM-DD"
:disabled="!editable || form.currency === 'RMB'"
placeholder="请选择"
@change="recalculateLocalAmount"
/>
<el-input-number
v-else-if="field.type === 'number'"
v-model="form[field.prop]"
:min="0"
:precision="6"
:controls="false"
:disabled="!editable || form.currency === 'RMB'"
@change="recalculateLocalAmount"
/>
<el-input
v-else-if="field.type === 'textarea'"
v-model="form[field.prop]"
type="textarea"
:rows="2"
:maxlength="field.maxlength"
show-word-limit
:disabled="!editable"
placeholder="请输入"
/>
<span v-else-if="field.prop === 'settlementType'" class="form-readonly">
{{ settlementTypeName }}
</span>
<span v-else-if="field.money" class="form-readonly">
{{ formatMoney(form[field.prop], moneyCurrency(field.prop)) }}
</span>
<span v-else class="form-readonly">{{ displayValue(form[field.prop]) }}</span>
</el-form-item>
</el-col>
</el-row>
</el-form>
<el-input-number
v-else-if="field.type === 'number'"
v-model="form[field.prop]"
:min="0"
:precision="6"
:controls="false"
:disabled="!editable || form.currency === 'RMB'"
@change="recalculateLocalAmount"
/>
<el-input
v-else-if="field.type === 'textarea'"
v-model="form[field.prop]"
type="textarea"
:rows="2"
:maxlength="field.maxlength"
show-word-limit
:disabled="!editable"
placeholder="请输入"
/>
<span v-else-if="field.prop === 'settlementType'" class="form-readonly">
{{ settlementTypeName }}
</span>
<span v-else-if="field.money" class="form-readonly">
{{ formatMoney(form[field.prop], moneyCurrency(field.prop)) }}
</span>
<span v-else class="form-readonly">{{ displayValue(form[field.prop]) }}</span>
</el-form-item>
</el-col>
</el-row>
</el-form>
</section-card>
<section-card title="结算合计">
@@ -130,7 +126,6 @@
<el-input-number
v-else-if="editable && column.prop === 'adjustAmount'"
v-model="row.adjustAmount"
:min="row.manualFlag === 1 ? 0 : undefined"
:precision="2"
:controls="false"
@change="recalculateSummaryRow(row)"
@@ -231,7 +226,7 @@
<el-table :data="filteredDetails" border class="pre-settlement-editor__detail-table">
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
<el-table-column
v-for="column in detailColumns"
v-for="column in visibleDetailColumns"
:key="column.prop"
:label="column.label"
:prop="column.prop"
@@ -253,6 +248,12 @@
<span v-else-if="column.prop === 'transportQuantityText'">
{{ formatQuantity(row) }}
</span>
<span v-else-if="pageMode && column.prop === 'mileage'">
{{ formatDetailMileage(row[column.prop]) }}
</span>
<span v-else-if="pageMode && column.prop === 'transportType'">
{{ transportTypeName(row[column.prop]) }}
</span>
<span v-else>{{ displayValue(row[column.prop]) }}</span>
</template>
</el-table-column>
@@ -265,7 +266,10 @@
>
<template #default="{ row }">
{{
formatMoney(parseFeeItems(row.feeItemsJson)[feeItem], row.currency || form.currency)
formatMoney(
parseFeeItems(row.feeItemsJson)[feeItem],
row.currency || form.currency
)
}}
</template>
</el-table-column>
@@ -301,19 +305,11 @@
button-text="上传附件"
@change="handleAttachmentChange"
/>
<el-button
type="primary"
:disabled="!attachments.length"
@click="downloadAttachments"
>
<el-button type="primary" :disabled="!attachments.length" @click="downloadAttachments">
批量下载
</el-button>
</div>
<el-table
:data="attachments"
border
@selection-change="handleAttachmentSelectionChange"
>
<el-table :data="attachments" border @selection-change="handleAttachmentSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column label="文件名" min-width="260" show-overflow-tooltip>
@@ -327,7 +323,13 @@
<template #default="{ row }">{{ formatFileSize(row.size) }}</template>
</el-table-column>
<el-table-column prop="uploadUserName" label="上传人" width="140" align="center" />
<el-table-column prop="uploadTime" label="上传时间" width="170" align="center" sortable />
<el-table-column
prop="uploadTime"
label="上传时间"
width="170"
align="center"
sortable
/>
<el-table-column label="操作" :width="editable ? 150 : 80" fixed="right" align="center">
<template #default="{ row, $index }">
<div class="pre-settlement-editor__links">
@@ -388,7 +390,7 @@
</section-card>
</div>
<template #footer>
<template v-if="!pageMode" #footer>
<el-button @click="visible = false">取消</el-button>
<el-button v-if="editable" :loading="saving" @click="saveDraft(false)">保存</el-button>
<el-button
@@ -400,7 +402,19 @@
提交
</el-button>
</template>
</el-dialog>
<div v-if="pageMode" class="pre-settlement-editor__page-actions">
<el-button @click="visible = false">取消</el-button>
<el-button v-if="editable" :loading="saving" @click="saveDraft(false)">保存</el-button>
<el-button
v-if="editable && hasPermission('pre_settlement_submit')"
type="primary"
:loading="submitting"
@click="saveDraft(true)"
>
提交
</el-button>
</div>
</component>
<el-dialog
v-model="candidateDialog.visible"
@@ -558,17 +572,6 @@
/>
</template>
</el-table-column>
<el-table-column label="结算金额(不含税)" min-width="180" align="center">
<template #default="{ row }">
<el-input-number
v-model="row.settlementAmountNoTax"
:min="0"
:precision="2"
:controls="false"
:disabled="adjustDialog.readonly"
/>
</template>
</el-table-column>
<el-table-column label="备注" min-width="200" align="center">
<template #default="{ row }">
<el-input v-model="row.remark" maxlength="200" :disabled="adjustDialog.readonly" />
@@ -644,6 +647,14 @@ export default {
type: Boolean,
default: false,
},
pageMode: {
type: Boolean,
default: false,
},
initialData: {
type: Object,
default: null,
},
},
emits: ['update:modelValue', 'success'],
data() {
@@ -663,6 +674,7 @@ export default {
contractOptions: [],
feeOptions: [],
feeCategoryOptions: [],
transportTypeOptions: [],
summaryFees: [],
details: [],
detailCollapsed: false,
@@ -735,6 +747,25 @@ export default {
this.$emit('update:modelValue', value);
},
},
editorContainer() {
return this.pageMode ? 'div' : 'el-dialog';
},
editorContainerProps() {
if (this.pageMode) {
return {
class: ['pre-settlement-editor', 'pre-settlement-editor--page'],
};
}
return {
modelValue: this.visible,
title: this.dialogTitle,
width: '96%',
top: '2vh',
appendToBody: true,
destroyOnClose: true,
class: 'pre-settlement-editor',
};
},
editable() {
return !this.readonly && ['draft', 'returned'].includes(this.form.approvalStatus);
},
@@ -755,7 +786,28 @@ export default {
return this.form.settlementType === 'receivable' ? '应收' : '应付';
},
summaryTotal() {
return this.summaryFees.reduce((total, row) => total + Number(row.settlementAmount || 0), 0);
if (this.summaryFees.length) {
return this.summaryFees.reduce(
(total, row) => total + Number(row.settlementAmount || 0),
0
);
}
return this.details.reduce(
(total, row) =>
total +
Number(
row.settlementAmountTax ??
row.totalAmount ??
row.afterAmount ??
row.settlementAmount ??
0
),
0
);
},
visibleDetailColumns() {
if (!this.pageMode) return this.detailColumns;
return this.detailColumns.filter(column => column.prop !== 'settlementAmountNoTax');
},
detailFeeItemNames() {
const names = new Set();
@@ -783,8 +835,11 @@ export default {
},
},
watch: {
modelValue(value) {
if (value) this.initialize();
modelValue: {
immediate: true,
handler(value) {
if (value) this.initialize();
},
},
summaryTotal(value) {
this.form.settlementAmount = Number(value || 0).toFixed(2);
@@ -800,7 +855,53 @@ export default {
await this.loadContractOptions('');
await this.loadFeeOptions();
await this.loadFeeCategoryOptions();
if (this.pageMode) await this.loadTransportTypeOptions();
if (this.recordId) await this.loadDetail();
else if (this.initialData) this.applyInitialData();
},
applyInitialData() {
const rows = Array.isArray(this.initialData?.rows) ? this.initialData.rows : [];
if (!rows.length) return;
const first = rows[0];
const settlementType = this.initialData.settlementType || first.settlementType || 'payable';
const contractId = first.contractId || '';
if (!this.contractOptions.some(item => String(item.id) === String(contractId))) {
this.contractOptions.push({
id: contractId,
contractNo: first.contractNo,
contractName: first.contractName,
projectId: first.projectId,
projectName: first.projectName,
deptId: first.deptId,
deptName: first.deptName,
settlementType,
partyA: settlementType === 'receivable' ? first.payeeName : first.payerName,
partyB: settlementType === 'receivable' ? first.payerName : first.payeeName,
});
}
this.handleContractChange(contractId);
Object.assign(this.form, {
contractId,
contractNo: first.contractNo || this.form.contractNo,
contractName: first.contractName || this.form.contractName,
projectId: first.projectId || this.form.projectId,
projectName: first.projectName || this.form.projectName,
deptId: first.deptId || this.form.deptId,
deptName: first.deptName || this.form.deptName,
payerName: first.payerName || this.form.payerName,
payeeName: first.payeeName || this.form.payeeName,
settlementType,
currency: first.currency || 'RMB',
localCurrency: first.localCurrency || 'RMB',
});
this.details = rows.map(row => ({
...row,
sourceDetailId: row.sourceDetailId || row.id,
settlementAmountTax:
row.settlementAmountTax ?? row.totalAmount ?? row.afterAmount ?? row.settlementAmount,
feeItemsJson: row.feeItemsJson || JSON.stringify(row.feeItems || {}),
}));
this.buildSummaryFeesFromDetails();
},
resetEditor() {
this.form = emptyPreSettlementForm();
@@ -869,6 +970,10 @@ export default {
const { data } = await getDictionary({ code: 'fee_category' });
this.feeCategoryOptions = data?.data || [];
},
async loadTransportTypeOptions() {
const { data } = await getDictionary({ code: 'transport_type' });
this.transportTypeOptions = data?.data || [];
},
handleContractChange(id) {
const contract = this.contractOptions.find(item => String(item.id) === String(id));
if (!contract) {
@@ -914,12 +1019,12 @@ export default {
await submit({ id: this.form.id });
this.$message.success('审批流程已发起');
this.visible = false;
this.$emit('success');
this.$emit('success', this.form.id);
return;
}
this.$message.success('保存成功');
await this.loadDetail();
this.$emit('success');
this.$emit('success', this.form.id);
} finally {
this[stateKey] = false;
}
@@ -968,6 +1073,17 @@ export default {
);
return option?.dictValue || value;
},
transportTypeName(value) {
if (value === undefined || value === null || value === '') return '';
const option = this.transportTypeOptions.find(
item => String(item.dictKey) === String(value) || String(item.dictValue) === String(value)
);
return option?.dictValue || value;
},
formatDetailMileage(value) {
if (value === undefined || value === null || value === '' || Number(value) === -1) return '';
return value;
},
removeSummaryFee(index) {
this.summaryFees.splice(index, 1);
},
@@ -979,6 +1095,75 @@ export default {
this.$message.warning('结算金额不能小于0');
}
},
buildSummaryFeesFromDetails() {
const summaryMap = new Map();
const defaultFreightItem =
this.feeOptions
.flatMap(item => item.feeItems || [])
.find(name => this.isFreightFeeItem(name)) || '运输费';
const appendSummary = (feeItem, value, fallbackFeeType = '') => {
const amount = Number(value || 0);
if (!feeItem || !Number.isFinite(amount) || Math.abs(amount) < 0.005) return;
const feeOption = this.feeOptions.find(item =>
(item.feeItems || []).some(name => String(name) === String(feeItem))
);
const feeType = feeOption?.feeType || fallbackFeeType || '';
const key = `${feeType}\u0000${feeItem}`;
const current = summaryMap.get(key) || {
id: '',
feeType,
feeItem,
originalAmount: 0,
adjustAmount: 0,
settlementAmount: 0,
remark: '',
manualFlag: 0,
};
current.originalAmount = Number((current.originalAmount + amount).toFixed(2));
current.settlementAmount = current.originalAmount;
summaryMap.set(key, current);
};
this.details.forEach(row => {
const feeItems = this.parseFeeItems(row.feeItemsJson || row.feeItems);
const feeItemEntries = Object.entries(feeItems);
feeItemEntries.forEach(([feeItem, amount]) =>
appendSummary(feeItem, amount, row.feeType)
);
let knownAmount = feeItemEntries.reduce(
(total, [, amount]) => total + Number(amount || 0),
0
);
if (!feeItemEntries.some(([feeItem]) => this.isFreightFeeItem(feeItem))) {
const freightAmount = Number(row.freightAmount || 0);
appendSummary(defaultFreightItem, freightAmount, row.feeType);
knownAmount += freightAmount;
}
if (!feeItemEntries.some(([feeItem]) => !this.isFreightFeeItem(feeItem))) {
const otherFeeAmount = Number(row.otherFeeAmount || 0);
appendSummary('其他费用', otherFeeAmount, row.feeType);
knownAmount += otherFeeAmount;
}
const totalAmount = Number(
row.settlementAmountTax ??
row.totalAmount ??
row.afterAmount ??
row.settlementAmount ??
0
);
const residualAmount = Number((totalAmount - knownAmount).toFixed(2));
if (residualAmount > 0) appendSummary('其他费用', residualAmount, row.feeType);
});
const manualFees = this.summaryFees.filter(row => row.manualFlag === 1);
this.summaryFees = [...summaryMap.values(), ...manualFees];
this.form.settlementAmount = this.summaryFees
.reduce((total, row) => total + Number(row.settlementAmount || 0), 0)
.toFixed(2);
this.recalculateLocalAmount();
},
recalculateLocalAmount() {
const rate = this.form.currency === 'RMB' ? 1 : Number(this.form.exchangeRate || 0);
this.form.localSettlementAmount = (Number(this.form.settlementAmount || 0) * rate).toFixed(2);
@@ -1054,12 +1239,13 @@ export default {
await this.$confirm('确认将该明细踢出预结算单?', '提示', { type: 'warning' });
if (!this.form.id || !row.id || row.id === row.sourceDetailId) {
this.details = this.details.filter(item => item !== row);
this.buildSummaryFeesFromDetails();
return;
}
await removeDetail(this.form.id, row.id);
this.$message.success('已移除');
await this.loadDetail();
this.$emit('success');
this.$emit('success', this.form.id);
},
handleDetailQuery() {
this.appliedDetailQuery = { ...this.detailQuery };
@@ -1138,7 +1324,7 @@ export default {
this.adjustDialog.visible = false;
this.$message.success('结算明细调整成功');
await this.loadDetail();
this.$emit('success');
this.$emit('success', this.form.id);
} finally {
this.adjustDialog.saving = false;
}
@@ -1313,6 +1499,19 @@ export default {
overflow-y: auto;
}
&--page &__content {
max-height: none;
overflow: visible;
}
&__page-actions {
display: flex;
justify-content: flex-end;
padding: 16px 0 4px;
border-top: 1px solid #eff1f7;
gap: 8px;
}
&__form {
:deep(.el-select),
:deep(.el-date-editor),