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 ++++++++++++++++++++++++++---------------------------
 1 files changed, 263 insertions(+), 278 deletions(-)

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