批量更新功能

This commit is contained in:
2026-09-14 16:15:44 +08:00
parent 6f106e43af
commit a26c98b92b
79 changed files with 4529 additions and 724 deletions
+92 -22
View File
@@ -14,6 +14,7 @@
<!-- 统计周期 -->
<div class="stat-period-bar">
<el-radio-group v-model="statsPeriod" @change="onPeriodChange">
<el-radio-button label="all">全部</el-radio-button>
<el-radio-button label="month">本月</el-radio-button>
<el-radio-button label="quarter">本季</el-radio-button>
<el-radio-button label="year">本年</el-radio-button>
@@ -30,10 +31,12 @@
:clearable="false"
@change="onRangeChange"
/>
<!-- 当前统计范围:切换周期后卡片按该区间重新统计(区间相同则数字自然相同) -->
<span class="stat-scope">统计范围:{{ statsScopeText }}</span>
</div>
<!-- 统计卡片 -->
<div class="stat-cards">
<div class="stat-cards" v-loading="statsLoading">
<div class="stat-card">
<span class="stat-label">合同总数</span>
<span class="stat-value">{{ stats.total ?? 0 }}</span>
@@ -47,7 +50,12 @@
<span class="stat-value">{{ formatMoney(stats.total_cost) }}</span>
</div>
<div class="stat-card">
<span class="stat-label">合同总利润</span>
<el-tooltip
content="仅统计非甲方(收款方)合同的利润;我方为甲方的合同属于支出,不计入利润"
placement="top"
>
<span class="stat-label">合同总利润</span>
</el-tooltip>
<span class="stat-value" :style="profitStyle(stats.total_profit)">{{ profitText(stats.total_profit) }}</span>
</div>
</div>
@@ -65,8 +73,8 @@
/>
</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 v-model="filters.our_role" clearable placeholder="全部" style="width: 130px">
<el-option v-for="i in ourRoleFilterOptions" :key="i.value" :label="i.label" :value="i.value" />
</el-select>
</el-form-item>
<el-form-item label="合同分类">
@@ -101,8 +109,13 @@
</el-tag>
</template>
</el-table-column>
<el-table-column prop="contract_no" label="合同编号" width="150" show-overflow-tooltip />
<el-table-column label="合同名称" min-width="190" show-overflow-tooltip fixed>
<el-table-column label="我方角色" width="90" align="center" fixed>
<template #default="{ row }">
<el-tag :type="ourRoleTag(row.our_role)" size="small" effect="plain">{{ ourRoleText(row.our_role) }}</el-tag>
</template>
</el-table-column>
<!-- <el-table-column prop="contract_no" label="合同编号" width="150" show-overflow-tooltip /> -->
<el-table-column label="合同名称" min-width="190" show-overflow-tooltip >
<template #default="{ row }">
<span class="name-link" @click="openRow(row)">
{{ row.contract_name }}
@@ -110,11 +123,6 @@
</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>
@@ -128,20 +136,28 @@
{{ formatMoney(row.summary?.total_amount ?? row.total_amount) }}
</template>
</el-table-column>
<el-table-column label="合同总利润" width="130" align="right">
<el-table-column label="合同利润/支出" width="140" align="right">
<template #header>
<el-tooltip
content="我方为甲方(付款方):按负数(绿色)显示合同支出 = 合同总金额;其余角色:显示合同利润 = 总金额 − 产品总成本"
placement="top"
>
<span>合同利润/支出</span>
</el-tooltip>
</template>
<template #default="{ row }">
<span :style="profitStyle(row.summary?.total_profit ?? row.total_profit)">
{{ profitText(row.summary?.total_profit ?? row.total_profit) }}
<span :style="profitStyle(contractProfitOrExpense(row))">
{{ profitText(contractProfitOrExpense(row)) }}
</span>
</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="100" align="center">
<el-table-column label="逾期" width="130" align="center">
<template #default="{ row }">
<el-tag v-if="contractOverdue(row).overdue" type="danger" size="small" effect="dark">
{{ overdueText(contractOverdue(row).days) }}
{{ contractOverdue(row).text }}
</el-tag>
<span v-else>-</span>
</template>
@@ -184,7 +200,7 @@
</template>
<script setup>
import { ref, reactive, onMounted, nextTick, watch } from "vue";
import { ref, reactive, computed, onMounted, nextTick, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { ElMessage, ElMessageBox } from "element-plus";
import { Plus, Search, Refresh } from "@element-plus/icons-vue";
@@ -197,16 +213,17 @@ import ContractCreate from "./components/create.vue";
import ContractDetail from "./components/detail.vue";
import {
CONTRACT_CATEGORY_OPTIONS,
OUR_ROLE_OPTIONS,
ourRoleFilterOptions,
CONTRACT_STATUS_OPTIONS,
ourRoleText,
ourRoleTag,
contractCategoryText,
contractStatusText,
contractStatusTag,
contractOverdue,
overdueText,
formatDateOnly,
formatMoney,
isPayerContract,
} from "../dict";
const route = useRoute();
@@ -228,15 +245,46 @@ const filters = reactive({
});
const pagination = reactive({ page: 1, pageSize: 20, total: 0 });
/** 统计周期:month/quarter/year/custom(自定义选区间),默认本年 */
const statsPeriod = ref("year");
/** 统计周期:all(全部)/ month / quarter / year / custom(自定义选区间),默认全部 */
const statsPeriod = ref("all");
const statsRange = ref([]);
/** 列表页统计:后端按周期汇总(排除已作废),周期参数透传给 Stats */
const stats = ref({ total: 0, total_amount: 0, total_cost: 0, total_profit: 0 });
/** 卡片加载态:切换周期时给出可见的刷新反馈 */
const statsLoading = ref(false);
/** 周期参数:统计与列表共用,保证两者口径一致 */
/** 当前统计范围文案:与后端口径一致(合同有效期与所选周期重叠),便于对照卡片数字 */
const statsScopeText = computed(() => {
const pad = (n) => String(n).padStart(2, "0");
const fmt = (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
const now = new Date();
const y = now.getFullYear();
const m = now.getMonth();
switch (statsPeriod.value) {
case "month":
return `${fmt(new Date(y, m, 1))} ~ ${fmt(new Date(y, m + 1, 0))}`;
case "quarter": {
const q = Math.floor(m / 3);
return `${fmt(new Date(y, q * 3, 1))} ~ ${fmt(new Date(y, q * 3 + 3, 0))}`;
}
case "year":
return `${y}-01-01 ~ ${y}-12-31`;
case "custom":
return statsRange.value?.length === 2
? `${statsRange.value[0]} ~ ${statsRange.value[1]}`
: "自定义区间";
default:
return "全部时间(不限有效期)";
}
});
/**
* 周期参数:统计与列表共用,保证两者口径一致。
* 「全部」不传周期参数——列表不按有效期过滤,避免有效期不在所选周期内的合同(如历史合同)被隐藏。
*/
function periodParams() {
if (statsPeriod.value === "all") return {};
const params = { period: statsPeriod.value };
if (statsPeriod.value === "custom" && statsRange.value && statsRange.value.length === 2) {
params.start_date = statsRange.value[0];
@@ -246,11 +294,14 @@ function periodParams() {
}
async function loadStats() {
statsLoading.value = true;
try {
const res = await getContractStats(periodParams());
stats.value = res?.data || stats.value;
} catch {
/* 统计失败不影响列表 */
} finally {
statsLoading.value = false;
}
}
@@ -373,6 +424,18 @@ function profitStyle(val) {
return {};
}
/**
* 列表展示值:我方为甲方(付款方)的合同是「支出」,取负值 → 显示为 -¥X(绿色);
* 其余角色的合同是「利润」(总金额 − 产品总成本)。表头已注明口径,单元格内不写"支出"文字。
*/
function contractProfitOrExpense(row) {
if (!isPayerContract(row.our_role)) {
return Number(row.summary?.total_profit ?? row.total_profit) || 0;
}
const paid = Number(row.summary?.total_amount ?? row.total_amount) || 0;
return -paid;
}
function openCreate() {
currentRow.value = null;
initStep.value = 1;
@@ -434,6 +497,13 @@ async function handleDelete(row) {
gap: 12px;
margin-bottom: 12px;
flex-wrap: wrap;
/* 当前统计范围:右对齐说明,切换周期后随卡片一起刷新 */
.stat-scope {
margin-left: auto;
font-size: 12px;
color: var(--el-text-color-secondary);
}
}
.stat-card {