From 154f3f8e6e8a98e0472d9cc02e7a11dc6bc2b0eb Mon Sep 17 00:00:00 2001
From: yyb <995253665@qq.com>
Date: 星期三, 01 四月 2026 14:58:01 +0800
Subject: [PATCH] 标准/实际成本对比分析联调
---
src/views/costAccounting/stdVsActCostAnalysis/index.vue | 541 ++++++++++++++++++++++++++---------------------------
src/api/costAccounting/productionSettlementBatches.js | 53 +++++
2 files changed, 316 insertions(+), 278 deletions(-)
diff --git a/src/api/costAccounting/productionSettlementBatches.js b/src/api/costAccounting/productionSettlementBatches.js
new file mode 100644
index 0000000..95a5fba
--- /dev/null
+++ b/src/api/costAccounting/productionSettlementBatches.js
@@ -0,0 +1,53 @@
+import request from "@/utils/request";
+
+// 鑾峰彇浜у搧绫诲埆锛堟寜鏈堜唤锛�
+export function getProductTypes(params) {
+ return request({
+ url: "/productionSettlementBatches/getProductTypes",
+ method: "get",
+ params,
+ });
+}
+
+// 鑾峰彇绉戠洰绫诲埆锛堟寜鏈堜唤锛�
+export function getSubjectNames(params) {
+ return request({
+ url: "/productionSettlementBatches/getSubjectNames",
+ method: "get",
+ params,
+ });
+}
+
+// 鏍囧噯鎴愭湰瀵煎叆锛坋l-upload 闇�瑕佸畬鏁� URL锛�
+export function getImportActionUrl() {
+ return `${import.meta.env.VITE_APP_BASE_API}/productionSettlementBatches/import`;
+}
+
+// 涓嬭浇瀵煎叆妯℃澘锛圙ET锛岃繑鍥� blob锛�
+export function downloadTemplate(params) {
+ return request({
+ url: "/productionSettlementBatches/downloadTemplate",
+ method: "get",
+ params,
+ responseType: "blob",
+ });
+}
+
+// 鏍哥畻鏌ヨ锛堟寜鏈堜唤/浜у搧绫诲瀷/绉戠洰/鎴愭湰绫诲瀷锛�
+export function getSettlement(params) {
+ return request({
+ url: "/productionSettlementBatches/getSettlement",
+ method: "get",
+ params,
+ });
+}
+
+// 姹囨�绘垚鏈紙绗簩妯″潡 KPI锛�
+export function getTotalCosts(params) {
+ return request({
+ url: "/productionSettlementBatches/getTotalCosts",
+ method: "get",
+ params,
+ });
+}
+
diff --git a/src/views/costAccounting/stdVsActCostAnalysis/index.vue b/src/views/costAccounting/stdVsActCostAnalysis/index.vue
index 51ff06c..55830cd 100644
--- a/src/views/costAccounting/stdVsActCostAnalysis/index.vue
+++ b/src/views/costAccounting/stdVsActCostAnalysis/index.vue
@@ -29,32 +29,30 @@
<div class="filter-layout">
<el-form :model="searchForm" :inline="true" class="filter-form">
- <el-form-item label="鏈堜唤鑼冨洿">
+ <el-form-item label="鏈堜唤">
<el-date-picker
- v-model="searchForm.monthRange"
- type="monthrange"
- range-separator="鑷�"
- start-placeholder="寮�濮嬫湀浠�"
- end-placeholder="缁撴潫鏈堜唤"
+ v-model="searchForm.month"
+ type="month"
value-format="YYYY-MM"
+ placeholder="閫夋嫨鏈堜唤"
class="w-260"
- @change="handleQuery"
+ @change="handleMonthChange"
/>
</el-form-item>
- <el-form-item label="浜у搧绫诲埆">
+ <el-form-item label="浜у搧绫诲瀷">
<el-select
- v-model="searchForm.category"
+ v-model="searchForm.productType"
clearable
filterable
- placeholder="鍏ㄩ儴绫诲埆"
+ placeholder="鍏ㄩ儴绫诲瀷"
class="w-180"
@change="handleQuery"
>
<el-option
v-for="item in categoryOptions"
- :key="item"
- :label="item"
- :value="item"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
/>
</el-select>
</el-form-item>
@@ -62,12 +60,16 @@
<el-select
v-model="searchForm.costType"
clearable
- placeholder="鍏ㄩ儴绫诲瀷"
+ placeholder="鍏ㄩ儴鎴愭湰绫诲瀷"
class="w-180"
@change="handleQuery"
>
- <el-option label="鑳借�楁垚鏈�" value="鑳借�楁垚鏈�" />
- <el-option label="鐢熶骇鎴愭湰" value="鐢熶骇鎴愭湰" />
+ <el-option
+ v-for="item in costTypeOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
</el-select>
</el-form-item>
</el-form>
@@ -78,30 +80,31 @@
<el-button class="lux-btn" @click="handleReset">閲嶇疆</el-button>
</div>
<div class="action-group">
- <el-dropdown trigger="click" @command="handleImportCommand">
- <el-button class="lux-btn" type="success" plain>
- 鏍囧噯鎴愭湰瀵煎叆
- <el-icon class="el-icon--right"><ArrowDown /></el-icon>
- </el-button>
- <template #dropdown>
- <el-dropdown-menu>
- <el-dropdown-item command="template">涓嬭浇瀵煎叆妯℃澘</el-dropdown-item>
- <el-dropdown-item command="upload">Excel 瀵煎叆</el-dropdown-item>
- </el-dropdown-menu>
- </template>
- </el-dropdown>
- <el-upload
- ref="uploadRef"
- class="hidden-upload"
- :auto-upload="false"
- :show-file-list="false"
- accept=".xlsx,.xls"
- :on-change="handleFileChange"
- />
+ <el-button class="lux-btn" type="success" plain @click="openImportDialog">
+ 鏍囧噯鎴愭湰瀵煎叆
+ </el-button>
</div>
</div>
</div>
</el-card>
+
+ <ImportDialog
+ ref="importDialogRef"
+ v-model="importDialogVisible"
+ title="鏍囧噯鎴愭湰瀵煎叆"
+ width="520px"
+ :headers="importHeaders"
+ :action="importAction"
+ :auto-upload="false"
+ :limit="1"
+ tip-text="浠呭厑璁稿鍏� xls銆亁lsx 鏍煎紡鏂囦欢銆�"
+ :show-download-template="true"
+ :on-success="handleImportSuccess"
+ @confirm="handleImportConfirm"
+ @download-template="downloadTemplate"
+ @close="handleImportDialogClose"
+ @cancel="handleImportDialogClose"
+ />
<el-card class="panel-card glass-card kpi-card" shadow="never">
<div class="kpi-strip">
@@ -194,29 +197,23 @@
</div>
</template>
<el-table :data="pagedTableData" stripe class="lux-table" @sort-change="handleSortChange">
- <el-table-column prop="month" label="鏈堜唤" width="110" />
- <el-table-column prop="category" label="浜у搧绫诲埆" min-width="140" />
+ <el-table-column prop="periodTime" label="鏈堜唤" width="110" />
+ <el-table-column prop="productType" label="浜у搧绫诲瀷" min-width="140" />
<el-table-column prop="costType" label="鎴愭湰绫诲瀷" min-width="120" />
- <el-table-column prop="standardCost" label="鏍囧噯鎴愭湰(鍏�)" sortable="custom" align="right">
- <template #default="scope">楼{{ formatMoney(scope.row.standardCost) }}</template>
+ <el-table-column prop="subjectName" label="绉戠洰" min-width="140" show-overflow-tooltip />
+ <el-table-column prop="budgetQty" label="棰勭畻鑰楅噺" sortable="custom" align="right" min-width="120" />
+ <el-table-column prop="budgetPrice" label="棰勭畻鍗曚环" sortable="custom" align="right" min-width="120" />
+ <el-table-column prop="budgetTotal" label="棰勭畻鎬绘垚鏈�" sortable="custom" align="right" min-width="130">
+ <template #default="scope">楼{{ formatMoney(scope.row.budgetTotal) }}</template>
</el-table-column>
- <el-table-column prop="actualCost" label="瀹為檯鎴愭湰(鍏�)" sortable="custom" align="right">
- <template #default="scope">楼{{ formatMoney(scope.row.actualCost) }}</template>
+ <el-table-column prop="actualQty" label="瀹為檯鑰楅噺" sortable="custom" align="right" min-width="120" />
+ <el-table-column prop="actualPrice" label="瀹為檯鍗曚环" sortable="custom" align="right" min-width="120" />
+ <el-table-column prop="actualTotal" label="瀹為檯鎬绘垚鏈�" sortable="custom" align="right" min-width="130">
+ <template #default="scope">楼{{ formatMoney(scope.row.actualTotal) }}</template>
</el-table-column>
- <el-table-column prop="diff" label="宸紓(鍏�)" sortable="custom" align="right">
- <template #default="scope">
- <span :class="scope.row.diff >= 0 ? 'cost-value' : 'ok-value'">
- {{ formatSignedMoney(scope.row.diff) }}
- </span>
- </template>
- </el-table-column>
- <el-table-column prop="diffRate" label="宸紓鐜�" sortable="custom" align="right">
- <template #default="scope">
- <span :class="scope.row.diffRate >= 0 ? 'cost-value' : 'ok-value'">
- {{ formatPercent(scope.row.diffRate) }}
- </span>
- </template>
- </el-table-column>
+ <el-table-column prop="diffQty" label="鑰楅噺宸紓" min-width="110" align="right" />
+ <el-table-column prop="diffPrice" label="鍗曚环宸紓" min-width="110" align="right" />
+ <el-table-column prop="diffTotal" label="鎬绘垚鏈樊寮�" min-width="110" align="right" />
</el-table>
<div class="pagination-container">
<el-pagination
@@ -247,23 +244,40 @@
ZoomIn,
} from "@element-plus/icons-vue";
import { ElMessage } from "element-plus";
+import ImportDialog from "@/components/Dialog/ImportDialog.vue";
+import { getToken } from "@/utils/auth.js";
import * as echarts from "echarts";
-// import * as XLSX from "xlsx";
+import {
+ downloadTemplate as downloadProductionSettlementTemplate,
+ getImportActionUrl,
+ getSettlement,
+ getTotalCosts,
+ getProductTypes,
+} from "@/api/costAccounting/productionSettlementBatches";
-const getDefaultMonthRange = () => {
+const getDefaultMonth = () => {
const end = new Date();
- const start = new Date();
- start.setMonth(start.getMonth() - 2);
- return [start.toISOString().slice(0, 7), end.toISOString().slice(0, 7)];
+ return end.toISOString().slice(0, 7);
};
const searchForm = reactive({
- monthRange: getDefaultMonthRange(),
- category: "",
+ month: getDefaultMonth(),
+ productType: "",
costType: "",
});
-const uploadRef = ref();
+const categoryOptions = ref([]);
+const costTypeOptions = ref([]);
+
+const importDialogVisible = ref(false);
+const importDialogRef = ref(null);
+
+const importHeaders = computed(() => ({
+ Authorization: `Bearer ${getToken()}`,
+}));
+
+const importAction = computed(() => getImportActionUrl());
+
const chartRef = ref(null);
const largeChartRef = ref(null);
let chartInstance = null;
@@ -271,150 +285,22 @@
const largeChartVisible = ref(false);
const currentChartOption = ref(null);
-// ------------------------------
-// 鍋囨暟鎹細鐢ㄤ簬鍏堣仈璋冮〉闈㈡覆鏌�
-// ------------------------------
-const actualCostSource = ref([]);
-const standardCostSource = ref([]);
-
-const fakeMonths = ["2026-01", "2026-02", "2026-03"];
-const fakeCategories = [
- "绮夌叅鐏�",
- "鐭崇伆",
- "姘存偿",
- "閾濈矇鑶�",
- "鑴辨ā鍓�",
- "鐭宠啅",
- "鎵撳寘甯�",
- "闃茶厫鍓傦紙鏉挎潗鐢級",
- "姘у寲闀侊紙鏉挎潗鐢級",
- "鍐锋尋涓濓紙鏉挎潗鐢級",
- "鍗℃墸锛堟澘鏉愮敤锛�",
- "鏉愭枡灏忚",
- "姘�",
- "鐢�",
- "钂告苯",
-];
-
-const fakeCostType = (category) => (["姘�", "鐢�", "钂告苯"].includes(category) ? "鑳借�楁垚鏈�" : "鐢熶骇鎴愭湰");
-
-// 姣忎釜绫诲埆鐨勬爣鍑嗘垚鏈熀鍑嗗�硷紙浠呯敤浜庡亣鏁版嵁锛�
-const baseStandardCostByCategory = {
- 绮夌叅鐏�: 98000,
- 鐭崇伆: 52000,
- 姘存偿: 175000,
- 閾濈矇鑶�: 32000,
- 鑴辨ā鍓�: 21000,
- 鐭宠啅: 41000,
- 鎵撳寘甯�: 14500,
- "闃茶厫鍓傦紙鏉挎潗鐢級": 12500,
- "姘у寲闀侊紙鏉挎潗鐢級": 22000,
- "鍐锋尋涓濓紙鏉挎潗鐢級": 9800,
- "鍗℃墸锛堟澘鏉愮敤锛�": 8600,
- 鏉愭枡灏忚: 420000,
- 姘�: 6800,
- 鐢�: 26000,
- 钂告苯: 52000,
-};
-
-// 鏈堜唤娉㈠姩绯绘暟锛堣鍥捐〃鐪嬭捣鏉ユ洿鈥滅湡瀹炩�濅竴浜涳級
-const monthFactorByMonth = {
- "2026-01": 1.0,
- "2026-02": 1.06,
- "2026-03": 0.97,
-};
-
-// 瀹為檯鎴愭湰鐩稿鏍囧噯鎴愭湰鐨勫亸绉绘瘮渚嬶紙鐢ㄤ簬娴嬭瘯姝h礋宸紓灞曠ず锛�
-const diffRatioByCategory = {
- 绮夌叅鐏�: 0.05,
- 鐭崇伆: -0.01,
- 姘存偿: 0.03,
- 閾濈矇鑶�: 0.0,
- 鑴辨ā鍓�: -0.04,
- 鐭宠啅: 0.02,
- 鎵撳寘甯�: -0.03,
- "闃茶厫鍓傦紙鏉挎潗鐢級": 0.06,
- "姘у寲闀侊紙鏉挎潗鐢級": -0.02,
- "鍐锋尋涓濓紙鏉挎潗鐢級": 0.01,
- "鍗℃墸锛堟澘鏉愮敤锛�": -0.05,
- 鏉愭枡灏忚: 0.02,
- 姘�: -0.01,
- 鐢�: 0.04,
- 钂告苯: -0.03,
-};
-
-const buildFakeSources = () => {
- const stdRows = [];
- const actRows = [];
-
- for (const month of fakeMonths) {
- const monthFactor = monthFactorByMonth[month] ?? 1;
- const monthAdj = month === "2026-02" ? 0.005 : month === "2026-03" ? -0.006 : 0;
-
- for (const category of fakeCategories) {
- const costType = fakeCostType(category);
- const base = baseStandardCostByCategory[category] ?? 0;
- const standardCost = Math.round(base * monthFactor);
- const diffRatio = (diffRatioByCategory[category] ?? 0) + monthAdj;
- const actualCost = Math.round(standardCost * (1 + diffRatio));
-
- stdRows.push({ month, category, costType, standardCost });
- actRows.push({ month, category, costType, actualCost });
- }
- }
-
- standardCostSource.value = stdRows;
- actualCostSource.value = actRows;
-};
-
-buildFakeSources();
-
-const categoryOptions = computed(() => {
- const all = [...actualCostSource.value, ...standardCostSource.value];
- return Array.from(new Set(all.map((item) => item.category)));
+const settlementRows = ref([]);
+const totalCosts = reactive({
+ budgetTotal: 0,
+ actualTotal: 0,
+ diffTotal: 0,
+ diffRate: "0%",
});
-const inRange = (value, range) => {
- if (!Array.isArray(range) || range.length !== 2 || !range[0] || !range[1]) return true;
- return value >= range[0] && value <= range[1];
-};
-
-const mergedRows = computed(() => {
- const key = (item) => `${item.month}__${item.category}__${item.costType}`;
- const stdMap = new Map(standardCostSource.value.map((item) => [key(item), item]));
- const actMap = new Map(actualCostSource.value.map((item) => [key(item), item]));
- const keySet = new Set([...stdMap.keys(), ...actMap.keys()]);
- const rows = [];
-
- for (const k of keySet) {
- const std = stdMap.get(k);
- const act = actMap.get(k);
- const month = std?.month || act?.month || "";
- const category = std?.category || act?.category || "";
- const costType = std?.costType || act?.costType || "";
- const standardCost = Number(std?.standardCost || 0);
- const actualCost = Number(act?.actualCost || 0);
- const diff = actualCost - standardCost;
- const diffRate = standardCost === 0 ? 0 : (diff / standardCost) * 100;
-
- rows.push({ month, category, costType, standardCost, actualCost, diff, diffRate });
- }
-
- return rows.sort((a, b) => {
- if (a.month !== b.month) return a.month > b.month ? 1 : -1;
- if (a.category !== b.category) return a.category.localeCompare(b.category, "zh-Hans-CN");
- return a.costType.localeCompare(b.costType, "zh-Hans-CN");
+const tableData = computed(() => {
+ return (Array.isArray(settlementRows.value) ? settlementRows.value : []).filter((item) => {
+ const hitMonth = !searchForm.month || item.periodTime === searchForm.month;
+ const hitProductType = !searchForm.productType || item.productType === searchForm.productType;
+ const hitCostType = !searchForm.costType || item.costType === searchForm.costType;
+ return hitMonth && hitProductType && hitCostType;
});
});
-
-const tableData = computed(() =>
- mergedRows.value.filter((item) => {
- const hitMonth = inRange(item.month, searchForm.monthRange);
- const hitCategory = !searchForm.category || item.category === searchForm.category;
- const hitCostType = !searchForm.costType || item.costType === searchForm.costType;
- return hitMonth && hitCategory && hitCostType;
- })
-);
const page = reactive({
current: 1,
@@ -454,26 +340,45 @@
return sortedTableData.value.slice(start, start + page.size);
});
-const overview = computed(() => {
- const standardCost = tableData.value.reduce((sum, item) => sum + item.standardCost, 0);
- const actualCost = tableData.value.reduce((sum, item) => sum + item.actualCost, 0);
- const diff = actualCost - standardCost;
- const diffRate = standardCost === 0 ? 0 : (diff / standardCost) * 100;
- return { standardCost, actualCost, diff, diffRate };
-});
+const overview = computed(() => ({
+ standardCost: Number(totalCosts.budgetTotal || 0),
+ actualCost: Number(totalCosts.actualTotal || 0),
+ diff: Number(totalCosts.diffTotal || 0),
+ diffRate: totalCosts.diffRate ?? "0%",
+}));
const getChartData = () => {
- const xAxis = tableData.value.map(
- (item) => `${item.month}\n${item.category}-${item.costType.replace("鎴愭湰", "")}`
+ // 鍥捐〃鍙e緞锛氭寜鈥滅鐩�濇眹鎬诲睍绀哄叏閮ㄧ鐩�
+ const agg = new Map();
+ for (const row of tableData.value) {
+ const subjectName = String(row?.subjectName || "").trim() || "-";
+ const budgetTotal = Number(row?.budgetTotal || 0);
+ const actualTotal = Number(row?.actualTotal || 0);
+ const bucket = agg.get(subjectName) || { subjectName, budgetTotal: 0, actualTotal: 0 };
+ bucket.budgetTotal += Number.isFinite(budgetTotal) ? budgetTotal : 0;
+ bucket.actualTotal += Number.isFinite(actualTotal) ? actualTotal : 0;
+ agg.set(subjectName, bucket);
+ }
+
+ const rows = Array.from(agg.values()).sort((a, b) =>
+ String(a.subjectName).localeCompare(String(b.subjectName), "zh-Hans-CN")
);
- const standard = tableData.value.map((item) => item.standardCost);
- const actual = tableData.value.map((item) => item.actualCost);
- const diffRate = tableData.value.map((item) => Number(item.diffRate.toFixed(2)));
- return { xAxis, standard, actual, diffRate };
+
+ const xAxis = rows.map((item) => item.subjectName);
+ const standard = rows.map((item) => item.budgetTotal);
+ const actual = rows.map((item) => item.actualTotal);
+ const diffRate = rows.map((item) => {
+ const base = Number(item.budgetTotal || 0);
+ const diff = Number(item.actualTotal || 0) - base;
+ if (!base) return 0;
+ return (diff / base) * 100;
+ });
+
+ return { xAxis, standard, actual, diffRate, rows };
};
const buildChartOption = () => {
- const { xAxis, standard, actual, diffRate } = getChartData();
+ const { xAxis, standard, actual, diffRate, rows } = getChartData();
return {
animation: true,
animationDuration: 920,
@@ -493,13 +398,17 @@
textStyle: { color: "rgba(15, 23, 42, 0.88)" },
extraCssText: "box-shadow: 0 12px 40px rgba(15, 23, 42, 0.12); border-radius: 12px;",
formatter: (params) => {
- const row = tableData.value[params[0]?.dataIndex] || {};
+ const row = rows?.[params[0]?.dataIndex] || {};
+ const budgetTotal = Number(row?.budgetTotal || 0);
+ const actualTotal = Number(row?.actualTotal || 0);
+ const diff = actualTotal - budgetTotal;
+ const rate = budgetTotal ? (diff / budgetTotal) * 100 : 0;
return [
- `${row.month || ""} ${row.category || ""} ${row.costType || ""}`,
- `鏍囧噯鎴愭湰锛毬�${formatMoney(row.standardCost || 0)}`,
- `瀹為檯鎴愭湰锛毬�${formatMoney(row.actualCost || 0)}`,
- `宸紓锛�${formatSignedMoney(row.diff || 0)}`,
- `宸紓鐜囷細${formatPercent(row.diffRate || 0)}`,
+ `绉戠洰锛�${row.subjectName || "-"}`,
+ `棰勭畻鎬绘垚鏈細楼${formatMoney(budgetTotal)}`,
+ `瀹為檯鎬绘垚鏈細楼${formatMoney(actualTotal)}`,
+ `宸紓锛�${formatSignedMoney(diff)}`,
+ `宸紓鐜囷細${formatPercent(rate)}`,
].join("<br/>");
},
},
@@ -636,72 +545,144 @@
standardCostSource.value = Array.from(map.values());
};
-const handleFileChange = async (uploadFile) => {
- try {
- const file = uploadFile.raw;
- if (!file) return;
- const data = await file.arrayBuffer();
- const workbook = XLSX.read(data, { type: "array" });
- const sheetName = workbook.SheetNames[0];
- const sheet = workbook.Sheets[sheetName];
- const rows = XLSX.utils.sheet_to_json(sheet, { defval: "" });
- const parsed = parseImportedRows(rows);
- if (!parsed.length) {
- ElMessage.warning("瀵煎叆澶辫触锛氭ā鏉垮唴瀹逛负绌烘垨瀛楁涓嶅尮閰�");
- return;
- }
- replaceStandardSourceByImport(parsed);
- ElMessage.success(`瀵煎叆鎴愬姛锛�${parsed.length} 鏉℃爣鍑嗘垚鏈褰昤);
- handleQuery();
- } catch (error) {
- console.error(error);
- ElMessage.error("瀵煎叆澶辫触锛岃妫�鏌� Excel 鏍煎紡");
- } finally {
- uploadRef.value?.clearFiles?.();
- }
+const openImportDialog = () => {
+ importDialogVisible.value = true;
};
-const openUploadSelector = () => {
- const input = uploadRef.value?.$el?.querySelector?.("input[type='file']");
- if (!input) {
- ElMessage.warning("涓婁紶缁勪欢灏氭湭灏辩华锛岃绋嶅悗閲嶈瘯");
- return;
- }
- input.click();
+const handleImportConfirm = () => {
+ importDialogRef.value?.submit?.();
};
-const handleImportCommand = (command) => {
- if (command === "template") {
- downloadTemplate();
- return;
- }
- if (command === "upload") {
- openUploadSelector();
- }
+const handleImportDialogClose = () => {
+ importDialogRef.value?.clearFiles?.();
};
const downloadTemplate = () => {
- const sample = [
- { 鏈堜唤: "2026-03", 浜у搧绫诲埆: "绮夌叅鐏�", 鎴愭湰绫诲瀷: "鏍囧噯鐢熶骇鎴愭湰", 鏍囧噯鎴愭湰: 98000 },
- { 鏈堜唤: "2026-03", 浜у搧绫诲埆: "姘存偿", 鎴愭湰绫诲瀷: "鏍囧噯鐢熶骇鎴愭湰", 鏍囧噯鎴愭湰: 175000 },
- { 鏈堜唤: "2026-03", 浜у搧绫诲埆: "鐢�", 鎴愭湰绫诲瀷: "鏍囧噯鑳借�楁垚鏈�", 鏍囧噯鎴愭湰: 26000 },
- { 鏈堜唤: "2026-03", 浜у搧绫诲埆: "钂告苯", 鎴愭湰绫诲瀷: "鏍囧噯鑳借�楁垚鏈�", 鏍囧噯鎴愭湰: 52000 },
- { 鏈堜唤: "2026-03", 浜у搧绫诲埆: "姘�", 鎴愭湰绫诲瀷: "鏍囧噯鑳借�楁垚鏈�", 鏍囧噯鎴愭湰: 6800 },
- ];
- const ws = XLSX.utils.json_to_sheet(sample);
- const wb = XLSX.utils.book_new();
- XLSX.utils.book_append_sheet(wb, ws, "鏍囧噯鎴愭湰妯℃澘");
- XLSX.writeFile(wb, "鏍囧噯鎴愭湰鎸夋湀瀵煎叆妯℃澘.xlsx");
- ElMessage.success("妯℃澘宸蹭笅杞�");
+ downloadProductionSettlementTemplate({ periodTime: searchForm.month || undefined })
+ .then((data) => {
+ const blob =
+ data instanceof Blob
+ ? data
+ : new Blob([data], {
+ type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ });
+ const url = window.URL.createObjectURL(blob);
+ const link = document.createElement("a");
+ link.href = url;
+ link.download = "鏍囧噯鎴愭湰瀵煎叆妯℃澘.xlsx";
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ window.URL.revokeObjectURL(url);
+ ElMessage.success("妯℃澘涓嬭浇鎴愬姛");
+ })
+ .catch(() => {
+ ElMessage.error("妯℃澘涓嬭浇澶辫触");
+ });
};
-const handleQuery = () => {
- updateChart();
+const handleImportSuccess = (response) => {
+ const code = response?.code;
+ const msg = response?.msg || response?.message;
+ if (code === 200) {
+ ElMessage.success(msg || "瀵煎叆鎴愬姛");
+ importDialogVisible.value = false;
+ importDialogRef.value?.clearFiles?.();
+ handleQuery();
+ return;
+ }
+ ElMessage.error(msg || "瀵煎叆澶辫触");
+};
+
+const normalizeSettlementResponse = (data) => {
+ const map = data && typeof data === "object" ? data : {};
+ const rows = [];
+ for (const [costType, list] of Object.entries(map)) {
+ if (!Array.isArray(list)) continue;
+ for (const item of list) {
+ rows.push({
+ ...item,
+ periodTime: searchForm.month,
+ costType: costType,
+ });
+ }
+ }
+ return rows;
+};
+
+const fetchCategoryOptions = async () => {
+ if (!searchForm.month) {
+ categoryOptions.value = [];
+ return;
+ }
+ try {
+ const { data } = await getProductTypes({ periodTime: searchForm.month });
+ const list = Array.isArray(data) ? data : data?.records || [];
+ categoryOptions.value = list.map((item) => ({
+ label: item.label || item.name || item.typeName || item,
+ value: item.value || item.code || item.typeCode || item,
+ }));
+ } catch (e) {
+ categoryOptions.value = [];
+ }
+};
+
+const fetchCostTypeOptions = async () => {
+ if (!searchForm.month) {
+ costTypeOptions.value = [];
+ return;
+ }
+ try {
+ // 涓嶅甫 costType锛屾嬁鍒板畬鏁村垎缁� key 鐢ㄤ簬涓嬫媺閫夐」
+ const res = await getSettlement({ periodTime: searchForm.month });
+ const map = res?.data || {};
+ const keys = Object.keys(map || {});
+ costTypeOptions.value = keys.map((k) => ({ label: k, value: k }));
+ } catch (e) {
+ costTypeOptions.value = [];
+ }
+};
+
+const handleMonthChange = () => {
+ searchForm.productType = "";
+ searchForm.costType = "";
+ Promise.all([fetchCategoryOptions(), fetchCostTypeOptions()]).then(() => {
+ handleQuery();
+ });
+};
+
+const handleQuery = async () => {
+ try {
+ const params = {
+ periodTime: searchForm.month || undefined,
+ productType: searchForm.productType || undefined,
+ costType: searchForm.costType || undefined,
+ };
+ const [settlementRes, totalRes] = await Promise.all([getSettlement(params), getTotalCosts(params)]);
+ const map = settlementRes?.data || {};
+ const rows = normalizeSettlementResponse(map);
+ settlementRows.value = rows;
+
+ const totals = totalRes?.data || {};
+ totalCosts.budgetTotal = Number(totals.budgetTotal || 0);
+ totalCosts.actualTotal = Number(totals.actualTotal || 0);
+ totalCosts.diffTotal = Number(totals.diffTotal || 0);
+ totalCosts.diffRate = totals.diffRate ?? "0%";
+
+ updateChart();
+ } catch (e) {
+ settlementRows.value = [];
+ totalCosts.budgetTotal = 0;
+ totalCosts.actualTotal = 0;
+ totalCosts.diffTotal = 0;
+ totalCosts.diffRate = "0%";
+ updateChart();
+ }
};
const handleReset = () => {
- searchForm.monthRange = getDefaultMonthRange();
- searchForm.category = "";
+ searchForm.month = getDefaultMonth();
+ searchForm.productType = "";
searchForm.costType = "";
tableSort.prop = "";
tableSort.order = "";
@@ -735,6 +716,7 @@
};
const formatPercent = (v) => {
+ if (typeof v === "string" && v.trim().endsWith("%")) return v.trim();
const n = Number.parseFloat(v);
const value = Number.isFinite(n) ? n : 0;
const sign = value >= 0 ? "+" : "";
@@ -802,6 +784,9 @@
}
updateChart();
});
+ Promise.all([fetchCategoryOptions(), fetchCostTypeOptions()]).then(() => {
+ handleQuery();
+ });
window.addEventListener("resize", handleResize);
});
--
Gitblit v1.9.3