Files
yunzerwebsiteallinone/backend/src/views/apps/crm/contract/components/detail_payback.vue
T
2026-09-13 14:09:34 +08:00

502 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="contract-payback">
<div class="section-head">
<span class="section-title">回款计划</span>
<el-tooltip :disabled="!paidOff" content="回款已全部完成(进度 100%),不可再新建回款计划" placement="top">
<span class="btn-wrap">
<el-button type="primary" size="small" :icon="Plus" :disabled="paidOff" @click="openCreate">
新建回款计划
</el-button>
</span>
</el-tooltip>
</div>
<el-table v-loading="loading" :data="list" border size="small">
<el-table-column label="状态" width="90" align="center">
<template #default="{ row }">
<el-tag :type="paybackStatusTag(row.status)" size="small">{{ paybackStatusText(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="回款周期" width="90" align="center">
<template #default="{ row }">
<el-tag size="small" effect="plain">{{ paybackCycleText(row.plan_type) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="回款进度" min-width="140" align="center">
<template #default="{ row }">
<el-progress :percentage="paybackPercent(row)" :stroke-width="10" />
</template>
</el-table-column>
<el-table-column label="计划回款总额" width="130" align="right">
<template #default="{ row }">{{ formatMoney(row.total_amount) }}</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-column label="回款方式" width="100" align="center">
<template #default="{ row }">{{ paybackMethodText(row.pay_method) }}</template>
</el-table-column>
<el-table-column label="负责人" width="90">
<template #default="{ row }">{{ row.owner_user_name || "-" }}</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="openDetail(row)">详情</el-button>
</template>
</el-table-column>
<template #empty>
<el-empty description="该合同暂无回款计划,点击右上角新建" :image-size="60" />
</template>
</el-table>
<!-- ============================ 回款进度(回款记录流水) ============================ -->
<div class="section-head progress-head">
<span class="section-title">回款进度</span>
<div class="progress-summary">
<span>计划回款总额:<b class="primary">¥{{ formatMoney(progressSummary.total_planned) }}</b></span>
<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="list.length > 0" content="回款进度以回款计划为先导条件,请先新建回款计划" placement="top">
<span class="btn-wrap">
<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="records" border size="small" row-key="id">
<el-table-column label="回款日期" width="110" align="center">
<template #default="{ row }">{{ row.received_date || "-" }}</template>
</el-table-column>
<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 }">
<span class="amount">¥{{ formatMoney(row.amount) }}</span>
</template>
</el-table-column>
<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 }">
<el-link v-if="row.receipt_url" type="primary" :href="row.receipt_url" target="_blank" :underline="false">查看回执</el-link>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="备注" min-width="120" show-overflow-tooltip>
<template #default="{ row }">{{ row.remark || "-" }}</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="danger" size="small" @click="handleDeleteRecord(row)">删除</el-button>
</template>
</el-table-column>
<template #empty>
<el-empty description="暂无回款记录,点击右上角「新建回款」登记一笔实收" :image-size="60" />
</template>
</el-table>
<!-- 新建 / 编辑回款计划(预设当前合同) -->
<PaybackCreate
v-model:visible="createVisible"
:preset-contract="contract"
:edit-data="editRow"
@success="handleSaved"
/>
<!-- 回款计划详情 -->
<PaybackDetail v-model:visible="detailVisible" :payback="currentRow" @edit="handleEdit" />
<!-- 新建回款(回款进度登记:以回款计划为先导) -->
<el-dialog v-model="receiveVisible" title="新建回款" width="460px">
<el-form :model="receiveForm" label-width="90px">
<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="本次回款" required>
<el-input-number
v-model="receiveForm.amount"
:min="0"
:precision="2"
:controls="false"
placeholder="0.00"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="回款日期">
<el-date-picker
v-model="receiveForm.received_date"
type="date"
value-format="YYYY-MM-DD"
placeholder="选择日期"
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
:show-file-list="false"
:http-request="handleReceiptUpload"
:disabled="uploading"
accept=".jpg,.jpeg,.png,.pdf,.doc,.docx,.xls,.xlsx"
>
<el-button :icon="Upload" :loading="uploading" size="small">上传流水回执</el-button>
</el-upload>
<div v-if="receiveForm.receipt_url" class="receipt-file">
<el-link type="primary" :href="receiveForm.receipt_url" target="_blank" :underline="false">
<el-icon><Document /></el-icon>
<span class="receipt-name">{{ receiveForm.receipt_name || "流水回执" }}</span>
</el-link>
<el-button link type="danger" size="small" :icon="Delete" @click="clearReceipt">移除</el-button>
</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>
<el-button type="primary" :loading="receiveSaving" @click="submitReceive">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { computed, ref, reactive } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { Plus, Upload, Document, Delete } from "@element-plus/icons-vue";
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,
formatMoney,
} from "../../dict";
const props = defineProps({
/** 当前合同详情对象(含 id / contract_name / contract_no / parties / summary) */
contract: { type: Object, default: null },
});
const emit = defineEmits(["count", "saved"]);
const loading = ref(false);
const list = ref([]);
const createVisible = ref(false);
const detailVisible = ref(false);
const currentRow = ref(null);
const editRow = ref(null);
/** 回款进度:一笔笔实际回款记录(回款流水) */
const progressLoading = ref(false);
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;
if (planned <= 0) return false;
return (Number(progressSummary.value.total_received) || 0) >= planned;
});
/** 回款进度百分比 */
function paybackPercent(row) {
const total = Number(row.total_amount) || 0;
if (!total) return 0;
return Math.min(100, Math.round((Number(row.received_amount) / total) * 100));
}
function openCreate() {
// 兜底拦截:回款已收齐时不允许再建计划
if (paidOff.value) {
ElMessage.warning("回款已全部完成(进度 100%),不可再新建回款计划");
return;
}
editRow.value = null;
createVisible.value = true;
}
function openDetail(row) {
currentRow.value = { ...row };
detailVisible.value = true;
}
/** 详情内点击编辑:关闭详情并打开编辑弹窗 */
function handleEdit(row) {
detailVisible.value = false;
editRow.value = { ...row };
createVisible.value = true;
}
function handleSaved() {
reload();
emit("saved");
}
/** 供父组件(detail.vue)在抽屉打开时调用 */
async function reload() {
const contractId = props.contract?.id;
if (!contractId) {
list.value = [];
records.value = [];
progressSummary.value = { total_planned: 0, total_received: 0 };
emit("count", 0);
return;
}
loading.value = true;
try {
const res = await getPaybackList({ contract_id: contractId, page: 1, pageSize: 100 });
list.value = res?.data?.list || [];
} catch (e) {
list.value = [];
} finally {
loading.value = false;
emit("count", list.value.length);
}
await fetchRecords();
}
/** 拉取本合同回款记录(回款进度流水,一笔一条) */
async function fetchRecords() {
const contractId = props.contract?.id;
if (!contractId) return;
progressLoading.value = true;
try {
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) {
records.value = [];
progressSummary.value = { total_planned: 0, total_received: 0 };
ElMessage.error(e.message || "查询回款进度失败");
} finally {
progressLoading.value = false;
}
}
/* ---------------------------- 新建回款(回款进度登记) ---------------------------- */
const receiveVisible = ref(false);
const receiveSaving = ref(false);
const receiveForm = reactive({
payback_id: null,
amount: 0,
received_date: "",
pay_method: 1,
receipt_url: "",
receipt_name: "",
remark: "",
});
/** 新建回款:以回款计划为先导条件(无计划时不允许登记) */
function openReceive() {
if (!list.value.length) {
ElMessage.warning("请先新建回款计划,回款进度以计划为先导条件");
return;
}
const first = list.value[0];
Object.assign(receiveForm, {
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;
uploading.value = true;
try {
const fd = new FormData();
fd.append("file", file);
const res = await uploadFile(fd);
const data = res?.data || {};
receiveForm.receipt_url = data.url || "";
receiveForm.receipt_name = data.name || file.name;
ElMessage.success("上传成功");
} catch (e) {
ElMessage.error(e.message || "上传失败");
} finally {
uploading.value = false;
}
}
function clearReceipt() {
receiveForm.receipt_url = "";
receiveForm.receipt_name = "";
}
async function submitReceive() {
if (!receiveForm.payback_id) {
ElMessage.warning("请选择回款计划");
return;
}
if (!(Number(receiveForm.amount) > 0)) {
ElMessage.warning("请填写本次回款金额");
return;
}
receiveSaving.value = true;
try {
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) {
ElMessage.error(e.message || "登记失败");
} finally {
receiveSaving.value = false;
}
}
/** 删除一条回款记录(会同步回写计划的已回款金额) */
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>
<style lang="less" scoped>
.section-head {
display: flex;
justify-content: space-between;
align-items: center;
margin: 18px 0 12px;
.section-title {
font-size: 14px;
font-weight: 600;
}
}
.progress-head {
margin-top: 22px;
gap: 12px;
flex-wrap: wrap;
.progress-summary {
display: flex;
flex: 1;
justify-content: center;
gap: 18px;
font-size: 13px;
color: #606266;
b {
font-weight: 600;
}
.primary { color: #409eff; }
.success { color: #67c23a; }
.warning { color: #e6a23c; }
}
}
.btn-wrap {
display: inline-flex;
}
.amount {
font-weight: 600;
color: #409eff;
}
.receipt-block {
width: 100%;
.receipt-file {
margin-top: 8px;
display: flex;
align-items: center;
gap: 8px;
.receipt-name {
margin-left: 4px;
}
}
}
</style>