feat(edu): 合同前端-分期表单区改造+详情缴费计划进度卡片+收款确认弹窗
This commit is contained in:
parent
8aa8734436
commit
08f24c3707
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user