新增合同管理相关功能
This commit is contained in:
@@ -0,0 +1,371 @@
|
||||
<template>
|
||||
<div class="crm-page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>合同管理</h2>
|
||||
<p>进度式创建合同,绑定项目或无头合同,管理甲乙丙丁各方与产品清单</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button :icon="Refresh" @click="fetchList">刷新</el-button>
|
||||
<el-button type="primary" :icon="Plus" @click="openCreate">新建合同</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="stat-cards">
|
||||
<div class="stat-card">
|
||||
<span class="stat-label">合同总数</span>
|
||||
<span class="stat-value">{{ pagination.total }}</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-label">合同总金额</span>
|
||||
<span class="stat-value primary">¥{{ formatMoney(stats.total_amount) }}</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-label">产品总成本</span>
|
||||
<span class="stat-value">¥{{ formatMoney(stats.total_cost) }}</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-label">合同总利润</span>
|
||||
<span class="stat-value success">¥{{ formatMoney(stats.total_profit) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filter-bar">
|
||||
<el-form :inline="true" :model="filters" @submit.prevent>
|
||||
<el-form-item>
|
||||
<el-input
|
||||
v-model="filters.keyword"
|
||||
clearable
|
||||
placeholder="搜索合同名称 / 编号 / 项目 / 签约方"
|
||||
:prefix-icon="Search"
|
||||
style="width: 260px"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="我方角色">
|
||||
<el-select v-model="filters.our_role" clearable placeholder="全部" style="width: 110px">
|
||||
<el-option v-for="i in OUR_ROLE_OPTIONS" :key="i.value" :label="i.label" :value="i.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="合同分类">
|
||||
<el-select v-model="filters.contract_category" clearable placeholder="全部" style="width: 130px">
|
||||
<el-option v-for="i in CONTRACT_CATEGORY_OPTIONS" :key="i.value" :label="i.label" :value="i.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="归属">
|
||||
<el-select v-model="filters.project_type" clearable placeholder="全部" style="width: 130px">
|
||||
<el-option label="项目合同" value="project" />
|
||||
<el-option label="无头合同" value="headless" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="filters.status" clearable placeholder="全部" style="width: 110px">
|
||||
<el-option v-for="i in CONTRACT_STATUS_OPTIONS" :key="i.value" :label="i.label" :value="i.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :icon="Search" @click="handleSearch">查询</el-button>
|
||||
<el-button :icon="Refresh" @click="resetFilters">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="table-container" v-loading="loading">
|
||||
<el-table :data="tableData" stripe border row-key="id">
|
||||
<el-table-column prop="contract_no" label="合同编号" width="150" show-overflow-tooltip />
|
||||
<el-table-column label="合同名称" min-width="190" show-overflow-tooltip fixed>
|
||||
<template #default="{ row }">
|
||||
<span class="name-link" @click="openRow(row)">
|
||||
{{ row.contract_name }}
|
||||
<el-tag v-if="Number(row.status) === 1" size="small" type="warning" effect="light">草稿</el-tag>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="我方角色" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag type="primary" size="small" effect="plain">{{ ourRoleText(row.our_role) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="分类" width="100" align="center">
|
||||
<template #default="{ row }">{{ contractCategoryText(row.contract_category) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="归属项目" min-width="160" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ row.project_name || "无" }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="甲方" min-width="150" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ partyName(row, "party_a") }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="乙方" min-width="150" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ partyName(row, "party_b") }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="合同总金额" width="130" align="right">
|
||||
<template #default="{ row }">
|
||||
{{ formatMoney(row.summary?.total_amount ?? row.total_amount) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="合同总利润" width="130" align="right">
|
||||
<template #default="{ row }">
|
||||
{{ formatMoney(row.summary?.total_profit ?? row.total_profit) }}
|
||||
</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="110" align="center">
|
||||
<template #default="{ row }">{{ formatDateOnly(row.sign_date) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="contractStatusTag(row.status)" size="small">
|
||||
{{ contractStatusText(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="openEdit(row)">
|
||||
{{ Number(row.status) === 1 ? "继续填写" : "编辑" }}
|
||||
</el-button>
|
||||
<!-- 状态流转:草稿 / 已完成 / 履约中 / 执行异常 / 已作废 互切 -->
|
||||
<el-dropdown trigger="click" @command="(cmd) => handleChangeStatus(row, 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(row)"
|
||||
:key="i.value"
|
||||
:command="i.value"
|
||||
>
|
||||
{{ i.label }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-button link type="danger" size="small" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty><el-empty description="暂无合同" :image-size="80" /></template>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSearch"
|
||||
@current-change="fetchList"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 进度式创建 / 编辑向导 -->
|
||||
<ContractCreate
|
||||
v-model:visible="createVisible"
|
||||
:edit-data="currentRow"
|
||||
:init-step="initStep"
|
||||
@success="fetchList"
|
||||
/>
|
||||
|
||||
<!-- 合同详情 -->
|
||||
<ContractDetail v-model:visible="detailVisible" :contract="currentRow" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { Plus, Search, Refresh, ArrowDown } from "@element-plus/icons-vue";
|
||||
import {
|
||||
getContractList,
|
||||
getContractStats,
|
||||
changeContractStatus,
|
||||
deleteContract,
|
||||
} from "@/api/crmContract";
|
||||
import ContractCreate from "./components/create.vue";
|
||||
import ContractDetail from "./components/detail.vue";
|
||||
import {
|
||||
CONTRACT_CATEGORY_OPTIONS,
|
||||
OUR_ROLE_OPTIONS,
|
||||
CONTRACT_STATUS_OPTIONS,
|
||||
ourRoleText,
|
||||
contractCategoryText,
|
||||
contractStatusText,
|
||||
contractStatusTag,
|
||||
formatDateOnly,
|
||||
formatMoney,
|
||||
} from "../dict";
|
||||
|
||||
const loading = ref(false);
|
||||
const tableData = ref([]);
|
||||
const createVisible = ref(false);
|
||||
const detailVisible = ref(false);
|
||||
const currentRow = ref(null);
|
||||
const initStep = ref(1);
|
||||
|
||||
const filters = reactive({
|
||||
keyword: "",
|
||||
our_role: "",
|
||||
contract_category: "",
|
||||
project_type: "",
|
||||
status: "",
|
||||
});
|
||||
const pagination = reactive({ page: 1, pageSize: 20, total: 0 });
|
||||
|
||||
/** 列表页统计:后端全租户汇总(排除已作废) */
|
||||
const stats = ref({ total_amount: 0, total_cost: 0, total_profit: 0 });
|
||||
|
||||
async function loadStats() {
|
||||
try {
|
||||
const res = await getContractStats();
|
||||
stats.value = res?.data || stats.value;
|
||||
} catch {
|
||||
/* 统计失败不影响列表 */
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchList();
|
||||
loadStats();
|
||||
});
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getContractList({
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
...filters,
|
||||
});
|
||||
tableData.value = res?.data?.list || [];
|
||||
pagination.total = res?.data?.total || 0;
|
||||
loadStats(); // 新增/删除后统计随之刷新
|
||||
} catch (e) {
|
||||
tableData.value = [];
|
||||
pagination.total = 0;
|
||||
ElMessage.error(e.message || "查询失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
filters.keyword = "";
|
||||
filters.our_role = "";
|
||||
filters.contract_category = "";
|
||||
filters.project_type = "";
|
||||
filters.status = "";
|
||||
handleSearch();
|
||||
}
|
||||
|
||||
/** 从行数据的 parties 里取某一方签约主体名称 */
|
||||
function partyName(row, role) {
|
||||
const hit = (row.parties || []).find((p) => p.role === role);
|
||||
return hit?.ref_name || "-";
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
currentRow.value = null;
|
||||
initStep.value = 1;
|
||||
createVisible.value = true;
|
||||
}
|
||||
|
||||
/** 草稿:按保存的进度定位步骤;已完成:从第一步编辑 */
|
||||
function openEdit(row) {
|
||||
currentRow.value = { ...row };
|
||||
initStep.value = Number(row.step) === 2 ? 2 : 1;
|
||||
createVisible.value = true;
|
||||
}
|
||||
|
||||
function openDetail(row) {
|
||||
currentRow.value = { ...row };
|
||||
detailVisible.value = true;
|
||||
}
|
||||
|
||||
/** 点击名称:草稿继续填写,已完成看详情 */
|
||||
function openRow(row) {
|
||||
if (Number(row.status) === 1) openEdit(row);
|
||||
else openDetail(row);
|
||||
}
|
||||
|
||||
/** 状态流转候选:排除当前状态 */
|
||||
function statusOptionsExcept(row) {
|
||||
return CONTRACT_STATUS_OPTIONS.filter((i) => String(i.value) !== String(row.status));
|
||||
}
|
||||
|
||||
async function handleChangeStatus(row, status) {
|
||||
try {
|
||||
await changeContractStatus(row.id, Number(status));
|
||||
ElMessage.success(`状态已更新为「${contractStatusText(status)}」`);
|
||||
fetchList();
|
||||
} catch (e) {
|
||||
ElMessage.error(e.message || "状态更新失败");
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定删除合同「${row.contract_name}」吗?删除后不可恢复。`,
|
||||
"删除确认",
|
||||
{ type: "warning" }
|
||||
);
|
||||
await deleteContract(row.id);
|
||||
ElMessage.success("删除成功");
|
||||
fetchList();
|
||||
} catch (e) {
|
||||
if (e !== "cancel") ElMessage.error(e.message || "删除失败");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped src="../styles/crm-page.less"></style>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.stat-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.stat-card {
|
||||
background: var(--el-bg-color);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 6px;
|
||||
padding: 14px 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: var(--el-text-color-primary);
|
||||
|
||||
&.primary {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
&.success {
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user