296 lines
8.3 KiB
Vue
296 lines
8.3 KiB
Vue
<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>
|