1.1.30 分款明细新增加合同信息列显示优化

dev
yinq 2 days ago
parent 3fe3d9ad58
commit d0d0e5050e

@ -59,6 +59,15 @@ export interface FinAccountInstallmentDetailVO {
*/
remark: string;
/** 合同总价(列表/详情 join erp_contract_info */
contractTotalPrice?: number;
/** 当前节点支付比例 %join erp_contract_payment_method */
contractPaymentPercentage?: number;
/** 当前节点合同约定支付金额join erp_contract_payment_method */
contractPaymentAmount?: number;
}
export interface FinAccountInstallmentDetailForm extends BaseEntity {
@ -117,11 +126,25 @@ export interface FinAccountInstallmentDetailForm extends BaseEntity {
*/
detailAmount?: number;
/**
* installment_status
*/
installmentStatus?: string;
/**
*
*/
remark?: string;
/** 仅展示/校验:合同总价(来自 join勿依赖提交 */
contractTotalPrice?: number;
/** 仅展示/校验:节点支付比例 % */
contractPaymentPercentage?: number;
/** 仅展示/校验:节点合同约定支付金额 */
contractPaymentAmount?: number;
}
export interface FinAccountInstallmentDetailQuery extends PageQuery {

@ -147,12 +147,12 @@
<el-table-column label="项目名称" align="center" prop="projectName" width="150" show-overflow-tooltip />
<el-table-column label="合同编号" align="center" prop="contractCode" width="120" show-overflow-tooltip />
<el-table-column label="合同名称" align="center" prop="contractName" width="150" show-overflow-tooltip />
<el-table-column label="分款阶段" align="center" prop="stageName" width="100" />
<el-table-column label="分款金额" align="center" width="100">
<template #default="{ row }">
<span class="font-medium">¥{{ formatMoney(row.detailAmount) }}</span>
</template>
</el-table-column>
<el-table-column label="分款阶段" align="center" prop="stageName" width="100" />
<el-table-column label="备注" align="center" prop="remark" show-overflow-tooltip />
<el-table-column label="操作" align="center" width="100" fixed="right">
<template #default="{ row }">
@ -236,8 +236,8 @@
<ContractSelectDialog v-model:visible="showContractDialog" @contract-selected="handleContractSelected" />
<!-- 分款明细表单对话框 -->
<el-dialog :title="detailDialog.title" v-model="detailDialog.visible" width="500px" append-to-body>
<el-form ref="detailFormRef" :model="detailForm" :rules="detailRules" label-width="100px">
<el-dialog :title="detailDialog.title" v-model="detailDialog.visible" width="560px" append-to-body>
<el-form ref="detailFormRef" :model="detailForm" :rules="detailRules" label-width="120px">
<el-form-item label="项目编号">
<el-input v-model="detailForm.projectCode" disabled />
</el-form-item>
@ -250,10 +250,23 @@
<el-form-item label="合同名称">
<el-input v-model="detailForm.contractName" disabled />
</el-form-item>
<el-form-item label="合同额">
<span class="font-medium text-gray-800">{{ contractTotalPriceDisplay }}</span>
</el-form-item>
<el-form-item label="分款节点" prop="paymentStageId">
<el-select v-model="detailForm.paymentStageId" placeholder="请选择分款节点" style="width: 100%">
<div style="display: flex; flex-flow: row nowrap; align-items: center; gap: 12px; width: 100%; min-width: 0; overflow-x: auto">
<el-select v-model="detailForm.paymentStageId" placeholder="请选择分款节点" style="width: 150px; flex-shrink: 0">
<el-option v-for="stage in paymentStageList" :key="stage.paymentStageId" :label="stage.stageName" :value="stage.paymentStageId" />
</el-select>
<template v-if="detailForm.paymentStageId">
<span style="white-space: nowrap; flex-shrink: 0">
回款比例 <span>{{ selectedStagePaymentRatioText }}</span>
</span>
<span style="white-space: nowrap; flex-shrink: 0">
金额 <span>¥{{ formatMoneyOrDash(selectedStageContractShareAmount) }}</span>
</span>
</template>
</div>
</el-form-item>
<el-form-item label="分款金额" prop="detailAmount">
<el-input-number
@ -264,7 +277,6 @@
placeholder="请输入分款金额"
style="width: 100%"
controls-position="right"
@change="handleDetailAmountChange"
/>
</el-form-item>
<el-form-item label="备注">
@ -297,10 +309,9 @@
</template>
<script setup name="FinAccountInstallment" lang="ts">
import { importFinAccountInstallment, listFinAccountInstallment } from '@/api/oa/erp/finAccountInstallment';
import { listFinAccountInstallment } from '@/api/oa/erp/finAccountInstallment';
import {
addFinAccountInstallmentDetail,
delFinAccountInstallmentDetail,
getErpFinAccountInstallmentDetailList,
updateFinAccountInstallmentDetail,
deleteInstallmentDetailByBo
@ -308,10 +319,8 @@ import {
import { FinAccountInstallmentQuery, FinAccountInstallmentVO } from '@/api/oa/erp/finAccountInstallment/types';
import { FinAccountInstallmentDetailForm, FinAccountInstallmentDetailVO } from '@/api/oa/erp/finAccountInstallmentDetail/types';
import { getErpContractPaymentMethodJoinList } from '@/api/oa/erp/finAccountReceivable';
import { PaymentStageVO } from '@/api/oa/base/paymentStage/types';
import { getToken } from '@/utils/auth';
import { DArrowLeft, UploadFilled } from '@element-plus/icons-vue';
import { DArrowLeft } from '@element-plus/icons-vue';
import ProjectSelectDialog from '@/views/oa/components/ProjectSelectDialog.vue';
import ContractSelectDialog from '@/views/oa/components/ContractSelectDialog.vue';
import { globalHeaders } from '@/utils/request';
@ -366,12 +375,10 @@ const formRemainingAmount = computed(() => {
});
//
const showImportDialog = ref(false);
const uploadRef = ref();
const importLoading = ref(false);
const importPreviewData = ref<any[]>([]);
const duplicateCount = ref(0);
const uploadHeaders = ref({ Authorization: 'Bearer ' + getToken() });
const importFile = ref<File | null>(null);
//
@ -379,26 +386,84 @@ const showProjectDialog = ref(false);
const showContractDialog = ref(false);
const selectedProject = ref<any>(null);
const paymentStageList = ref<PaymentStageVO[]>([]);
/** 新增时查询应收款节点信息列表 */
/** 合同付款节点 join 列表(含支付比例、节点约定金额等) */
interface ContractPaymentStageJoinRow {
paymentStageId: string | number;
stageName?: string;
paymentPercentage?: number | string;
paymentAmount?: number | string;
}
const paymentStageList = ref<ContractPaymentStageJoinRow[]>([]);
/** 当前表单关联合同的合同额(总价),用于展示与节点金额推算 */
const detailFormContractTotalPrice = ref<number | undefined>(undefined);
const formatMoney = (money: number | undefined | null) => {
if (money === undefined || money === null) return '0.00';
return money.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
};
const formatMoneyOrDash = (money: number | undefined | null) => {
if (money === undefined || money === null || Number.isNaN(Number(money))) return '—';
return formatMoney(money);
};
const contractTotalPriceDisplay = computed(() => {
const v = detailFormContractTotalPrice.value;
if (v === undefined || v === null || Number.isNaN(Number(v))) return '—';
return `¥${formatMoney(v)}`;
});
const selectedPaymentStageRow = computed(() => {
const id = detailForm.paymentStageId;
if (id == null || id === '') return null;
return paymentStageList.value.find((s) => String(s.paymentStageId) === String(id)) ?? null;
});
const selectedStagePaymentRatioText = computed(() => {
const row = selectedPaymentStageRow.value;
if (row && row.paymentPercentage !== undefined && row.paymentPercentage !== null && row.paymentPercentage !== '') {
const n = Number(row.paymentPercentage);
if (!Number.isNaN(n)) return `${n}%`;
}
const p = detailForm.contractPaymentPercentage;
if (p !== undefined && p !== null && !Number.isNaN(Number(p))) {
const n = Number(p);
if (!Number.isNaN(n)) return `${n}%`;
}
return '—';
});
/** 当前节点约定回款金额:优先付款节点列表;否则用明细 join 带回的合同节点金额/比例推算 */
const selectedStageContractShareAmount = computed(() => {
const row = selectedPaymentStageRow.value;
if (row) {
const backend = row.paymentAmount !== undefined && row.paymentAmount !== null && row.paymentAmount !== '' ? Number(row.paymentAmount) : NaN;
if (!Number.isNaN(backend)) return backend;
const total = Number(detailFormContractTotalPrice.value);
const pct = Number(row.paymentPercentage);
if (!Number.isNaN(total) && !Number.isNaN(pct)) return (total * pct) / 100;
}
const dAmt = detailForm.contractPaymentAmount;
if (dAmt !== undefined && dAmt !== null && !Number.isNaN(Number(dAmt))) {
return Number(dAmt);
}
const total = Number(detailFormContractTotalPrice.value);
const pct = Number(detailForm.contractPaymentPercentage);
if (!Number.isNaN(total) && !Number.isNaN(pct)) return (total * pct) / 100;
return undefined;
});
/** 新增时查询合同付款节点列表 */
const getPaymentStages = async (contractId) => {
paymentStageList.value = [];
if (contractId && contractId !== '') {
const res = await getErpContractPaymentMethodJoinList({ contractId: contractId });
console.log(res.data);
paymentStageList.value = res.data;
paymentStageList.value = (res.data || []) as ContractPaymentStageJoinRow[];
}
};
// Mock
// const paymentStageList = ref([
// { id: 1, stageName: '', stageOrder: 1 },
// { id: 2, stageName: '', stageOrder: 2 },
// { id: 3, stageName: '', stageOrder: 3 },
// { id: 4, stageName: '', stageOrder: 4 },
// { id: 5, stageName: '', stageOrder: 5 }
// ]);
//
const detailDialog = reactive({
visible: false,
@ -417,19 +482,41 @@ const initDetailForm: FinAccountInstallmentDetailForm = {
contractName: undefined,
paymentStageId: undefined,
detailAmount: undefined,
remark: undefined
installmentStatus: undefined,
remark: undefined,
contractTotalPrice: undefined,
contractPaymentPercentage: undefined,
contractPaymentAmount: undefined
};
const detailForm = reactive<FinAccountInstallmentDetailForm>({ ...initDetailForm });
function validateDetailAmountNotExceedNode(_rule: unknown, value: unknown, callback: (e?: Error) => void) {
const max = selectedStageContractShareAmount.value;
if (max == null || Number.isNaN(Number(max))) {
callback();
return;
}
const maxN = Number(max);
const v = Number(value);
if (value === undefined || value === null || value === '' || Number.isNaN(v)) {
callback();
return;
}
const vCents = Math.round(v * 100);
const maxCents = Math.round(maxN * 100);
if (vCents > maxCents) {
callback(new Error(`分款金额不能大于节点约定金额(¥${formatMoney(maxN)}`));
return;
}
callback();
}
const detailRules = {
paymentStageId: [{ required: true, message: '请选择分款节点', trigger: 'change' }],
detailAmount: [{ required: true, message: '请输入分款金额', trigger: 'blur' }]
};
//
const formatMoney = (money: number | undefined | null) => {
if (money === undefined || money === null) return '0.00';
return money.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
detailAmount: [
{ required: true, message: '请输入分款金额', trigger: 'blur' },
{ validator: validateDetailAmountNotExceedNode, trigger: ['blur', 'change'] }
]
};
const INSTALLMENT_STATUS = {
@ -438,26 +525,21 @@ const INSTALLMENT_STATUS = {
COMPLETE: '2' //
};
//
const getStatusType = (status: string | undefined) => {
if (!status) return 'info';
const map: Record<string, string> = {
'0': 'warning', // -
'1': 'primary', // -
'2': 'success' // - 绿
};
return map[status] || 'info';
};
//
const getStatusText = (status: string | undefined) => {
if (!status) return '未知';
const map: Record<string, string> = {
'0': '未分款',
'1': '部分分款',
'2': '已分款完成'
};
return map[status] || '未知';
/** 明细列表刷新后,按剩余回款与已分款情况同步左侧选中行及列表中的分款状态 */
const syncSelectedInstallmentStatusWithDetailList = () => {
if (!selectedInstallment.value) return;
const newStatus =
remainingAmount.value === 0
? INSTALLMENT_STATUS.COMPLETE
: allocatedAmount.value > 0
? INSTALLMENT_STATUS.PARTIALLY
: INSTALLMENT_STATUS.NOT;
selectedInstallment.value.installmentStatus = newStatus;
const id = selectedInstallment.value.accountInstallmentId;
const index = finAccountInstallmentList.value.findIndex((item) => item.accountInstallmentId === id);
if (index !== -1) {
finAccountInstallmentList.value[index].installmentStatus = newStatus;
}
};
/*** 用户导入参数 */
@ -573,8 +655,6 @@ const handleAddDetail = () => {
//
const handleProjectSelected = (data: any) => {
selectedProject.value = data.project;
console.log(data);
// (contractFlag !== '1')
if (data.project.contractFlag !== '1') {
showContractDialog.value = true;
@ -593,8 +673,11 @@ const handleContractSelected = (contract: any) => {
}
};
//
// syncAddDetailAmountFromSelectedStage
const fillDetailForm = (project: any, contract: any) => {
const totalPriceRaw = contract?.totalPrice;
detailFormContractTotalPrice.value =
totalPriceRaw !== undefined && totalPriceRaw !== null && totalPriceRaw !== '' ? Number(totalPriceRaw) : undefined;
Object.assign(detailForm, {
installmentDetailId: undefined,
accountInstallmentId: selectedInstallment.value?.accountInstallmentId,
@ -606,23 +689,45 @@ const fillDetailForm = (project: any, contract: any) => {
contractName: contract?.contractName || '',
paymentStageId: undefined,
detailAmount: undefined,
remark: undefined
remark: undefined,
contractTotalPrice: undefined,
contractPaymentPercentage: undefined,
contractPaymentAmount: undefined
});
detailDialog.title = '新增分款明细';
detailDialog.visible = true;
};
//
const handleEditDetail = (row: FinAccountInstallmentDetailVO) => {
/** 新增明细:选定分款节点后,分款金额默认取当前节点约定金额 */
const syncAddDetailAmountFromSelectedStage = () => {
if (!detailDialog.visible || detailForm.installmentDetailId) return;
let next: number | undefined;
if (detailForm.paymentStageId != null && detailForm.paymentStageId !== '') {
const raw = selectedStageContractShareAmount.value;
const n = raw != null && !Number.isNaN(Number(raw)) ? Number(raw) : NaN;
if (!Number.isNaN(n) && n > 0) {
const rounded = Math.round(n * 100) / 100;
next = rounded < 0.01 ? 0.01 : rounded;
}
}
detailForm.detailAmount = next;
if (detailFormRef.value) {
nextTick(() => detailFormRef.value?.validateField('detailAmount'));
}
};
// / join getContractInfo
const handleEditDetail = async (row: FinAccountInstallmentDetailVO) => {
Object.assign(detailForm, row);
detailDialog.title = '编辑分款明细';
detailDialog.visible = true;
getPaymentStages(detailForm.contractId);
};
//
const handleDetailAmountChange = () => {
// computedformRemainingAmount
const tp = row.contractTotalPrice;
detailFormContractTotalPrice.value =
tp !== undefined && tp !== null && !Number.isNaN(Number(tp)) ? Number(tp) : undefined;
await getPaymentStages(detailForm.contractId);
nextTick(() => detailFormRef.value?.validateField('detailAmount'));
};
//
@ -635,16 +740,7 @@ const handleDeleteDetail = async (row: FinAccountInstallmentDetailVO) => {
await deleteInstallmentDetailByBo(deleteForm);
proxy?.$modal.msgSuccess('删除成功');
await getDetailList();
//
if (selectedInstallment.value) {
const newStatus = remainingAmount.value === 0 ? INSTALLMENT_STATUS.COMPLETE : (allocatedAmount.value > 0 ? INSTALLMENT_STATUS.PARTIALLY : INSTALLMENT_STATUS.NOT);
selectedInstallment.value.installmentStatus = newStatus;
//
const index = finAccountInstallmentList.value.findIndex((item) => item.accountInstallmentId === selectedInstallment.value?.accountInstallmentId);
if (index !== -1) {
finAccountInstallmentList.value[index].installmentStatus = newStatus;
}
}
syncSelectedInstallmentStatusWithDetailList();
};
//
@ -664,16 +760,7 @@ const submitDetailForm = async () => {
}
detailDialog.visible = false;
await getDetailList();
//
if (selectedInstallment.value) {
const newStatus = remainingAmount.value === 0 ? INSTALLMENT_STATUS.COMPLETE : (allocatedAmount.value > 0 ? INSTALLMENT_STATUS.PARTIALLY : INSTALLMENT_STATUS.NOT);
selectedInstallment.value.installmentStatus = newStatus;
//
const index = finAccountInstallmentList.value.findIndex((item) => item.accountInstallmentId === selectedInstallment.value?.accountInstallmentId);
if (index !== -1) {
finAccountInstallmentList.value[index].installmentStatus = newStatus;
}
}
syncSelectedInstallmentStatusWithDetailList();
} finally {
detailButtonLoading.value = false;
}
@ -682,7 +769,6 @@ const submitDetailForm = async () => {
//
const cancelDetail = () => {
detailDialog.visible = false;
Object.assign(detailForm, initDetailForm);
};
// Excel
@ -720,25 +806,31 @@ const submitImport = async () => {
uploadRef.value?.submit();
};
//
watch(showImportDialog, (val) => {
if (!val) {
importPreviewData.value = [];
importFile.value = null;
uploadRef.value?.clearFiles();
}
});
//
watch(
() => detailDialog.visible,
(val) => {
if (!val) {
Object.assign(detailForm, initDetailForm);
detailFormContractTotalPrice.value = undefined;
}
}
);
watch(
() => [
detailDialog.visible,
detailForm.installmentDetailId,
detailForm.paymentStageId,
paymentStageList.value,
detailFormContractTotalPrice.value
],
() => {
syncAddDetailAmountFromSelectedStage();
},
{ flush: 'post' }
);
//
onMounted(() => {
getList();

Loading…
Cancel
Save