优化回款功能

This commit is contained in:
2026-09-13 14:09:34 +08:00
parent aea53e0766
commit 2f6cdbb3cd
13 changed files with 855 additions and 668 deletions
@@ -49,7 +49,7 @@
</template>
</el-table>
<!-- ============================ 回款进度 ============================ -->
<!-- ============================ 回款进度(回款记录流水) ============================ -->
<div class="section-head progress-head">
<span class="section-title">回款进度</span>
<div class="progress-summary">
@@ -57,49 +57,29 @@
<span>已回款:<b class="success">¥{{ formatMoney(progressSummary.total_received) }}</b></span>
<span>待回款:<b class="warning">¥{{ formatMoney((progressSummary.total_planned || 0) - (progressSummary.total_received || 0)) }}</b></span>
</div>
<el-tooltip :disabled="!paidOff" content="回款已全部完成(进度 100%),不可再新建回款计划" placement="top">
<el-tooltip :disabled="list.length > 0" content="回款进度以回款计划为先导条件,请先新建回款计划" placement="top">
<span class="btn-wrap">
<el-button type="primary" size="small" :icon="Plus" :disabled="paidOff" @click="openCreate">
新建回款计划
<el-button type="primary" size="small" :icon="Plus" :disabled="!list.length" @click="openReceive">
新建回款
</el-button>
</span>
</el-tooltip>
</div>
<el-table v-loading="progressLoading" :data="progressList" border size="small" row-key="rowKey">
<el-table-column label="回款周期" width="90" align="center">
<template #default="{ row }">{{ paybackCycleText(row.plan_type) }}</template>
</el-table-column>
<el-table-column label="明细" min-width="140" show-overflow-tooltip>
<template #default="{ row }">{{ row.name || `第${row.seq}期` }}</template>
</el-table-column>
<el-table-column label="进度百分比" width="100" align="center">
<template #default="{ row }">{{ row.percent ? `${row.percent}%` : "-" }}</template>
</el-table-column>
<el-table-column label="计划金额" width="120" align="right">
<template #default="{ row }">{{ formatMoney(row.amount) }}</template>
</el-table-column>
<el-table-column label="计划日期" width="110" align="center">
<template #default="{ row }">{{ row.plan_date || "-" }}</template>
</el-table-column>
<el-table-column label="已回款" width="120" align="right">
<template #default="{ row }">{{ formatMoney(row.received_amount) }}</template>
</el-table-column>
<el-table v-loading="progressLoading" :data="records" border size="small" row-key="id">
<el-table-column label="回款日期" width="110" align="center">
<template #default="{ row }">{{ (row.received_date && row.received_date !== "<nil>") ? row.received_date : "-" }}</template>
<template #default="{ row }">{{ row.received_date || "-" }}</template>
</el-table-column>
<el-table-column label="逾期" width="100" align="center">
<el-table-column label="对应回款计划" min-width="180" show-overflow-tooltip>
<template #default="{ row }">{{ planText(planOf(row.payback_id)) }}</template>
</el-table-column>
<el-table-column label="本次回款金额" width="130" align="right">
<template #default="{ row }">
<el-tag v-if="paybackItemOverdue(row).overdue" type="danger" size="small" effect="dark">
{{ overdueText(paybackItemOverdue(row).days) }}
</el-tag>
<span v-else>-</span>
<span class="amount">¥{{ formatMoney(row.amount) }}</span>
</template>
</el-table-column>
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="paybackItemStatusTag(row.status)" size="small">{{ paybackItemStatusText(row.status) }}</el-tag>
</template>
<el-table-column label="回款方式" width="100" align="center">
<template #default="{ row }">{{ paybackMethodText(row.pay_method) }}</template>
</el-table-column>
<el-table-column label="流水回执" width="110" align="center">
<template #default="{ row }">
@@ -107,16 +87,16 @@
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="负责人" width="90">
<template #default="{ row }">{{ row.owner_user_name || "-" }}</template>
<el-table-column label="备注" min-width="120" show-overflow-tooltip>
<template #default="{ row }">{{ row.remark || "-" }}</template>
</el-table-column>
<el-table-column label="操作" width="100" align="center" fixed="right">
<el-table-column label="操作" width="80" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="openReceive(row)">回款登记</el-button>
<el-button link type="danger" size="small" @click="handleDeleteRecord(row)">删除</el-button>
</template>
</el-table-column>
<template #empty>
<el-empty description="该合同暂无回款进度,先新建回款计划" :image-size="60" />
<el-empty description="暂无回款记录,点击右上角「新建回款」登记一笔实收" :image-size="60" />
</template>
</el-table>
@@ -131,21 +111,26 @@
<!-- 回款计划详情 -->
<PaybackDetail v-model:visible="detailVisible" :payback="currentRow" @edit="handleEdit" />
<!-- 回款登记 -->
<el-dialog v-model="receiveVisible" title="回款登记" width="440px">
<!-- 新建回款(回款进度登记:以回款计划为先导) -->
<el-dialog v-model="receiveVisible" title="新建回款" width="460px">
<el-form :model="receiveForm" label-width="90px">
<el-form-item label="明细">
<span>{{ receiveForm.name || `第${receiveForm.seq}期` }}</span>
<el-form-item label="回款计划" required>
<el-select
v-model="receiveForm.payback_id"
placeholder="请选择回款计划"
style="width: 100%"
@change="onPlanChange"
>
<el-option v-for="p in list" :key="p.id" :label="planText(p)" :value="p.id" />
</el-select>
</el-form-item>
<el-form-item label="计划金额">
<span class="amount">¥{{ formatMoney(receiveForm.amount) }}</span>
</el-form-item>
<el-form-item label="实收金额">
<el-form-item label="本次回款" required>
<el-input-number
v-model="receiveForm.received_amount"
v-model="receiveForm.amount"
:min="0"
:precision="2"
:controls="false"
placeholder="0.00"
style="width: 100%"
/>
</el-form-item>
@@ -158,6 +143,11 @@
style="width: 100%"
/>
</el-form-item>
<el-form-item label="回款方式">
<el-select v-model="receiveForm.pay_method" style="width: 100%">
<el-option v-for="m in PAYBACK_METHOD_OPTIONS" :key="m.value" :label="m.label" :value="m.value" />
</el-select>
</el-form-item>
<el-form-item label="流水回执">
<div class="receipt-block">
<el-upload
@@ -177,6 +167,9 @@
</div>
</div>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="receiveForm.remark" type="textarea" :rows="2" maxlength="200" placeholder="选填" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="receiveVisible = false">取消</el-button>
@@ -188,21 +181,23 @@
<script setup>
import { computed, ref, reactive } from "vue";
import { ElMessage } from "element-plus";
import { ElMessage, ElMessageBox } from "element-plus";
import { Plus, Upload, Document, Delete } from "@element-plus/icons-vue";
import { getPaybackList, getPaybackProgress, registerPaybackReceive } from "@/api/crmPayback";
import {
getPaybackList,
getPaybackRecordList,
createPaybackRecord,
deletePaybackRecord,
} from "@/api/crmPayback";
import { uploadFile } from "@/api/file";
import PaybackCreate from "../../payback/components/create.vue";
import PaybackDetail from "../../payback/components/detail.vue";
import {
PAYBACK_METHOD_OPTIONS,
paybackCycleText,
paybackMethodText,
paybackStatusText,
paybackStatusTag,
paybackItemStatusText,
paybackItemStatusTag,
paybackItemOverdue,
overdueText,
formatMoney,
} from "../../dict";
@@ -220,11 +215,32 @@ const detailVisible = ref(false);
const currentRow = ref(null);
const editRow = ref(null);
/** 回款进度(摊平的分期明细) */
/** 回款进度:一笔笔实际回款记录(回款流水) */
const progressLoading = ref(false);
const progressList = ref([]);
const records = ref([]);
const progressSummary = ref({ total_planned: 0, total_received: 0 });
/** 今日日期(新建回款默认回款日期) */
function todayStr() {
const d = new Date();
const m = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${d.getFullYear()}-${m}-${day}`;
}
/** 按计划 id 取回款计划对象(回款记录上只存 payback_id) */
function planOf(paybackId) {
return list.value.find((i) => Number(i.id) === Number(paybackId)) || null;
}
/** 回款计划展示文案:周期 + 计划金额 + 已回款 */
function planText(p) {
if (!p) return "-";
return `${paybackCycleText(p.plan_type)}|计划 ¥${formatMoney(p.total_amount)}|已回 ¥${formatMoney(
p.received_amount
)}`;
}
/** 回款计划是否已全部收齐(总进度达到 100%),收齐后禁止再新建回款计划 */
const paidOff = computed(() => {
const planned = Number(progressSummary.value.total_planned) || 0;
@@ -271,7 +287,7 @@ async function reload() {
const contractId = props.contract?.id;
if (!contractId) {
list.value = [];
progressList.value = [];
records.value = [];
progressSummary.value = { total_planned: 0, total_received: 0 };
emit("count", 0);
return;
@@ -286,23 +302,23 @@ async function reload() {
loading.value = false;
emit("count", list.value.length);
}
await fetchProgress();
await fetchRecords();
}
/** 拉取本合同回款进度 */
async function fetchProgress() {
/** 拉取本合同回款记录(回款进度流水,一笔一条) */
async function fetchRecords() {
const contractId = props.contract?.id;
if (!contractId) return;
progressLoading.value = true;
try {
const res = await getPaybackProgress({ contract_id: contractId });
progressList.value = (res?.data?.list || []).map((r) => ({ ...r, rowKey: `${r.payback_id}_${r.seq}` }));
const res = await getPaybackRecordList({ contract_id: contractId });
records.value = res?.data?.list || [];
progressSummary.value = {
total_planned: res?.data?.total_planned || 0,
total_received: res?.data?.total_received || 0,
};
} catch (e) {
progressList.value = [];
records.value = [];
progressSummary.value = { total_planned: 0, total_received: 0 };
ElMessage.error(e.message || "查询回款进度失败");
} finally {
@@ -310,34 +326,44 @@ async function fetchProgress() {
}
}
/* ---------------------------- 回款登记 ---------------------------- */
/* ---------------------------- 新建回款(回款进度登记) ---------------------------- */
const receiveVisible = ref(false);
const receiveSaving = ref(false);
const receiveForm = reactive({
payback_id: null,
seq: 0,
name: "",
amount: 0,
received_amount: 0,
received_date: "",
pay_method: 1,
receipt_url: "",
receipt_name: "",
remark: "",
});
function openReceive(row) {
/** 新建回款:以回款计划为先导条件(无计划时不允许登记) */
function openReceive() {
if (!list.value.length) {
ElMessage.warning("请先新建回款计划,回款进度以计划为先导条件");
return;
}
const first = list.value[0];
Object.assign(receiveForm, {
payback_id: row.payback_id,
seq: row.seq,
name: row.name,
amount: row.amount,
received_amount: Number(row.received_amount) || 0,
received_date: row.received_date && row.received_date !== "<nil>" ? row.received_date : "",
receipt_url: row.receipt_url || "",
receipt_name: row.receipt_name || "",
payback_id: first.id,
amount: 0,
received_date: todayStr(),
pay_method: first.pay_method || 1,
receipt_url: "",
receipt_name: "",
remark: "",
});
receiveVisible.value = true;
}
/** 切换回款计划:同步该计划的回款方式作为默认值 */
function onPlanChange(pid) {
const p = planOf(pid);
if (p) receiveForm.pay_method = p.pay_method || 1;
}
const uploading = ref(false);
async function handleReceiptUpload(options) {
const file = options.file;
@@ -363,18 +389,28 @@ function clearReceipt() {
}
async function submitReceive() {
if (!receiveForm.payback_id) {
ElMessage.warning("请选择回款计划");
return;
}
if (!(Number(receiveForm.amount) > 0)) {
ElMessage.warning("请填写本次回款金额");
return;
}
receiveSaving.value = true;
try {
await registerPaybackReceive(receiveForm.payback_id, {
seq: receiveForm.seq,
received_amount: Number(receiveForm.received_amount) || 0,
await createPaybackRecord({
payback_id: receiveForm.payback_id,
amount: Number(receiveForm.amount) || 0,
received_date: receiveForm.received_date || "",
pay_method: receiveForm.pay_method,
receipt_url: receiveForm.receipt_url,
receipt_name: receiveForm.receipt_name,
remark: receiveForm.remark,
});
ElMessage.success("回款登记成功");
receiveVisible.value = false;
// 回款计划(含各期已回款金额、总进度)与回款进度明细一并刷新,无需重开抽屉
// 计划的已回款金额与回款进度流水一并刷新,无需重开抽屉
await reload();
emit("saved");
} catch (e) {
@@ -384,6 +420,23 @@ async function submitReceive() {
}
}
/** 删除一条回款记录(会同步回写计划的已回款金额) */
async function handleDeleteRecord(row) {
try {
await ElMessageBox.confirm(
`确定删除 ${row.received_date || "该"} 的这笔回款(¥${formatMoney(row.amount)})吗?`,
"删除确认",
{ type: "warning" }
);
await deletePaybackRecord(row.id);
ElMessage.success("删除成功");
await reload();
emit("saved");
} catch (e) {
if (e !== "cancel") ElMessage.error(e.message || "删除失败");
}
}
defineExpose({ reload });
</script>