502 lines
16 KiB
Vue
502 lines
16 KiB
Vue
<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>
|