322 lines
9.9 KiB
Vue
322 lines
9.9 KiB
Vue
<template>
|
||
<el-drawer
|
||
:model-value="visible"
|
||
:title="project?.project_name || '项目详情'"
|
||
direction="rtl"
|
||
size="1200px"
|
||
@update:model-value="handleClose"
|
||
@opened="loadAll"
|
||
>
|
||
<div v-if="project" class="project-detail">
|
||
<div class="detail-toolbar">
|
||
<div class="toolbar-status">
|
||
<el-tag :type="projectStatusTag(project.status)" size="small">{{ projectStatusText(project.status) }}</el-tag>
|
||
<el-tag v-if="projectOverdue(project).overdue" type="danger" size="small" effect="dark">
|
||
{{ projectOverdue(project).text }}
|
||
</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(project.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-model="activeTab" tab-position="left" class="detail-tabs">
|
||
<!-- 基本信息 -->
|
||
<el-tab-pane label="基本信息" name="basic">
|
||
<DetailBasic :project="project" />
|
||
</el-tab-pane>
|
||
|
||
<!-- 合同管理(一个项目可关联多份合同,横向 Tab 切换查看) -->
|
||
<el-tab-pane :label="`合同管理 (${counts.contract})`" name="contract">
|
||
<DetailContract
|
||
ref="contractRef"
|
||
:project-id="project.id"
|
||
:project-name="project.project_name || ''"
|
||
:project-type="project.project_type"
|
||
@count="counts.contract = $event"
|
||
/>
|
||
</el-tab-pane>
|
||
|
||
<!-- 跟进记录(链路累积:线索 + 商机 + 项目) -->
|
||
<el-tab-pane :label="`跟进记录 (${counts.follow})`" name="follow">
|
||
<DetailFollow
|
||
ref="followRef"
|
||
:related-id="project.id"
|
||
:related-type="RELATED_TYPE"
|
||
:related-name="project.project_name || ''"
|
||
:include-source="true"
|
||
@count="counts.follow = $event"
|
||
@saved="emit('refresh')"
|
||
/>
|
||
</el-tab-pane>
|
||
|
||
<!-- 联系人(按项目类型分组:从对应企业通讯录中挑选,支持多选) -->
|
||
<el-tab-pane :label="`联系人 (${counts.contact})`" name="contact">
|
||
<DetailContact ref="contactRef" :project="project" @count="counts.contact = $event" />
|
||
</el-tab-pane>
|
||
|
||
<!-- 项目文档(同步到文档库:共享文档 / 项目文档 / {项目名称}) -->
|
||
<el-tab-pane :label="`项目文档 (${counts.docs})`" name="docs">
|
||
<DetailDocs
|
||
ref="docsRef"
|
||
:project-id="project.id"
|
||
:project-name="project.project_name || ''"
|
||
@count="counts.docs = $event"
|
||
/>
|
||
</el-tab-pane>
|
||
|
||
<!-- 附件(链路累积:线索 + 商机 + 项目) -->
|
||
<el-tab-pane :label="`附件 (${counts.attach})`" name="attach">
|
||
<DetailAttach
|
||
ref="attachRef"
|
||
:related-id="project.id"
|
||
:related-type="RELATED_TYPE"
|
||
:include-source="true"
|
||
:show-stage="true"
|
||
@count="counts.attach = $event"
|
||
/>
|
||
</el-tab-pane>
|
||
|
||
<!-- 操作日志 -->
|
||
<el-tab-pane label="操作日志" name="log">
|
||
<DetailLog ref="logRef" :related-id="project.id" :related-type="RELATED_TYPE" />
|
||
</el-tab-pane>
|
||
</el-tabs>
|
||
</div>
|
||
</el-drawer>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, watch } from "vue";
|
||
import { ElMessage, ElMessageBox } from "element-plus";
|
||
import { Edit, Delete, ArrowDown } from "@element-plus/icons-vue";
|
||
import { deleteProject, changeProjectStatus, getProjectDetail } from "@/api/crmPipeline";
|
||
import DetailBasic from "./detail_basic.vue";
|
||
import DetailContact from "./detail_contact.vue";
|
||
import DetailFollow from "./detail_follow.vue";
|
||
import DetailContract from "./detail_contract.vue";
|
||
import DetailAttach from "./detail_attach.vue";
|
||
import DetailDocs from "./detail_docs.vue";
|
||
import DetailLog from "./detail_log.vue";
|
||
import {
|
||
PROJECT_STATUS_OPTIONS,
|
||
projectStatusText,
|
||
projectStatusTag,
|
||
projectOverdue,
|
||
} from "../../dict";
|
||
|
||
const props = defineProps({
|
||
visible: { type: Boolean, default: false },
|
||
project: { type: Object, default: null },
|
||
});
|
||
const emit = defineEmits(["update:visible", "refresh", "edit", "delete"]);
|
||
|
||
// 关联类型:3=项目
|
||
const RELATED_TYPE = 3;
|
||
|
||
const activeTab = ref("basic");
|
||
const counts = reactive({ contact: 0, follow: 0, contract: 0, attach: 0, docs: 0 });
|
||
|
||
const contactRef = ref();
|
||
const followRef = ref();
|
||
const contractRef = ref();
|
||
const attachRef = ref();
|
||
const docsRef = ref();
|
||
const logRef = ref();
|
||
|
||
watch(
|
||
() => props.visible,
|
||
(val) => {
|
||
if (val) {
|
||
activeTab.value = "basic";
|
||
counts.contact = 0;
|
||
counts.follow = 0;
|
||
counts.contract = 0;
|
||
counts.attach = 0;
|
||
counts.docs = 0;
|
||
}
|
||
}
|
||
);
|
||
|
||
function handleClose() {
|
||
emit("update:visible", false);
|
||
}
|
||
|
||
function handleEdit() {
|
||
emit("edit", props.project);
|
||
}
|
||
|
||
/** 状态流转候选:排除当前状态 */
|
||
function statusOptionsExcept(cur) {
|
||
return PROJECT_STATUS_OPTIONS.filter((i) => String(i.value) !== String(cur));
|
||
}
|
||
|
||
/**
|
||
* 项目状态流转:成功后更新本地展示并通知父组件刷新列表。
|
||
* 置为「异常」时先询问原因,原因会写入操作日志,便于追溯是什么导致的。
|
||
*/
|
||
async function handleChangeStatus(status) {
|
||
if (!props.project?.id) return;
|
||
let reason = "";
|
||
if (Number(status) === 5) {
|
||
try {
|
||
const { value } = await ElMessageBox.prompt(
|
||
"请填写项目异常原因,该原因会记入操作日志",
|
||
"异常原因",
|
||
{
|
||
confirmButtonText: "确定",
|
||
cancelButtonText: "取消",
|
||
inputPlaceholder: "如:客户预算取消 / 供应商延期交付",
|
||
inputValidator: (v) => (String(v || "").trim() ? true : "异常原因不能为空"),
|
||
}
|
||
);
|
||
reason = String(value || "").trim();
|
||
} catch (e) {
|
||
return; // 用户取消:不流转状态
|
||
}
|
||
}
|
||
try {
|
||
await changeProjectStatus(props.project.id, Number(status), reason);
|
||
ElMessage.success(`状态已更新为「${projectStatusText(status)}」`);
|
||
props.project.status = Number(status);
|
||
emit("refresh");
|
||
} catch (e) {
|
||
ElMessage.error(e.message || "状态更新失败");
|
||
}
|
||
}
|
||
|
||
async function handleDelete() {
|
||
try {
|
||
await ElMessageBox.confirm(`确定删除项目「${props.project.project_name}」吗?删除后不可恢复。`, "删除确认", {
|
||
type: "warning",
|
||
});
|
||
await deleteProject(props.project.id);
|
||
ElMessage.success("删除成功");
|
||
emit("update:visible", false);
|
||
emit("delete");
|
||
} catch (e) {
|
||
if (e !== "cancel") ElMessage.error(e.message || "删除失败");
|
||
}
|
||
}
|
||
|
||
/** 抽屉打开后刷新各 Tab 数据 */
|
||
async function loadAll() {
|
||
if (!props.project?.id) return;
|
||
// 列表行数据是打开页面时的快照,联系人选中集合等字段可能已过期(如刚在其他项目/本次会话保存过),
|
||
// 先拉取最新详情刷新快照,各 Tab(含联系人)再按最新数据加载
|
||
try {
|
||
const res = await getProjectDetail(props.project.id);
|
||
const data = res?.data;
|
||
if (data && typeof data === "object" && props.project) {
|
||
Object.assign(props.project, data);
|
||
}
|
||
} catch (e) {
|
||
// 详情接口失败时保持快照展示,不阻断其他 Tab
|
||
}
|
||
contactRef.value?.reload();
|
||
followRef.value?.reload();
|
||
contractRef.value?.reload();
|
||
attachRef.value?.reload();
|
||
docsRef.value?.reload();
|
||
logRef.value?.reload();
|
||
}
|
||
|
||
defineExpose({ loadAll });
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
:deep(.el-drawer__body) {
|
||
overflow-x: hidden;
|
||
}
|
||
|
||
.project-detail {
|
||
.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;
|
||
}
|
||
}
|
||
|
||
.detail-tabs {
|
||
// 横向 flex:导航固定宽 + 内容区占满剩余宽度
|
||
display: flex;
|
||
width: 100%;
|
||
min-width: 0;
|
||
|
||
// 关键:内容区占满「容器剩余宽度」并允许收缩,内部表格才能随容器自适应。
|
||
// 注意不能用 overflow: hidden——它会把附件/日志等较高的内容直接截断,
|
||
// 让内容自然撑高,滚动交给 drawer 自身(el-drawer__body)处理。
|
||
:deep(.el-tabs__content) {
|
||
flex: 1;
|
||
min-width: 0;
|
||
overflow: visible;
|
||
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>
|