优化合同个管理和产品管理相关功能
This commit is contained in:
@@ -11,23 +11,44 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计周期 -->
|
||||
<div class="stat-period-bar">
|
||||
<el-radio-group v-model="statsPeriod" @change="onPeriodChange">
|
||||
<el-radio-button label="month">本月</el-radio-button>
|
||||
<el-radio-button label="quarter">本季</el-radio-button>
|
||||
<el-radio-button label="year">本年</el-radio-button>
|
||||
<el-radio-button label="custom">自定义</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-date-picker
|
||||
v-if="statsPeriod === 'custom'"
|
||||
v-model="statsRange"
|
||||
type="daterange"
|
||||
value-format="YYYY-MM-DD"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
:clearable="false"
|
||||
@change="loadStats"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="stat-cards">
|
||||
<div class="stat-card">
|
||||
<span class="stat-label">合同总数</span>
|
||||
<span class="stat-value">{{ pagination.total }}</span>
|
||||
<span class="stat-value">{{ stats.total ?? 0 }}</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-label">合同总金额</span>
|
||||
<span class="stat-value primary">¥{{ formatMoney(stats.total_amount) }}</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>
|
||||
<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>
|
||||
<span class="stat-value" :style="profitStyle(stats.total_profit)">{{ profitText(stats.total_profit) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -109,40 +130,25 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="合同总利润" width="130" align="right">
|
||||
<template #default="{ row }">
|
||||
{{ formatMoney(row.summary?.total_profit ?? row.total_profit) }}
|
||||
<span :style="profitStyle(row.summary?.total_profit ?? row.total_profit)">
|
||||
{{ profitText(row.summary?.total_profit ?? row.total_profit) }}
|
||||
</span>
|
||||
</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="190" align="center" class-name="op-col" fixed="right">
|
||||
<el-table-column label="逾期" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<!-- 状态流转:草稿 / 已完成 / 履约中 / 执行异常 / 已作废 互切 -->
|
||||
<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="primary" size="small" @click="openEdit(row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button link type="danger" size="small" @click="handleDelete(row)">删除</el-button>
|
||||
<el-tag v-if="contractOverdue(row).overdue" type="danger" size="small" effect="dark">
|
||||
{{ overdueText(contractOverdue(row).days) }}
|
||||
</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="负责人" width="90">
|
||||
<template #default="{ row }">{{ row.owner_user_name || "-" }}</template>
|
||||
</el-table-column>
|
||||
<template #empty><el-empty description="暂无合同" :image-size="80" /></template>
|
||||
</el-table>
|
||||
|
||||
@@ -168,19 +174,22 @@
|
||||
/>
|
||||
|
||||
<!-- 合同详情 -->
|
||||
<ContractDetail v-model:visible="detailVisible" :contract="currentRow" />
|
||||
<ContractDetail
|
||||
v-model:visible="detailVisible"
|
||||
:contract="currentRow"
|
||||
@refresh="fetchList"
|
||||
@edit="onDetailEdit"
|
||||
/>
|
||||
</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 { Plus, Search, Refresh } 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";
|
||||
@@ -192,6 +201,8 @@ import {
|
||||
contractCategoryText,
|
||||
contractStatusText,
|
||||
contractStatusTag,
|
||||
contractOverdue,
|
||||
overdueText,
|
||||
formatDateOnly,
|
||||
formatMoney,
|
||||
} from "../dict";
|
||||
@@ -212,18 +223,39 @@ const filters = reactive({
|
||||
});
|
||||
const pagination = reactive({ page: 1, pageSize: 20, total: 0 });
|
||||
|
||||
/** 列表页统计:后端全租户汇总(排除已作废) */
|
||||
const stats = ref({ total_amount: 0, total_cost: 0, total_profit: 0 });
|
||||
/** 统计周期:month/quarter/year/custom(自定义选区间) */
|
||||
const statsPeriod = ref("month");
|
||||
const statsRange = ref([]);
|
||||
|
||||
/** 列表页统计:后端按周期汇总(排除已作废),周期参数透传给 Stats */
|
||||
const stats = ref({ total: 0, total_amount: 0, total_cost: 0, total_profit: 0 });
|
||||
|
||||
async function loadStats() {
|
||||
try {
|
||||
const res = await getContractStats();
|
||||
const params = { period: statsPeriod.value };
|
||||
if (statsPeriod.value === "custom" && statsRange.value && statsRange.value.length === 2) {
|
||||
params.start_date = statsRange.value[0];
|
||||
params.end_date = statsRange.value[1];
|
||||
}
|
||||
const res = await getContractStats(params);
|
||||
stats.value = res?.data || stats.value;
|
||||
} catch {
|
||||
/* 统计失败不影响列表 */
|
||||
}
|
||||
}
|
||||
|
||||
/** 切到「自定义」首次未选区间时,默认当前月,避免卡片空值 */
|
||||
function onPeriodChange(val) {
|
||||
if (val === "custom" && (!statsRange.value || statsRange.value.length !== 2)) {
|
||||
const now = new Date();
|
||||
const y = now.getFullYear();
|
||||
const m = String(now.getMonth() + 1).padStart(2, "0");
|
||||
const last = new Date(y, now.getMonth() + 1, 0).getDate();
|
||||
statsRange.value = [`${y}-${m}-01`, `${y}-${m}-${last}`];
|
||||
}
|
||||
loadStats();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchList();
|
||||
loadStats();
|
||||
@@ -269,6 +301,23 @@ function partyName(row, role) {
|
||||
return hit?.ref_name || "-";
|
||||
}
|
||||
|
||||
/** 利润正负:正数加 + 号,负数保持 - 号 */
|
||||
function profitText(val) {
|
||||
const n = Number(val) || 0;
|
||||
const s = formatMoney(Math.abs(n));
|
||||
if (n > 0) return "+" + s;
|
||||
if (n < 0) return "-" + s;
|
||||
return s;
|
||||
}
|
||||
|
||||
/** 利润正负配色:正利润红色(danger),负利润绿色(success) */
|
||||
function profitStyle(val) {
|
||||
const n = Number(val) || 0;
|
||||
if (n > 0) return { color: "var(--el-color-danger)" };
|
||||
if (n < 0) return { color: "var(--el-color-success)" };
|
||||
return {};
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
currentRow.value = null;
|
||||
initStep.value = 1;
|
||||
@@ -287,25 +336,15 @@ function openDetail(row) {
|
||||
detailVisible.value = true;
|
||||
}
|
||||
|
||||
/** 点击名称:草稿继续填写,已完成看详情 */
|
||||
/** 点击名称:统一查看详情(编辑 / 删除请走合同详情) */
|
||||
function openRow(row) {
|
||||
if (Number(row.status) === 1) openEdit(row);
|
||||
else openDetail(row);
|
||||
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 || "状态更新失败");
|
||||
}
|
||||
/** 详情页触发编辑:关闭详情抽屉,打开创建/编辑向导 */
|
||||
function onDetailEdit(row) {
|
||||
detailVisible.value = false;
|
||||
openEdit(row || currentRow.value);
|
||||
}
|
||||
|
||||
async function handleDelete(row) {
|
||||
@@ -332,6 +371,15 @@ async function handleDelete(row) {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.stat-period-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--el-bg-color);
|
||||
@@ -360,10 +408,10 @@ async function handleDelete(row) {
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 操作列:强制水平居中(多按钮换行也整体居中) */
|
||||
|
||||
:deep(.el-table .op-col .cell) {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
Reference in New Issue
Block a user