Compare commits
3 Commits
347c2cb0aa
...
6dd053b2b3
| Author | SHA1 | Date | |
|---|---|---|---|
| 6dd053b2b3 | |||
| 08f24c3707 | |||
| 8aa8734436 |
|
|
@ -37,6 +37,8 @@ export const paymentMethodOptions = [
|
|||
{ label: '信用卡', value: 'CARD' }
|
||||
];
|
||||
|
||||
import type { PaymentPlanVO, PaymentPlanForm } from '@/api/edu/payment-plan/types';
|
||||
|
||||
export interface ContractPackageVO {
|
||||
id: number | string;
|
||||
contractId: number | string;
|
||||
|
|
@ -107,7 +109,16 @@ export interface ContractVO extends BaseEntity {
|
|||
installmentThird: number;
|
||||
actualTotalReceived: number;
|
||||
settlementDate: string;
|
||||
/** 签约负责人(姓名, 旧字段保留兼容) */
|
||||
signPerson: string;
|
||||
/** 签约负责人ID(edu_staff.id) */
|
||||
signUserId?: number | string;
|
||||
/** 课程顾问姓名(查询翻译填充) */
|
||||
salesName?: string;
|
||||
/** 管家姓名(查询翻译填充) */
|
||||
housekeeperName?: string;
|
||||
/** 签约负责人姓名(查询翻译填充) */
|
||||
signUserName?: string;
|
||||
contractRecovered: string;
|
||||
remittanceName: string;
|
||||
needInvoice: string;
|
||||
|
|
@ -141,6 +152,8 @@ export interface ContractVO extends BaseEntity {
|
|||
logs: any[];
|
||||
/** 里程碑列表(升学服务合同) */
|
||||
milestones?: any[];
|
||||
/** 缴费计划列表(分期合同) */
|
||||
paymentPlans?: PaymentPlanVO[];
|
||||
}
|
||||
|
||||
export interface ContractForm {
|
||||
|
|
@ -161,9 +174,24 @@ export interface ContractForm {
|
|||
isInstallment: string;
|
||||
installmentSecond: number;
|
||||
installmentThird: number;
|
||||
/** 分期数 2 或 3 */
|
||||
installmentCount?: number;
|
||||
/** 缴费计划明细(分期时携带) */
|
||||
paymentPlans?: PaymentPlanForm[];
|
||||
/** 应收总额(前端汇总展示, 后端汇总覆盖) */
|
||||
totalAmount?: number;
|
||||
actualTotalReceived: number;
|
||||
settlementDate: string;
|
||||
/** 签约负责人(姓名, 旧字段保留兼容) */
|
||||
signPerson: string;
|
||||
/** 签约负责人ID(edu_staff.id) */
|
||||
signUserId?: number | string;
|
||||
/** 课程顾问姓名(查询翻译填充) */
|
||||
salesName?: string;
|
||||
/** 管家姓名(查询翻译填充) */
|
||||
housekeeperName?: string;
|
||||
/** 签约负责人姓名(查询翻译填充) */
|
||||
signUserName?: string;
|
||||
contractRecovered: string;
|
||||
remittanceName: string;
|
||||
needInvoice: string;
|
||||
|
|
|
|||
18
src/api/edu/payment-plan/index.ts
Normal file
18
src/api/edu/payment-plan/index.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import request from '@/utils/request';
|
||||
import { AxiosPromise } from 'axios';
|
||||
import { PaymentPlanVO, PaymentReceiveForm, EduPaymentPlanBoPayload } from './types';
|
||||
|
||||
// 查询某合同的缴费计划列表
|
||||
export function listPaymentPlan(contractId: number | string): AxiosPromise<PaymentPlanVO[]> {
|
||||
return request({ url: '/edu/contract/payment-plan/list/' + contractId, method: 'get' });
|
||||
}
|
||||
|
||||
// 修改单期计划(仅待收态)
|
||||
export function updatePaymentPlan(data: EduPaymentPlanBoPayload) {
|
||||
return request({ url: '/edu/contract/payment-plan', method: 'put', data });
|
||||
}
|
||||
|
||||
// 收款确认(联动账本)
|
||||
export function receivePayment(data: PaymentReceiveForm) {
|
||||
return request({ url: '/edu/contract/payment-plan/receive', method: 'put', data });
|
||||
}
|
||||
42
src/api/edu/payment-plan/types.ts
Normal file
42
src/api/edu/payment-plan/types.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
export const paymentPlanStatusOptions = [
|
||||
{ label: '待收', value: '0', elTagType: 'warning' },
|
||||
{ label: '已收', value: '1', elTagType: 'success' },
|
||||
{ label: '已作废', value: '4', elTagType: 'info' }
|
||||
];
|
||||
|
||||
export interface PaymentPlanVO {
|
||||
id: number | string;
|
||||
contractId: number | string;
|
||||
installmentNo: number;
|
||||
planAmount: number;
|
||||
planDate: string;
|
||||
paidAmount: number;
|
||||
paidDate: string;
|
||||
paymentMethod: string;
|
||||
status: string;
|
||||
remark: string;
|
||||
}
|
||||
|
||||
/** 表单用: 新增/修改合同时携带的计划明细 */
|
||||
export interface PaymentPlanForm {
|
||||
installmentNo: number;
|
||||
planAmount: number;
|
||||
planDate: string;
|
||||
}
|
||||
|
||||
/** 修改单期计划载荷(匹配后端 EduPaymentPlanBo) */
|
||||
export interface EduPaymentPlanBoPayload {
|
||||
id: number | string;
|
||||
planAmount?: number;
|
||||
planDate?: string;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
/** 收款确认表单 */
|
||||
export interface PaymentReceiveForm {
|
||||
id: number | string;
|
||||
paidAmount: number;
|
||||
paymentMethod: string;
|
||||
paidDate: string;
|
||||
remark?: string;
|
||||
}
|
||||
86
src/components/StaffSelect/index.vue
Normal file
86
src/components/StaffSelect/index.vue
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
<template>
|
||||
<el-select
|
||||
:model-value="modelValue"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:clearable="clearable"
|
||||
filterable
|
||||
style="width: 100%"
|
||||
@update:model-value="onChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in options"
|
||||
:key="item.id"
|
||||
:label="item.name + (item.staffNo ? ' (' + item.staffNo + ')' : '')"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="StaffSelect">
|
||||
import { staffOptionselect } from '@/api/edu/staff';
|
||||
import { StaffVO } from '@/api/edu/staff/types';
|
||||
|
||||
interface PropType {
|
||||
/** 选中的员工ID(staff.id) */
|
||||
modelValue?: number | string;
|
||||
/** 角色筛选 SALES/HOUSEKEEPER/TUTOR/... 不传则全部 */
|
||||
role?: string;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
clearable?: boolean;
|
||||
}
|
||||
|
||||
const prop = withDefaults(defineProps<PropType>(), {
|
||||
modelValue: undefined,
|
||||
role: undefined,
|
||||
placeholder: '请选择',
|
||||
disabled: false,
|
||||
clearable: true
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'change']);
|
||||
|
||||
const options = ref<StaffVO[]>([]);
|
||||
|
||||
/** 拉取候选员工(按 role 过滤) */
|
||||
const loadOptions = async () => {
|
||||
const res = await staffOptionselect(prop.role);
|
||||
options.value = res.data || [];
|
||||
};
|
||||
|
||||
const onChange = (val: number | string) => {
|
||||
emit('update:modelValue', val);
|
||||
const staff = options.value.find((o) => o.id === val);
|
||||
emit('change', val, staff);
|
||||
};
|
||||
|
||||
/**
|
||||
* 传入 staff id 时若当前候选不含, 单条补查并补入候选, 保证回显正确
|
||||
*/
|
||||
const ensureEcho = async () => {
|
||||
if (prop.modelValue === undefined || prop.modelValue === null || prop.modelValue === '') return;
|
||||
const exists = options.value.some((o) => o.id === prop.modelValue);
|
||||
if (!exists) {
|
||||
// 角色已限定时, 该 id 可能不在候选内 → 放宽 role 重新拉一次全部再匹配
|
||||
const res = await staffOptionselect(undefined);
|
||||
const all = res.data || [];
|
||||
const found = all.find((o) => o.id === prop.modelValue);
|
||||
if (found && !options.value.some((o) => o.id === found.id)) {
|
||||
options.value.push(found);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => prop.role,
|
||||
() => {
|
||||
loadOptions();
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
await loadOptions();
|
||||
await ensureEcho();
|
||||
});
|
||||
</script>
|
||||
|
|
@ -139,7 +139,9 @@
|
|||
<el-descriptions-item label="服务期限">{{ detailData.servicePeriodText || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="服务合同">{{ detailData.serviceContractName || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="激活时间">{{ proxy.parseTime(detailData.activatedAt) || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="签约人">{{ detailData.signPerson || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="课程顾问">{{ detailData.salesName || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="管家">{{ detailData.housekeeperName || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="签约人">{{ detailData.signUserName || detailData.signPerson || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="签约地点">{{ detailData.signLocation || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="合同回收">
|
||||
<el-tag :type="detailData.contractRecovered === 'Y' ? 'success' : 'info'" size="small">{{ detailData.contractRecovered === 'Y' ? '已回收' : '未回收' }}</el-tag>
|
||||
|
|
@ -153,14 +155,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 +362,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">
|
||||
|
|
@ -366,18 +431,18 @@
|
|||
<el-divider content-position="left">签约信息</el-divider>
|
||||
<el-row>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="顾问ID" prop="salesId">
|
||||
<el-input v-model="form.salesId" placeholder="课程顾问ID" />
|
||||
<el-form-item label="课程顾问" prop="salesId">
|
||||
<staff-select v-model="form.salesId" role="SALES" placeholder="课程顾问" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="管家ID" prop="housekeeperId">
|
||||
<el-input v-model="form.housekeeperId" placeholder="管家ID" />
|
||||
<el-form-item label="管家" prop="housekeeperId">
|
||||
<staff-select v-model="form.housekeeperId" role="HOUSEKEEPER" placeholder="管家" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="签约人" prop="signPerson">
|
||||
<el-input v-model="form.signPerson" placeholder="签约人姓名" />
|
||||
<el-form-item label="签约人" prop="signUserId">
|
||||
<staff-select v-model="form.signUserId" placeholder="签约负责人" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
|
@ -610,6 +675,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 +735,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 +751,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,9 +779,12 @@ const initFormData: ContractForm = {
|
|||
isInstallment: 'N',
|
||||
installmentSecond: 0,
|
||||
installmentThird: 0,
|
||||
installmentCount: 2,
|
||||
paymentPlans: [],
|
||||
actualTotalReceived: 0,
|
||||
settlementDate: '',
|
||||
signPerson: '',
|
||||
signUserId: undefined,
|
||||
contractRecovered: 'N',
|
||||
remittanceName: '',
|
||||
needInvoice: 'N',
|
||||
|
|
@ -743,6 +846,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 +856,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 +973,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 +991,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 +1130,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