优化合同个管理和产品管理相关功能
This commit is contained in:
1 parent
2b1874943f
commit
4455b95e37
92 files changed
+5367
-3144
No files matched your search
@@ -1,143 +1,88 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
:model-value="visible"
|
||||
title="合同详情"
|
||||
size="680px"
|
||||
:title="detail?.contract_name || '合同详情'"
|
||||
direction="rtl"
|
||||
size="900px"
|
||||
:close-on-click-modal="false"
|
||||
@update:model-value="emit('update:visible', $event)"
|
||||
@update:model-value="handleClose"
|
||||
@opened="loadAll"
|
||||
>
|
||||
<div v-loading="loading" class="contract-detail">
|
||||
<template v-if="detail.id">
|
||||
<div class="detail-head">
|
||||
<div class="detail-title">
|
||||
<span class="name">{{ detail.contract_name }}</span>
|
||||
<el-tag :type="contractStatusTag(detail.status)" size="small">
|
||||
{{ contractStatusText(detail.status) }}
|
||||
</el-tag>
|
||||
<el-tag type="primary" size="small" effect="plain">
|
||||
我方·{{ ourRoleText(detail.our_role) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="detail-sub">
|
||||
{{ detail.contract_no }} · {{ contractCategoryText(detail.contract_category) }}
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<el-descriptions :column="2" border style="margin-top: 16px">
|
||||
<el-descriptions-item label="所属项目" :span="2">
|
||||
<template v-if="detail.project_id">
|
||||
<el-tag size="small" type="primary" effect="light">项目合同</el-tag>
|
||||
<span style="margin-left: 8px">{{ detail.project_name }}</span>
|
||||
<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>
|
||||
<template v-else>
|
||||
<el-tag size="small" type="warning" effect="light">无头合同</el-tag>
|
||||
</template>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="签订日期">{{ formatDateOnly(detail.sign_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="项目负责人">{{ detail.owner_name || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="生效日期">{{ formatDateOnly(detail.effective_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="结束日期">{{ formatDateOnly(detail.expire_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="2">{{ detail.remark || "-" }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-divider content-position="left">各方签约主体</el-divider>
|
||||
<el-table :data="detail.parties || []" border size="small">
|
||||
<el-table-column label="角色" width="80" align="center">
|
||||
<template #default="{ row }">{{ partyLabel(row.role) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="签约主体" min-width="160" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.ref_name || "-" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="来源" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="Number(row.ref_type) === 1" size="small" type="success" effect="plain">客户</el-tag>
|
||||
<el-tag v-else-if="Number(row.ref_type) === 2" size="small" type="warning" effect="plain">供应商</el-tag>
|
||||
<span v-else>本公司</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="signer_name" label="签约人" width="100">
|
||||
<template #default="{ row }">{{ row.signer_name || "-" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="signer_phone" label="联系电话" width="120">
|
||||
<template #default="{ row }">{{ row.signer_phone || "-" }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-divider content-position="left">产品清单</el-divider>
|
||||
<el-table :data="detail.products || []" border size="small">
|
||||
<el-table-column type="index" label="#" width="45" align="center" />
|
||||
<el-table-column prop="name" label="产品名称" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="类别" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="productCategoryTag(row.category)" size="small" effect="plain">
|
||||
{{ productCategoryText(row.category) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="unit" label="单位" width="60" align="center" />
|
||||
<el-table-column label="数量" width="80" align="right">
|
||||
<template #default="{ row }">{{ row.quantity ?? "-" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="单价" width="100" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.price) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="小计" width="110" align="right">
|
||||
<template #default="{ row }">
|
||||
{{ formatMoney((Number(row.quantity) || 0) * (Number(row.price) || 0)) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="100" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.remark || "-" }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-divider content-position="left">金额汇总</el-divider>
|
||||
<div class="summary-grid">
|
||||
<div class="summary-card">
|
||||
<span class="label">合同总金额</span>
|
||||
<span class="value primary">¥{{ formatMoney(summary.total_amount) }}</span>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<span class="label">产品总成本</span>
|
||||
<span class="value">¥{{ formatMoney(summary.total_cost) }}</span>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<span class="label">合同总利润</span>
|
||||
<span class="value success">¥{{ formatMoney(summary.total_profit) }}</span>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<span class="label">硬件部分金额</span>
|
||||
<span class="value warning">¥{{ formatMoney(summary.hardware_amount) }}</span>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<span class="label">软件部分金额</span>
|
||||
<span class="value primary">¥{{ formatMoney(summary.software_amount) }}</span>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<span class="label">其他部分金额</span>
|
||||
<span class="value">¥{{ formatMoney(summary.other_amount) }}</span>
|
||||
</div>
|
||||
</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>
|
||||
</template>
|
||||
</div>
|
||||
<el-tabs v-if="detail.id" v-model="activeTab" tab-position="left" class="detail-tabs">
|
||||
<!-- 基本信息(拆分自原详情内容) -->
|
||||
<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"
|
||||
/>
|
||||
</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, computed, watch } from "vue";
|
||||
import { getContractDetail } from "@/api/crmContract";
|
||||
import { PARTY_ROLES, buildSummary } from "./utils";
|
||||
import { ref, reactive, watch } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { Edit, Delete, ArrowDown } from "@element-plus/icons-vue";
|
||||
import {
|
||||
formatMoney,
|
||||
formatDateOnly,
|
||||
contractCategoryText,
|
||||
ourRoleText,
|
||||
getContractDetail,
|
||||
changeContractStatus,
|
||||
deleteContract,
|
||||
} from "@/api/crmContract";
|
||||
import {
|
||||
CONTRACT_STATUS_OPTIONS,
|
||||
contractStatusText,
|
||||
contractStatusTag,
|
||||
productCategoryText,
|
||||
productCategoryTag,
|
||||
} 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 },
|
||||
@@ -145,23 +90,29 @@ const props = defineProps({
|
||||
contract: { type: Object, default: null },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:visible"]);
|
||||
const emit = defineEmits(["update:visible", "refresh", "edit"]);
|
||||
|
||||
// 合同操作日志与项目共用关联类型 3
|
||||
const RELATED_TYPE = 3;
|
||||
// 电子合同附件专属关联类型(与线索 1 / 商机 2 / 项目 3 区分)
|
||||
const E_CONTRACT_TYPE = 4;
|
||||
|
||||
const loading = ref(false);
|
||||
const detail = ref({});
|
||||
const activeTab = ref("basic");
|
||||
const counts = reactive({ payback: 0, econtract: 0 });
|
||||
|
||||
const summary = computed(() => {
|
||||
const s = detail.value.summary;
|
||||
if (s && Object.keys(s).length) return s;
|
||||
return buildSummary(detail.value.products || []);
|
||||
});
|
||||
|
||||
const partyLabel = (role) => PARTY_ROLES.find((r) => r.key === role)?.label || role || "-";
|
||||
const paybackRef = ref();
|
||||
const logRef = ref();
|
||||
const econtractRef = ref();
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
async (val) => {
|
||||
if (!val) return;
|
||||
activeTab.value = "basic";
|
||||
counts.payback = 0;
|
||||
counts.econtract = 0;
|
||||
detail.value = {};
|
||||
if (!props.contract?.id) return;
|
||||
loading.value = true;
|
||||
@@ -175,6 +126,56 @@ watch(
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
function handleClose() {
|
||||
emit("update:visible", false);
|
||||
}
|
||||
|
||||
/** 抽屉打开后刷新各 Tab 数据 */
|
||||
function loadAll() {
|
||||
if (!detail.value?.id) return;
|
||||
paybackRef.value?.reload();
|
||||
logRef.value?.reload();
|
||||
econtractRef.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>
|
||||
@@ -182,58 +183,72 @@ watch(
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.detail-head {
|
||||
.detail-title {
|
||||
.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;
|
||||
|
||||
.name {
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.detail-sub {
|
||||
margin-top: 6px;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
:deep(.el-drawer__body) {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
padding: 12px 16px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 8px;
|
||||
background: var(--el-fill-color-extra-light);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
.detail-tabs {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
|
||||
.label {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
: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;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
|
||||
&.primary {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
&.success {
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
&.warning {
|
||||
color: var(--el-color-warning);
|
||||
}
|
||||
.el-button + .el-button {
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user