feat(edu): 合同前端-分期表单区改造+详情缴费计划进度卡片+收款确认弹窗

This commit is contained in:
liuhuayong 2026-06-27 10:25:38 +08:00
parent 8aa8734436
commit 08f24c3707

View File

@ -153,14 +153,46 @@
<el-descriptions-item label="结算日期">{{ proxy.parseTime(detailData.settlementDate, '{y}-{m}-{d}') || '-' }}</el-descriptions-item>
</el-descriptions>
<!-- 分期信息 -->
<template v-if="detailData.isInstallment === 'Y'">
<!-- 缴费计划进度 -->
<template v-if="detailData.isInstallment === 'Y' && detailData.paymentPlans && detailData.paymentPlans.length > 0">
<el-card shadow="never" class="mt-3 mb-3">
<div class="mb-2" style="font-weight: 600; color: #606266;">分期信息</div>
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="二期金额">¥{{ detailData.installmentSecond || 0 }}</el-descriptions-item>
<el-descriptions-item label="三期金额">¥{{ detailData.installmentThird || 0 }}</el-descriptions-item>
</el-descriptions>
<div class="mb-2" style="font-weight: 600; color: #606266;">缴费计划进度</div>
<el-table border :data="detailData.paymentPlans" size="small">
<el-table-column label="期次" width="70" align="center">
<template #default="{ row }">{{ row.installmentNo }}</template>
</el-table-column>
<el-table-column label="应收金额" prop="planAmount" align="center" width="100" />
<el-table-column label="应付日期" align="center" width="110">
<template #default="{ row }">{{ proxy.parseTime(row.planDate, '{y}-{m}-{d}') }}</template>
</el-table-column>
<el-table-column label="实收金额" prop="paidAmount" align="center" width="100" />
<el-table-column label="收款日期" align="center" width="110">
<template #default="{ row }">{{ row.paidDate ? proxy.parseTime(row.paidDate, '{y}-{m}-{d}') : '-' }}</template>
</el-table-column>
<el-table-column label="状态" align="center" width="80">
<template #default="{ row }">
<el-tag :type="getPlanStatusTag(row.status)" size="small">{{ getPlanStatusLabel(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="100" align="center">
<template #default="{ row }">
<el-button
v-if="row.status === '0'"
v-hasPermi="['edu:contract:activate']"
link type="success" @click="handleReceive(row)"
>收款确认</el-button>
</template>
</el-table-column>
</el-table>
<div class="mt-2 plan-summary">
<span>应收 <b>¥{{ planSummary.total }}</b></span>
<span class="ml-3">已收 <b class="money-success">¥{{ planSummary.received }}</b></span>
<span class="ml-3">待收 <b class="money-warn">¥{{ planSummary.pending }}</b></span>
<el-progress
:percentage="planSummary.total ? Math.round(planSummary.received / planSummary.total * 100) : 0"
:stroke-width="14" :text-inside="true" class="plan-progress"
/>
</div>
</el-card>
</template>
@ -328,23 +360,54 @@
<el-row>
<el-col :span="8">
<el-form-item label="是否分期" prop="isInstallment">
<el-select v-model="form.isInstallment" placeholder="请选择" style="width: 100%">
<el-select v-model="form.isInstallment" placeholder="请选择" style="width: 100%" @change="onInstallmentChange">
<el-option label="是" value="Y" />
<el-option label="否" value="N" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8" v-if="form.isInstallment === 'Y'">
<el-form-item label="二期金额">
<el-input-number v-model="form.installmentSecond" :min="0" :precision="2" controls-position="right" style="width: 100%" />
<el-form-item label="分期数" prop="installmentCount">
<el-select v-model="form.installmentCount" placeholder="请选择" style="width: 100%" @change="autoGeneratePlan">
<el-option label="2 期" :value="2" />
<el-option label="3 期" :value="3" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8" v-if="form.isInstallment === 'Y'">
<el-form-item label="三期金额">
<el-input-number v-model="form.installmentThird" :min="0" :precision="2" controls-position="right" style="width: 100%" />
<el-form-item label="应收总额">
<el-input :model-value="computedTotalAmount" disabled />
</el-form-item>
</el-col>
</el-row>
<!-- 缴费计划明细(分期时显示) -->
<template v-if="form.isInstallment === 'Y'">
<el-divider content-position="left">
缴费计划
<el-button class="ml-2" type="primary" plain icon="MagicStick" size="small" @click="autoGeneratePlan">自动生成</el-button>
</el-divider>
<el-table border :data="form.paymentPlans" size="small">
<el-table-column label="期次" width="80" align="center">
<template #default="{ row }">{{ row.installmentNo }}</template>
</el-table-column>
<el-table-column label="应收金额" width="160" align="center">
<template #default="{ row }">
<el-input-number v-model="row.planAmount" :min="0" :precision="2" size="small" controls-position="right" />
</template>
</el-table-column>
<el-table-column label="应付日期" width="180" align="center">
<template #default="{ row }">
<el-date-picker v-model="row.planDate" type="date" value-format="YYYY-MM-DD" size="small" style="width: 100%" />
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center">
<template #default="{ $index }">
<el-button link type="danger" icon="Delete" @click="form.paymentPlans?.splice($index, 1)" />
</template>
</el-table-column>
</el-table>
</template>
<el-row>
<el-col :span="8">
<el-form-item label="实收总额" prop="actualTotalReceived">
@ -610,6 +673,36 @@
<el-button @click="transferDialog.visible = false"> </el-button>
</template>
</el-dialog>
<!-- 收款确认弹窗 -->
<el-dialog v-model="receiveDialog.visible" title="缴费计划收款确认" width="420px" append-to-body>
<el-form label-width="100px">
<el-form-item label="期次">
<span> {{ receiveRow.installmentNo }} </span>
</el-form-item>
<el-form-item label="应收金额">
<span>¥{{ receiveRow.planAmount }}</span>
</el-form-item>
<el-form-item label="实收金额">
<el-input-number v-model="receiveForm.paidAmount" :min="0" :precision="2" controls-position="right" />
</el-form-item>
<el-form-item label="支付方式">
<el-select v-model="receiveForm.paymentMethod" placeholder="请选择" style="width: 100%">
<el-option v-for="opt in paymentMethodOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
</el-form-item>
<el-form-item label="收款日期">
<el-date-picker v-model="receiveForm.paidDate" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" style="width: 100%" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="receiveForm.remark" type="textarea" placeholder="备注" />
</el-form-item>
</el-form>
<template #footer>
<el-button type="success" @click="submitReceive">确认收款</el-button>
<el-button @click="receiveDialog.visible = false"> </el-button>
</template>
</el-dialog>
</div>
</template>
@ -640,6 +733,8 @@ import {
} from '@/api/edu/contract/types';
import { deliverMilestone } from '@/api/edu/milestone';
import { milestoneStatusOptions } from '@/api/edu/milestone/types';
import { receivePayment } from '@/api/edu/payment-plan';
import { PaymentReceiveForm, PaymentPlanForm, paymentPlanStatusOptions } from '@/api/edu/payment-plan/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@ -654,12 +749,15 @@ const dialog = reactive<DialogOption>({ visible: false, title: '' });
const activateDialog = reactive<DialogOption>({ visible: false, title: '' });
const refundDialog = reactive<DialogOption>({ visible: false, title: '' });
const transferDialog = reactive<DialogOption>({ visible: false, title: '' });
const receiveDialog = reactive<DialogOption>({ visible: false, title: '' });
const detailVisible = ref(false);
const detailData = ref<ContractVO>({} as ContractVO);
const activateRow = ref<ContractVO>({} as ContractVO);
const refundRow = ref<ContractVO>({} as ContractVO);
const transferRow = ref<ContractVO>({} as ContractVO);
const receiveRow = ref<any>({});
const receiveForm = reactive<PaymentReceiveForm>({ id: '', paidAmount: 0, paymentMethod: 'WECHAT', paidDate: '', remark: '' });
const initFormData: ContractForm = {
id: undefined,
@ -679,6 +777,8 @@ const initFormData: ContractForm = {
isInstallment: 'N',
installmentSecond: 0,
installmentThird: 0,
installmentCount: 2,
paymentPlans: [],
actualTotalReceived: 0,
settlementDate: '',
signPerson: '',
@ -743,6 +843,8 @@ const getStatusTag = (val: string) => contractStatusOptions.find((o) => o.value
const getPayMethodLabel = (val: string) => paymentMethodOptions.find((o) => o.value === val)?.label || val;
const getMilestoneLabel = (val: string) => milestoneStatusOptions.find((o) => o.value === val)?.label || val;
const getMilestoneTag = (val: string) => milestoneStatusOptions.find((o) => o.value === val)?.elTagType || 'info';
const getPlanStatusLabel = (val: string) => paymentPlanStatusOptions.find((o) => o.value === val)?.label || val;
const getPlanStatusTag = (val: string) => paymentPlanStatusOptions.find((o) => o.value === val)?.elTagType || 'info';
/** 课时进度颜色(>80%红色预警, >50%橙色, 否则绿色) */
const hoursProgressColor = computed(() => {
@ -751,6 +853,84 @@ const hoursProgressColor = computed(() => {
return pct >= 80 ? '#f56c6c' : pct >= 50 ? '#e6a23c' : '#67c23a';
});
/** 应收总额(从课包明细汇总) */
const computedTotalAmount = computed(() => {
return (form.value.packages || []).reduce((sum, p) => sum + (Number(p.finalAmount) || Number(p.salePrice) || 0), 0);
});
/** 缴费计划汇总(详情卡片用) */
const planSummary = computed(() => {
const plans: any[] = detailData.value.paymentPlans || [];
const total = plans.reduce((s, p) => s + (Number(p.planAmount) || 0), 0);
const received = plans
.filter((p: any) => p.status === '1')
.reduce((s, p) => s + (Number(p.paidAmount) || 0), 0);
const pending = plans
.filter((p: any) => p.status === '0')
.reduce((s, p) => s + (Number(p.planAmount) || 0), 0);
return { total, received, pending };
});
/** 分期开关变化 */
const onInstallmentChange = (val: string) => {
if (val === 'Y') {
if (!form.value.installmentCount) form.value.installmentCount = 2;
autoGeneratePlan();
} else {
form.value.paymentPlans = [];
}
};
/** 按规则自动生成缴费计划: 首期吸收尾差, 日期按 30 天间隔 */
const autoGeneratePlan = () => {
const count = form.value.installmentCount;
if (!count || (count !== 2 && count !== 3)) {
proxy?.$modal.msgWarning('请选择分期数(2 或 3)');
return;
}
const total = Number(computedTotalAmount.value) || 0;
if (total <= 0) {
proxy?.$modal.msgWarning('应收总额大于 0 才能生成计划');
return;
}
// base = total/count, 2~N base, 1
const base = Math.round((total / count) * 100) / 100;
const first = Math.round((total - base * (count - 1)) * 100) / 100;
const signDate = form.value.signDate || proxy.parseTime(new Date(), '{y}-{m}-{d}');
const signMs = new Date(signDate).getTime();
const plans: PaymentPlanForm[] = [];
for (let i = 1; i <= count; i++) {
plans.push({
installmentNo: i,
planAmount: i === 1 ? first : base,
planDate: proxy.parseTime(new Date(signMs + 30 * (i - 1) * 86400000), '{y}-{m}-{d}')
});
}
form.value.paymentPlans = plans;
};
/** 缴费计划收款确认 */
const handleReceive = (row: any) => {
receiveRow.value = row;
receiveForm.id = row.id;
receiveForm.paidAmount = row.planAmount;
receiveForm.paymentMethod = detailData.value.paymentMethod || 'WECHAT';
receiveForm.paidDate = proxy.parseTime(new Date(), '{y}-{m}-{d}');
receiveForm.remark = '';
receiveDialog.visible = true;
};
const submitReceive = async () => {
await proxy?.$modal.confirm(`确认第 ${receiveRow.value.installmentNo} 期收款 ${receiveForm.paidAmount} 元?`);
await receivePayment({ ...receiveForm });
proxy?.$modal.msgSuccess('收款成功');
receiveDialog.visible = false;
//
const res = await getContract(detailData.value.id);
detailData.value = res.data;
await getList();
};
const getList = async () => {
loading.value = true;
const res = await listContract(queryParams.value);
@ -790,6 +970,13 @@ const handleUpdate = async (row: ContractVO) => {
const res = await getContract(row.id);
Object.assign(form.value, res.data);
form.value.packages = res.data.packages || [];
// (: //)
form.value.paymentPlans = (res.data.paymentPlans || []).map((p: any) => ({
installmentNo: p.installmentNo,
planAmount: p.planAmount,
planDate: p.planDate
}));
form.value.installmentCount = res.data.paymentPlans?.length || 2;
dialog.visible = true;
dialog.title = '修改合同';
};
@ -801,6 +988,20 @@ const submitForm = () => {
proxy?.$modal.msgWarning('请至少添加一个课包明细');
return;
}
// : +
if (form.value.isInstallment === 'Y') {
if (!form.value.installmentCount || form.value.paymentPlans?.length !== form.value.installmentCount) {
proxy?.$modal.msgWarning('缴费计划期数与分期数不一致, 请点"自动生成"');
return;
}
const sum = (form.value.paymentPlans || []).reduce((s, p) => s + (Number(p.planAmount) || 0), 0);
if (Math.abs(sum - computedTotalAmount.value) > 0.01) {
proxy?.$modal.msgWarning(`计划金额之和 ${sum.toFixed(2)} 与应收总额 ${computedTotalAmount.value.toFixed(2)} 不一致`);
return;
}
} else {
form.value.paymentPlans = [];
}
form.value.id ? await updateContract(form.value) : await addContract(form.value);
proxy?.$modal.msgSuccess('操作成功');
dialog.visible = false;
@ -926,5 +1127,7 @@ onMounted(() => {
.hours-progress { display: flex; align-items: center; gap: 12px; }
.hours-label { white-space: nowrap; color: #606266; font-weight: 600; }
.hours-detail { white-space: nowrap; font-size: 13px; color: #909399; }
.plan-summary { display: flex; align-items: center; gap: 4px; font-size: 13px; color: #606266; }
.plan-progress { flex: 1; margin-left: 12px; max-width: 280px; }
:deep(.el-drawer__body) { padding-top: 12px; }
</style>