Files
yunzerwebsiteallinone/backend/src/views/apps/crm/contract/components/detail.vue
T
2026-11-11 08:53:14 +08:00

296 lines
8.3 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>
<el-drawer
:model-value="visible"
:title="detail?.contract_name || '合同详情'"
direction="rtl"
size="900px"
:close-on-click-modal="false"
@update:model-value="handleClose"
@opened="loadAll"
>
<div v-loading="loading" class="contract-detail">
<div v-if="detail.id" class="detail-toolbar">
<div class="toolbar-status">
<!-- <el-tag :type="contractStatusTag(detail.status)" size="small">{{ contractStatusText(detail.status) }}</el-tag> -->
</div>
<div class="toolbar-actions">
<el-dropdown trigger="click" @command="(cmd) => handleChangeStatus(cmd)">
<el-button link type="warning" size="small">
状态流转<el-icon style="margin-left: 2px"><ArrowDown /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="i in statusOptionsExcept(detail.status)"
:key="i.value"
:command="i.value"
>
{{ i.label }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-button type="primary" size="small" :icon="Edit" @click="handleEdit">编辑</el-button>
<el-button type="danger" size="small" :icon="Delete" @click="handleDelete">删除</el-button>
</div>
</div>
<el-tabs
v-if="detail.id"
v-model="activeTab"
tab-position="left"
class="detail-tabs"
@tab-change="handleTabChange"
>
<!-- 基本信息(拆分自原详情内容) -->
<el-tab-pane label="基本信息" name="basic">
<DetailBasic :detail="detail" />
</el-tab-pane>
<!-- 回款计划(针对本合同创建 / 查看回款计划) -->
<el-tab-pane :label="`回款计划 (${counts.payback})`" name="payback">
<DetailPayback ref="paybackRef" :contract="detail" @count="counts.payback = $event" @saved="emit('refresh')" />
</el-tab-pane>
<!-- 电子合同(上传 / 查看合同扫描件、电子合同正本) -->
<el-tab-pane :label="`电子合同 (${counts.econtract})`" name="econtract">
<DetailAttach
ref="econtractRef"
:related-id="detail.id"
:related-type="E_CONTRACT_TYPE"
tip="上传合同扫描件 / 电子合同正本,支持图片与 PDF"
@count="counts.econtract = $event"
/>
</el-tab-pane>
<!-- 发票管理(上传发票、在线预览,与合同关联) -->
<el-tab-pane :label="`发票管理 (${counts.invoice})`" name="invoice">
<DetailAttach
ref="invoiceRef"
:related-id="detail.id"
:related-type="INVOICE_TYPE"
tip="上传销售 / 采购发票(图片或 PDF),点击可在线预览"
@count="counts.invoice = $event"
/>
</el-tab-pane>
<!-- 操作日志 -->
<el-tab-pane label="操作日志" name="log">
<DetailLog ref="logRef" :related-id="detail.id" :related-type="RELATED_TYPE" />
</el-tab-pane>
</el-tabs>
<el-empty v-else-if="!loading" description="暂无数据" />
</div>
</el-drawer>
</template>
<script setup>
import { ref, reactive, watch, nextTick } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { Edit, Delete, ArrowDown } from "@element-plus/icons-vue";
import {
getContractDetail,
changeContractStatus,
deleteContract,
} from "@/api/crmContract";
import {
CONTRACT_STATUS_OPTIONS,
contractStatusText,
contractStatusTag,
} from "../../dict";
import DetailBasic from "./detail_basic.vue";
import DetailPayback from "./detail_payback.vue";
import DetailLog from "../../project/components/detail_log.vue";
import DetailAttach from "../../project/components/detail_attach.vue";
const props = defineProps({
visible: { type: Boolean, default: false },
/** 行数据(含 id),详情从接口拉取完整数据 */
contract: { type: Object, default: null },
});
const emit = defineEmits(["update:visible", "refresh", "edit"]);
// 合同操作日志独立关联类型 6(与项目 3 解耦,避免 ID 撞车时日志串数据)
const RELATED_TYPE = 6;
// 电子合同附件专属关联类型(与线索 1 / 商机 2 / 项目 3 区分)
const E_CONTRACT_TYPE = 4;
// 发票管理附件专属关联类型
const INVOICE_TYPE = 5;
const loading = ref(false);
const detail = ref({});
const activeTab = ref("basic");
const counts = reactive({ payback: 0, econtract: 0, invoice: 0 });
const paybackRef = ref();
const logRef = ref();
const econtractRef = ref();
const invoiceRef = ref();
// 同时监听展开状态与目标合同 id:
// 页面被 keep-alive 缓存时,从其他入口再打开另一份合同时 visible 可能一直是 true(未变化),
// 只监听 visible 会导致不重新加载、内容串成上一份合同。
watch(
[() => props.visible, () => props.contract?.id],
async ([val]) => {
if (!val) return;
activeTab.value = "basic";
counts.payback = 0;
counts.econtract = 0;
counts.invoice = 0;
detail.value = {};
if (!props.contract?.id) return;
loading.value = true;
try {
const res = await getContractDetail(props.contract.id);
detail.value = res?.data || props.contract;
} catch {
detail.value = props.contract;
} finally {
loading.value = false;
}
// 抽屉已打开时不会再触发 @opened,这里主动刷新各 Tab 数据(回款 / 附件 / 日志)
await nextTick();
loadAll();
}
);
function handleClose() {
emit("update:visible", false);
}
/** 抽屉打开后刷新各 Tab 数据 */
function loadAll() {
if (!detail.value?.id) return;
paybackRef.value?.reload();
logRef.value?.reload();
econtractRef.value?.reload();
invoiceRef.value?.reload();
}
/** 切换 Tab 时刷新对应面板数据,保证与数量角标一致 */
function handleTabChange(name) {
const refMap = {
payback: paybackRef,
econtract: econtractRef,
invoice: invoiceRef,
log: logRef,
};
refMap[name]?.value?.reload();
}
/** 状态流转候选:排除当前状态 */
function statusOptionsExcept(cur) {
return CONTRACT_STATUS_OPTIONS.filter((i) => String(i.value) !== String(cur));
}
async function handleChangeStatus(status) {
try {
await changeContractStatus(detail.value.id, Number(status));
ElMessage.success(`状态已更新为「${contractStatusText(status)}」`);
detail.value.status = Number(status);
emit("refresh");
} catch (e) {
ElMessage.error(e.message || "状态更新失败");
}
}
function handleEdit() {
emit("edit", detail.value);
}
async function handleDelete() {
try {
await ElMessageBox.confirm(
`确定删除合同「${detail.value.contract_name}」吗?删除后不可恢复。`,
"删除确认",
{ type: "warning" }
);
await deleteContract(detail.value.id);
ElMessage.success("删除成功");
emit("update:visible", false);
emit("refresh");
} catch (e) {
if (e !== "cancel") ElMessage.error(e.message || "删除失败");
}
}
defineExpose({ loadAll });
</script>
<style lang="less" scoped>
.contract-detail {
padding: 0 4px;
}
.detail-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px 4px 12px;
margin-bottom: 8px;
border-bottom: 1px solid var(--el-border-color-lighter);
.toolbar-status {
display: flex;
align-items: center;
gap: 10px;
}
.toolbar-actions {
display: flex;
align-items: center;
gap: 8px;
}
}
:deep(.el-drawer__body) {
overflow-x: hidden;
}
.detail-tabs {
display: flex;
width: 100%;
min-width: 0;
:deep(.el-tabs__content) {
flex: 1;
min-width: 0;
overflow: hidden;
padding-left: 4px;
}
:deep(.el-tabs__header) {
margin-right: 12px;
flex-shrink: 0;
}
:deep(.el-tabs__item) {
justify-content: flex-start;
text-align: left;
padding: 0 12px;
min-width: 96px;
font-size: 13px;
}
:deep(.el-tab-pane) {
width: 100%;
min-width: 0;
}
:deep(.el-table) {
width: 100%;
.el-button.is-link {
padding: 0;
height: auto;
min-height: auto;
}
.el-button + .el-button {
margin-left: 6px;
}
}
}
</style>