From 31328593a2e9d99ad82044bed3331ba070f657ff Mon Sep 17 00:00:00 2001
From: zhangwencui <1064582902@qq.com>
Date: 星期一, 23 三月 2026 11:48:59 +0800
Subject: [PATCH] Merge branch 'dev_银川_中盛建材' of http://114.132.189.42:9002/r/product-inventory-management into dev_银川_中盛建材

---
 src/views/costAccounting/stdVsActCostAnalysis/index.vue     |  849 ++++++++++++++++++++++++++++
 src/views/basicData/customerFile/index.vue                  |   30 
 src/views/productionManagement/productionOrder/index.vue    |    4 
 src/views/costAccounting/productionCostAccounting/index.vue |  905 ++++++++++++++++++++++++++++++
 4 files changed, 1,771 insertions(+), 17 deletions(-)

diff --git a/src/views/basicData/customerFile/index.vue b/src/views/basicData/customerFile/index.vue
index 16aec98..eb0b6a7 100644
--- a/src/views/basicData/customerFile/index.vue
+++ b/src/views/basicData/customerFile/index.vue
@@ -784,7 +784,7 @@
       label: "鎿嶄綔",
       align: "center",
       fixed: "right",
-      width: 250,
+      width: 150,
       operation: [
         {
           name: "缂栬緫",
@@ -800,20 +800,20 @@
             openDetailDialog(row);
           },
         },
-        {
-          name: "鍥炶鎻愰啋",
-          type: "text",
-          clickFun: row => {
-            openReminderDialog(row);
-          },
-        },
-        {
-          name: "娣诲姞娲借皥杩涘害",
-          type: "text",
-          clickFun: row => {
-            openNegotiationDialog(row);
-          },
-        },
+        // {
+        //   name: "鍥炶鎻愰啋",
+        //   type: "text",
+        //   clickFun: row => {
+        //     openReminderDialog(row);
+        //   },
+        // },
+        // {
+        //   name: "娣诲姞娲借皥杩涘害",
+        //   type: "text",
+        //   clickFun: row => {
+        //     openNegotiationDialog(row);
+        //   },
+        // },
       ],
     },
   ]);
diff --git a/src/views/costAccounting/productionCostAccounting/index.vue b/src/views/costAccounting/productionCostAccounting/index.vue
new file mode 100644
index 0000000..8fc60c2
--- /dev/null
+++ b/src/views/costAccounting/productionCostAccounting/index.vue
@@ -0,0 +1,905 @@
+<template>
+  <div class="production-cost-page">
+    <el-card class="filter-card" shadow="never">
+      <template #header>
+        <div class="card-head">
+          <div class="card-head-left">
+            <el-icon class="card-icon ui-icon"><DataLine /></el-icon>
+            <span class="card-title">鐢熶骇鎴愭湰鏍哥畻</span>
+            <span class="subtle">鎴愭湰 = 危 鎶曞叆閲� 脳 瀵瑰簲鍗曚环</span>
+          </div>
+          <div class="card-head-right">
+            <el-radio-group
+              v-model="statisticsType"
+              size="small"
+              @change="handleTypeChange"
+            >
+              <el-radio-button label="day">鎸夋棩</el-radio-button>
+              <el-radio-button label="month">鎸夋湀</el-radio-button>
+            </el-radio-group>
+          </div>
+        </div>
+      </template>
+
+      <div class="filter-layout">
+        <el-form :model="searchForm" :inline="true" class="filter-form">
+          <el-form-item label="鏃堕棿鑼冨洿">
+            <el-date-picker
+              v-if="statisticsType === 'day'"
+              v-model="searchForm.dateRange"
+              type="daterange"
+              range-separator="鑷�"
+              start-placeholder="寮�濮嬫棩鏈�"
+              end-placeholder="缁撴潫鏃ユ湡"
+              value-format="YYYY-MM-DD"
+              class="w-260"
+              @change="handleQuery"
+            />
+            <el-date-picker
+              v-else
+              v-model="searchForm.monthRange"
+              type="monthrange"
+              range-separator="鑷�"
+              start-placeholder="寮�濮嬫湀浠�"
+              end-placeholder="缁撴潫鏈堜唤"
+              value-format="YYYY-MM"
+              class="w-260"
+              @change="handleQuery"
+            />
+          </el-form-item>
+          <el-form-item label="浜у搧绫诲埆">
+            <el-select
+              v-model="searchForm.category"
+              clearable
+              filterable
+              placeholder="鍏ㄩ儴绫诲埆"
+              class="w-180"
+              @change="handleQuery"
+            >
+              <el-option
+                v-for="item in categoryOptions"
+                :key="item"
+                :label="item"
+                :value="item"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="鐢熶骇璁㈠崟">
+            <el-select
+              v-model="searchForm.orderNo"
+              clearable
+              filterable
+              placeholder="鍏ㄩ儴璁㈠崟"
+              class="w-180"
+              @change="handleQuery"
+            >
+              <el-option
+                v-for="item in orderOptions"
+                :key="item"
+                :label="item"
+                :value="item"
+              />
+            </el-select>
+          </el-form-item>
+        </el-form>
+        <div class="filter-actions">
+          <el-button class="lux-btn" type="primary" @click="handleQuery">
+            鍒锋柊
+          </el-button>
+          <el-button class="lux-btn" @click="handleReset">閲嶇疆</el-button>
+          <el-button class="lux-btn" type="success" plain @click="handleExport">
+            瀵煎嚭
+          </el-button>
+        </div>
+      </div>
+    </el-card>
+
+    <el-card class="panel-card" shadow="never">
+      <div class="kpi-strip">
+        <div class="kpi-item kpi-total">
+          <div class="kpi-label">鎬荤敓浜ф垚鏈�</div>
+          <div class="kpi-value">楼{{ formatMoney(overview.totalCost) }}</div>
+        </div>
+        <div class="kpi-item kpi-raw">
+          <div class="kpi-label">鍘熸枡鎴愭湰</div>
+          <div class="kpi-value">楼{{ formatMoney(overview.rawCost) }}</div>
+        </div>
+        <div class="kpi-item kpi-aux">
+          <div class="kpi-label">杈呮枡鎴愭湰</div>
+          <div class="kpi-value">楼{{ formatMoney(overview.auxCost) }}</div>
+        </div>
+        <div class="kpi-item kpi-order">
+          <div class="kpi-label">璁㈠崟鏁伴噺</div>
+          <div class="kpi-value">{{ overview.orderCount }}</div>
+        </div>
+      </div>
+    </el-card>
+
+    <el-row :gutter="14" class="summary-row">
+      <el-col :span="12">
+        <el-card class="table-card" shadow="never">
+          <template #header>
+            <div class="panel-head">
+              <span class="card-title">鎸変骇鍝佺被鍒眹鎬�</span>
+            </div>
+          </template>
+          <el-table :data="categorySummary" stripe class="lux-table" height="260">
+            <el-table-column prop="category" label="浜у搧绫诲埆" min-width="140" />
+            <el-table-column prop="rawCost" label="鍘熸枡鎴愭湰(鍏�)" align="right">
+              <template #default="scope">
+                <span class="price-value">{{ formatMoney(scope.row.rawCost) }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column prop="auxCost" label="杈呮枡鎴愭湰(鍏�)" align="right">
+              <template #default="scope">
+                <span class="price-value">{{ formatMoney(scope.row.auxCost) }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column prop="totalCost" label="鎬绘垚鏈�(鍏�)" align="right">
+              <template #default="scope">
+                <span class="cost-value">楼{{ formatMoney(scope.row.totalCost) }}</span>
+              </template>
+            </el-table-column>
+          </el-table>
+        </el-card>
+      </el-col>
+      <el-col :span="12">
+        <el-card class="table-card" shadow="never">
+          <template #header>
+            <div class="panel-head">
+              <span class="card-title">鎸夌敓浜ц鍗曟眹鎬�</span>
+            </div>
+          </template>
+          <el-table :data="orderSummary" stripe class="lux-table" height="260">
+            <el-table-column prop="orderNo" label="鐢熶骇璁㈠崟" min-width="150" />
+            <el-table-column prop="category" label="浜у搧绫诲埆" min-width="120" />
+            <el-table-column prop="totalCost" label="鎬绘垚鏈�(鍏�)" align="right">
+              <template #default="scope">
+                <span class="cost-value">楼{{ formatMoney(scope.row.totalCost) }}</span>
+              </template>
+            </el-table-column>
+          </el-table>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <el-card class="table-card" shadow="never">
+      <template #header>
+        <div class="panel-head">
+          <span class="card-title">澶氱淮搴︽眹鎬绘槑缁�</span>
+          <span class="subtle">{{ timeColumnLabel }} + 浜у搧绫诲埆 + 鐢熶骇璁㈠崟</span>
+        </div>
+      </template>
+
+      <el-table :data="pagedTableData" stripe class="lux-table">
+        <el-table-column prop="timeLabel" :label="timeColumnLabel" min-width="110" />
+        <el-table-column prop="category" label="浜у搧绫诲埆" min-width="120" />
+        <el-table-column prop="orderNo" label="鐢熶骇璁㈠崟" min-width="150" />
+        <el-table-column prop="rawCost" label="鍘熸枡鎴愭湰(鍏�)" align="right">
+          <template #default="scope">
+            <span class="price-value">{{ formatMoney(scope.row.rawCost) }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="auxCost" label="杈呮枡鎴愭湰(鍏�)" align="right">
+          <template #default="scope">
+            <span class="price-value">{{ formatMoney(scope.row.auxCost) }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="totalCost" label="鎬绘垚鏈�(鍏�)" align="right">
+          <template #default="scope">
+            <span class="cost-value">楼{{ formatMoney(scope.row.totalCost) }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="鎷嗗垎鏄庣粏" width="92" fixed="right">
+          <template #default="scope">
+            <el-button link type="primary" @click="openDetail(scope.row)">鏌ョ湅</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <div class="pagination-container">
+        <el-pagination
+          v-model:current-page="page.current"
+          v-model:page-size="page.size"
+          :page-sizes="[10, 20, 50, 100]"
+          :total="tableData.length"
+          layout="total, sizes, prev, pager, next, jumper"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange"
+        />
+      </div>
+    </el-card>
+
+    <el-drawer
+      v-model="detailVisible"
+      :with-header="false"
+      class="detail-drawer"
+      size="760px"
+      :close-on-click-modal="true"
+      :close-on-press-escape="true"
+      destroy-on-close
+    >
+      <div v-if="detailRow" class="drawer-head">
+        <div class="meta-item">
+          <span class="meta-label">{{ timeColumnLabel }}</span>
+          <span class="meta-value">{{ detailRow.timeLabel }}</span>
+        </div>
+        <div class="meta-item">
+          <span class="meta-label">浜у搧绫诲埆</span>
+          <span class="meta-value">{{ detailRow.category }}</span>
+        </div>
+        <div class="meta-item">
+          <span class="meta-label">鐢熶骇璁㈠崟</span>
+          <span class="meta-value">{{ detailRow.orderNo }}</span>
+        </div>
+      </div>
+      <el-table :data="detailMaterials" class="lux-table" stripe>
+        <el-table-column prop="materialName" label="鐗╂枡鍚嶇О" min-width="120" />
+        <el-table-column prop="materialType" label="绫诲瀷" width="94">
+          <template #default="scope">
+            <span
+              class="material-type-tag"
+              :class="scope.row.materialType === '鍘熸枡' ? 'is-raw' : 'is-aux'"
+            >
+              {{ scope.row.materialType }}
+            </span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="quantity" label="鎶曞叆閲�" align="right" min-width="140">
+          <template #default="scope">
+            <span class="quantity-cell">
+              <span class="quantity-value">{{ formatNumber(scope.row.quantity, 2) }}</span>
+              <span class="quantity-unit">{{ scope.row.unit }}</span>
+            </span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="unitPrice" label="鍗曚环(鍏�)" align="right">
+          <template #default="scope">
+            {{ formatNumber(scope.row.unitPrice, 2) }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="cost" label="鎴愭湰(鍏�)" align="right" min-width="132">
+          <template #default="scope">
+            <span class="cost-value no-wrap-money">楼{{ formatMoney(scope.row.cost) }}</span>
+          </template>
+        </el-table-column>
+      </el-table>
+      <div class="drawer-foot">
+        <div class="foot-item">
+          <span class="foot-label">鍘熸枡</span>
+          <span class="foot-value no-wrap-money">楼{{ formatMoney(detailRawCost) }}</span>
+        </div>
+        <div class="foot-item">
+          <span class="foot-label">杈呮枡</span>
+          <span class="foot-value no-wrap-money">楼{{ formatMoney(detailAuxCost) }}</span>
+        </div>
+        <div class="foot-item total">
+          <span class="foot-label">鍚堣</span>
+          <span class="foot-value no-wrap-money">楼{{ formatMoney(detailTotalCost) }}</span>
+        </div>
+      </div>
+    </el-drawer>
+  </div>
+</template>
+
+<script setup>
+import { computed, reactive, ref, watch } from "vue";
+import { DataLine } from "@element-plus/icons-vue";
+import { ElMessage } from "element-plus";
+
+const statisticsType = ref("day");
+
+const getDefaultDateRange = () => {
+  const end = new Date();
+  const start = new Date();
+  start.setDate(start.getDate() - 6);
+  return [start.toISOString().slice(0, 10), end.toISOString().slice(0, 10)];
+};
+
+const getDefaultMonthRange = () => {
+  const end = new Date();
+  const start = new Date();
+  start.setMonth(start.getMonth() - 2);
+  return [start.toISOString().slice(0, 7), end.toISOString().slice(0, 7)];
+};
+
+const searchForm = reactive({
+  dateRange: getDefaultDateRange(),
+  monthRange: getDefaultMonthRange(),
+  category: "",
+  orderNo: "",
+});
+
+const sourceRecords = ref([
+  { date: "2026-03-17", category: "鐡风爾", orderNo: "PO-260317-01", materialName: "闄剁摲绮�", materialType: "鍘熸枡", quantity: 1200, unit: "kg", unitPrice: 2.8 },
+  { date: "2026-03-17", category: "鐡风爾", orderNo: "PO-260317-01", materialName: "閲夋枡", materialType: "杈呮枡", quantity: 180, unit: "kg", unitPrice: 8.6 },
+  { date: "2026-03-17", category: "姘存偿", orderNo: "PO-260317-02", materialName: "鐔熸枡", materialType: "鍘熸枡", quantity: 2200, unit: "kg", unitPrice: 1.36 },
+  { date: "2026-03-17", category: "姘存偿", orderNo: "PO-260317-02", materialName: "鐭宠啅", materialType: "杈呮枡", quantity: 260, unit: "kg", unitPrice: 0.92 },
+  { date: "2026-03-18", category: "鐮傛祮", orderNo: "PO-260318-01", materialName: "鏈哄埗鐮�", materialType: "鍘熸枡", quantity: 1600, unit: "kg", unitPrice: 0.58 },
+  { date: "2026-03-18", category: "鐮傛祮", orderNo: "PO-260318-01", materialName: "淇濇按鍓�", materialType: "杈呮枡", quantity: 65, unit: "kg", unitPrice: 11.4 },
+  { date: "2026-03-19", category: "鐡风爾", orderNo: "PO-260319-01", materialName: "闄剁摲绮�", materialType: "鍘熸枡", quantity: 980, unit: "kg", unitPrice: 2.9 },
+  { date: "2026-03-19", category: "鐡风爾", orderNo: "PO-260319-01", materialName: "鑹叉枡", materialType: "杈呮枡", quantity: 42, unit: "kg", unitPrice: 15.8 },
+  { date: "2026-03-19", category: "鐮傛祮", orderNo: "PO-260319-03", materialName: "鏈哄埗鐮�", materialType: "鍘熸枡", quantity: 1400, unit: "kg", unitPrice: 0.56 },
+  { date: "2026-03-19", category: "鐮傛祮", orderNo: "PO-260319-03", materialName: "鍑忔按鍓�", materialType: "杈呮枡", quantity: 74, unit: "kg", unitPrice: 7.2 },
+  { date: "2026-03-20", category: "姘存偿", orderNo: "PO-260320-02", materialName: "鐔熸枡", materialType: "鍘熸枡", quantity: 2400, unit: "kg", unitPrice: 1.33 },
+  { date: "2026-03-20", category: "姘存偿", orderNo: "PO-260320-02", materialName: "鐭跨矇", materialType: "杈呮枡", quantity: 380, unit: "kg", unitPrice: 1.08 },
+]);
+
+const normalizedRecords = computed(() =>
+  sourceRecords.value.map((item) => {
+    const month = item.date.slice(0, 7);
+    const cost = Number(item.quantity) * Number(item.unitPrice);
+    return { ...item, month, cost };
+  })
+);
+
+const categoryOptions = computed(() =>
+  Array.from(new Set(normalizedRecords.value.map((item) => item.category)))
+);
+
+const orderOptions = computed(() =>
+  Array.from(new Set(normalizedRecords.value.map((item) => item.orderNo)))
+);
+
+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 getMonthRangeDays = (monthRange) => {
+  if (!Array.isArray(monthRange) || monthRange.length !== 2 || !monthRange[0] || !monthRange[1]) {
+    return 0;
+  }
+  const [startMonth, endMonth] = monthRange;
+  const startDate = new Date(`${startMonth}-01T00:00:00`);
+  const endDate = new Date(`${endMonth}-01T00:00:00`);
+  if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime()) || startDate > endDate) {
+    return 0;
+  }
+  const endMonthLastDay = new Date(endDate.getFullYear(), endDate.getMonth() + 1, 0);
+  const diffMs = endMonthLastDay.getTime() - startDate.getTime();
+  return Math.floor(diffMs / (24 * 60 * 60 * 1000)) + 1;
+};
+
+const buildQueryParams = () => {
+  const isDay = statisticsType.value === "day";
+  const params = {
+    statisticsType: statisticsType.value,
+    category: searchForm.category || undefined,
+    orderNo: searchForm.orderNo || undefined,
+  };
+
+  if (isDay) {
+    const [startDate, endDate] = searchForm.dateRange || [];
+    params.startDate = startDate;
+    params.endDate = endDate;
+  } else {
+    const [startMonth, endMonth] = searchForm.monthRange || [];
+    params.startMonth = startMonth;
+    params.endMonth = endMonth;
+    params.days = getMonthRangeDays(searchForm.monthRange);
+  }
+
+  return params;
+};
+
+const filteredRecords = computed(() =>
+  normalizedRecords.value.filter((item) => {
+    const hitTime =
+      statisticsType.value === "day"
+        ? inRange(item.date, searchForm.dateRange)
+        : inRange(item.month, searchForm.monthRange);
+    const hitCategory = !searchForm.category || item.category === searchForm.category;
+    const hitOrder = !searchForm.orderNo || item.orderNo === searchForm.orderNo;
+    return hitTime && hitCategory && hitOrder;
+  })
+);
+
+const timeColumnLabel = computed(() => (statisticsType.value === "day" ? "鏃ユ湡" : "鏈堜唤"));
+
+const aggregateBy = (list, keyFn) => {
+  const map = new Map();
+  for (const item of list) {
+    const key = keyFn(item);
+    if (!map.has(key)) {
+      map.set(key, {
+        rawCost: 0,
+        auxCost: 0,
+        totalCost: 0,
+        materials: [],
+      });
+    }
+    const bucket = map.get(key);
+    if (item.materialType === "鍘熸枡") bucket.rawCost += item.cost;
+    if (item.materialType === "杈呮枡") bucket.auxCost += item.cost;
+    bucket.totalCost += item.cost;
+    bucket.materials.push(item);
+  }
+  return map;
+};
+
+const groupedMap = computed(() =>
+  aggregateBy(filteredRecords.value, (item) => {
+    const timeKey = statisticsType.value === "day" ? item.date : item.month;
+    return `${timeKey}__${item.category}__${item.orderNo}`;
+  })
+);
+
+const tableData = computed(() => {
+  const rows = [];
+  for (const [key, val] of groupedMap.value) {
+    const [timeLabel, category, orderNo] = key.split("__");
+    rows.push({
+      key,
+      timeLabel,
+      category,
+      orderNo,
+      rawCost: val.rawCost,
+      auxCost: val.auxCost,
+      totalCost: val.totalCost,
+      materials: val.materials,
+    });
+  }
+  return rows.sort((a, b) => (a.timeLabel > b.timeLabel ? -1 : 1));
+});
+
+const page = reactive({
+  current: 1,
+  size: 10,
+});
+
+const pagedTableData = computed(() => {
+  const start = (page.current - 1) * page.size;
+  return tableData.value.slice(start, start + page.size);
+});
+
+const categorySummary = computed(() => {
+  const map = aggregateBy(filteredRecords.value, (item) => item.category);
+  const rows = [];
+  for (const [category, val] of map) {
+    rows.push({
+      category,
+      rawCost: val.rawCost,
+      auxCost: val.auxCost,
+      totalCost: val.totalCost,
+    });
+  }
+  return rows.sort((a, b) => b.totalCost - a.totalCost);
+});
+
+const orderSummary = computed(() => {
+  const map = aggregateBy(filteredRecords.value, (item) => item.orderNo);
+  const rows = [];
+  for (const [orderNo, val] of map) {
+    rows.push({
+      orderNo,
+      category: val.materials[0]?.category || "-",
+      totalCost: val.totalCost,
+    });
+  }
+  return rows.sort((a, b) => b.totalCost - a.totalCost);
+});
+
+const overview = computed(() => {
+  const rawCost = filteredRecords.value
+    .filter((item) => item.materialType === "鍘熸枡")
+    .reduce((sum, item) => sum + item.cost, 0);
+  const auxCost = filteredRecords.value
+    .filter((item) => item.materialType === "杈呮枡")
+    .reduce((sum, item) => sum + item.cost, 0);
+  const orderCount = new Set(filteredRecords.value.map((item) => item.orderNo)).size;
+  return {
+    rawCost,
+    auxCost,
+    totalCost: rawCost + auxCost,
+    orderCount,
+  };
+});
+
+const detailVisible = ref(false);
+const detailRow = ref(null);
+
+const detailMaterials = computed(() => detailRow.value?.materials || []);
+
+const detailRawCost = computed(() =>
+  detailMaterials.value
+    .filter((item) => item.materialType === "鍘熸枡")
+    .reduce((sum, item) => sum + item.cost, 0)
+);
+const detailAuxCost = computed(() =>
+  detailMaterials.value
+    .filter((item) => item.materialType === "杈呮枡")
+    .reduce((sum, item) => sum + item.cost, 0)
+);
+const detailTotalCost = computed(() =>
+  detailMaterials.value.reduce((sum, item) => sum + item.cost, 0)
+);
+
+const openDetail = (row) => {
+  detailRow.value = row;
+  detailVisible.value = true;
+};
+
+const handleTypeChange = () => {
+  handleQuery();
+};
+
+const handleQuery = () => {
+  page.current = 1;
+  const queryParams = buildQueryParams();
+  console.log("[productionCostAccounting] query params:", queryParams);
+  ElMessage.success("宸叉寜鏉′欢瀹屾垚姹囨��");
+};
+
+const handleReset = () => {
+  searchForm.dateRange = getDefaultDateRange();
+  searchForm.monthRange = getDefaultMonthRange();
+  searchForm.category = "";
+  searchForm.orderNo = "";
+  handleQuery();
+};
+
+const handleSizeChange = (val) => {
+  page.size = val;
+  page.current = 1;
+};
+
+const handleCurrentChange = (val) => {
+  page.current = val;
+};
+
+const handleExport = () => {
+  const headers = [timeColumnLabel.value, "浜у搧绫诲埆", "鐢熶骇璁㈠崟", "鍘熸枡鎴愭湰", "杈呮枡鎴愭湰", "鎬绘垚鏈�"];
+  const lines = tableData.value.map((row) =>
+    [
+      row.timeLabel,
+      row.category,
+      row.orderNo,
+      row.rawCost.toFixed(2),
+      row.auxCost.toFixed(2),
+      row.totalCost.toFixed(2),
+    ].join(",")
+  );
+  const csv = [headers.join(","), ...lines].join("\n");
+  const blob = new Blob(["\uFEFF" + csv], { type: "text/csv;charset=utf-8;" });
+  const url = URL.createObjectURL(blob);
+  const link = document.createElement("a");
+  link.href = url;
+  link.download = `鐢熶骇鎴愭湰姹囨�籣${statisticsType.value}_${Date.now()}.csv`;
+  link.click();
+  URL.revokeObjectURL(url);
+  ElMessage.success("瀵煎嚭鎴愬姛");
+};
+
+const formatMoney = (v) => {
+  const n = Number.parseFloat(v);
+  const value = Number.isFinite(n) ? n : 0;
+  return value.toLocaleString("zh-CN", {
+    minimumFractionDigits: 2,
+    maximumFractionDigits: 2,
+  });
+};
+
+const formatNumber = (v, digits = 2) => {
+  const n = Number.parseFloat(v);
+  if (!Number.isFinite(n)) return "--";
+  return n.toLocaleString("zh-CN", {
+    minimumFractionDigits: digits,
+    maximumFractionDigits: digits,
+  });
+};
+
+watch(tableData, () => {
+  const maxPage = Math.max(1, Math.ceil(tableData.value.length / page.size));
+  if (page.current > maxPage) page.current = maxPage;
+});
+</script>
+
+<style scoped lang="scss">
+.production-cost-page {
+  --lux-bg: #f6f7fb;
+  --lux-card: rgba(255, 255, 255, 0.86);
+  --lux-border: rgba(15, 23, 42, 0.08);
+  --lux-text: rgba(15, 23, 42, 0.92);
+  --lux-subtle: rgba(15, 23, 42, 0.58);
+  --lux-muted: rgba(15, 23, 42, 0.38);
+  --lux-primary: #2f6fed;
+  --lux-success: #16a34a;
+  --lux-warning: #f59e0b;
+  --lux-danger: #ef4444;
+  --lux-shadow: 0 18px 50px rgba(15, 23, 42, 0.08);
+  --lux-shadow-soft: 0 10px 28px rgba(15, 23, 42, 0.06);
+  --lux-radius: 14px;
+
+  padding: 18px 22px 24px;
+  background: radial-gradient(
+      1200px 420px at 20% 0%,
+      rgba(47, 111, 237, 0.1),
+      transparent 55%
+    ),
+    linear-gradient(180deg, var(--lux-bg) 0%, #ffffff 58%);
+}
+
+.filter-card,
+.panel-card,
+.table-card {
+  border-radius: var(--lux-radius);
+  border-color: var(--lux-border);
+  background: var(--lux-card);
+  box-shadow: var(--lux-shadow-soft);
+}
+
+.filter-card {
+  margin-bottom: 16px;
+}
+
+.panel-card,
+.summary-row {
+  margin-bottom: 14px;
+}
+
+.filter-layout {
+  display: flex;
+  align-items: flex-start;
+  justify-content: space-between;
+  gap: 14px;
+}
+
+.filter-form {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px 14px;
+}
+
+.filter-form :deep(.el-form-item) {
+  margin: 0;
+}
+
+.filter-actions {
+  display: flex;
+  gap: 10px;
+}
+
+.card-head,
+.panel-head {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+}
+
+.card-head-left {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.card-head-right {
+  display: flex;
+  align-items: center;
+}
+
+.card-icon {
+  color: var(--lux-primary);
+}
+
+.card-title {
+  font-weight: 760;
+  color: var(--lux-text);
+}
+
+.subtle {
+  color: var(--lux-subtle);
+  font-size: 12px;
+}
+
+.kpi-strip {
+  display: grid;
+  grid-template-columns: repeat(4, minmax(0, 1fr));
+  gap: 12px;
+}
+
+.kpi-item {
+  padding: 12px 14px;
+  border-radius: 12px;
+  border: 1px solid rgba(15, 23, 42, 0.08);
+}
+
+.kpi-total {
+  background: linear-gradient(135deg, rgba(47, 111, 237, 0.1), rgba(255, 255, 255, 0.86));
+}
+
+.kpi-raw {
+  background: linear-gradient(135deg, rgba(22, 163, 74, 0.1), rgba(255, 255, 255, 0.86));
+}
+
+.kpi-aux {
+  background: linear-gradient(135deg, rgba(245, 158, 11, 0.1), rgba(255, 255, 255, 0.86));
+}
+
+.kpi-order {
+  background: linear-gradient(135deg, rgba(100, 116, 139, 0.1), rgba(255, 255, 255, 0.86));
+}
+
+.kpi-label {
+  font-size: 12px;
+  color: var(--lux-subtle);
+}
+
+.kpi-value {
+  font-size: 22px;
+  margin-top: 6px;
+  font-weight: 780;
+  color: var(--lux-text);
+}
+
+.price-value {
+  font-weight: 700;
+  color: var(--lux-success);
+}
+
+.cost-value {
+  font-weight: 700;
+  color: var(--lux-danger);
+}
+
+.no-wrap-money {
+  display: inline-block;
+  white-space: nowrap;
+}
+
+.drawer-head {
+  display: grid;
+  grid-template-columns: repeat(3, minmax(0, 1fr));
+  gap: 10px;
+  margin-bottom: 12px;
+}
+
+.meta-item {
+  padding: 10px 12px;
+  border-radius: 10px;
+  border: 1px solid var(--lux-border);
+  background: rgba(15, 23, 42, 0.03);
+  display: grid;
+  gap: 4px;
+}
+
+.meta-label {
+  font-size: 12px;
+  color: var(--lux-subtle);
+}
+
+.meta-value {
+  color: var(--lux-text);
+  font-size: 16px;
+  font-weight: 700;
+}
+
+.material-type-tag {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  min-width: 46px;
+  height: 24px;
+  padding: 0 8px;
+  border-radius: 999px;
+  font-size: 12px;
+  font-weight: 700;
+}
+
+.material-type-tag.is-raw {
+  color: #15803d;
+  background: rgba(22, 163, 74, 0.12);
+}
+
+.material-type-tag.is-aux {
+  color: #b45309;
+  background: rgba(245, 158, 11, 0.16);
+}
+
+.quantity-value {
+  font-weight: 700;
+  color: var(--lux-text);
+  margin-right: 6px;
+}
+
+.quantity-cell {
+  display: inline-flex;
+  align-items: baseline;
+  white-space: nowrap;
+}
+
+.quantity-unit {
+  color: var(--lux-subtle);
+}
+
+.drawer-foot {
+  display: flex;
+  justify-content: flex-end;
+  gap: 12px;
+  margin-top: 12px;
+  padding-top: 12px;
+  border-top: 1px dashed var(--lux-border);
+}
+
+.foot-item {
+  display: inline-flex;
+  align-items: center;
+  gap: 8px;
+  padding: 7px 16px;
+  border-radius: 999px;
+  border: 1px solid var(--lux-border);
+  background: #fff;
+}
+
+.foot-label {
+  color: var(--lux-subtle);
+  font-size: 14px;
+}
+
+.foot-value {
+  color: var(--lux-text);
+  font-weight: 700;
+  font-size: 16px;
+}
+
+.foot-item.total {
+  border-color: rgba(47, 111, 237, 0.26);
+  background: rgba(47, 111, 237, 0.08);
+}
+
+.foot-item.total .foot-value {
+  color: #1e3a8a;
+  font-size: 18px;
+  font-weight: 800;
+}
+
+.pagination-container {
+  display: flex;
+  justify-content: flex-end;
+  padding-top: 12px;
+}
+
+.strong {
+  font-weight: 800;
+}
+
+.w-260 {
+  width: 260px;
+}
+
+.w-180 {
+  width: 180px;
+}
+
+::deep(.lux-table) {
+  border-radius: 12px;
+  overflow: hidden;
+}
+
+::deep(.lux-table th.el-table__cell) {
+  background: rgba(15, 23, 42, 0.03);
+}
+
+::deep(.lux-table .el-table__row:hover > td.el-table__cell) {
+  background-color: rgba(47, 111, 237, 0.06) !important;
+}
+
+@media (max-width: 1100px) {
+  .kpi-strip {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
+
+  .filter-layout {
+    flex-direction: column;
+  }
+
+  .drawer-head {
+    grid-template-columns: 1fr;
+  }
+
+  .drawer-foot {
+    justify-content: flex-start;
+    flex-wrap: wrap;
+  }
+}
+</style>
diff --git a/src/views/costAccounting/stdVsActCostAnalysis/index.vue b/src/views/costAccounting/stdVsActCostAnalysis/index.vue
new file mode 100644
index 0000000..45b0e25
--- /dev/null
+++ b/src/views/costAccounting/stdVsActCostAnalysis/index.vue
@@ -0,0 +1,849 @@
+<template>
+  <div class="std-cost-page">
+    <el-card class="filter-card" shadow="never">
+      <template #header>
+        <div class="card-head">
+          <div class="card-head-left">
+            <el-icon class="card-icon ui-icon"><DataLine /></el-icon>
+            <span class="card-title">鏍囧噯/瀹為檯鎴愭湰瀵规瘮鍒嗘瀽</span>
+            <span class="subtle">宸紓 = 瀹為檯鎴愭湰 - 鏍囧噯鎴愭湰</span>
+          </div>
+        </div>
+      </template>
+
+      <div class="filter-layout">
+        <el-form :model="searchForm" :inline="true" class="filter-form">
+          <el-form-item label="鏈堜唤鑼冨洿">
+            <el-date-picker
+              v-model="searchForm.monthRange"
+              type="monthrange"
+              range-separator="鑷�"
+              start-placeholder="寮�濮嬫湀浠�"
+              end-placeholder="缁撴潫鏈堜唤"
+              value-format="YYYY-MM"
+              class="w-260"
+              @change="handleQuery"
+            />
+          </el-form-item>
+          <el-form-item label="浜у搧绫诲埆">
+            <el-select
+              v-model="searchForm.category"
+              clearable
+              filterable
+              placeholder="鍏ㄩ儴绫诲埆"
+              class="w-180"
+              @change="handleQuery"
+            >
+              <el-option
+                v-for="item in categoryOptions"
+                :key="item"
+                :label="item"
+                :value="item"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="鎴愭湰绫诲瀷">
+            <el-select
+              v-model="searchForm.costType"
+              clearable
+              placeholder="鍏ㄩ儴绫诲瀷"
+              class="w-180"
+              @change="handleQuery"
+            >
+              <el-option label="鑳借�楁垚鏈�" value="鑳借�楁垚鏈�" />
+              <el-option label="鐢熶骇鎴愭湰" value="鐢熶骇鎴愭湰" />
+            </el-select>
+          </el-form-item>
+        </el-form>
+
+        <div class="filter-actions">
+          <div class="action-group">
+            <el-button class="lux-btn" type="primary" @click="handleQuery">鍒锋柊</el-button>
+            <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"
+            />
+          </div>
+        </div>
+      </div>
+    </el-card>
+
+    <el-card class="panel-card" shadow="never">
+      <div class="kpi-strip">
+        <div class="kpi-item kpi-std">
+          <div class="kpi-label">鏍囧噯鎴愭湰鍚堣</div>
+          <div class="kpi-value">楼{{ formatMoney(overview.standardCost) }}</div>
+        </div>
+        <div class="kpi-item kpi-act">
+          <div class="kpi-label">瀹為檯鎴愭湰鍚堣</div>
+          <div class="kpi-value">楼{{ formatMoney(overview.actualCost) }}</div>
+        </div>
+        <div class="kpi-item kpi-diff">
+          <div class="kpi-label">宸紓鍚堣</div>
+          <div class="kpi-value" :class="overview.diff >= 0 ? 'cost-value' : 'ok-value'">
+            楼{{ formatMoney(overview.diff) }}
+          </div>
+        </div>
+        <div class="kpi-item kpi-rate">
+          <div class="kpi-label">宸紓鐜�</div>
+          <div class="kpi-value">{{ formatPercent(overview.diffRate) }}</div>
+        </div>
+      </div>
+    </el-card>
+
+    <el-card class="table-card" shadow="never">
+      <template #header>
+        <div class="panel-head">
+          <span class="card-title">鏍囧噯/瀹為檯鎴愭湰鍙鍖栵紙鏌辩姸 + 鎶樼嚎锛�</span>
+          <span class="subtle">鏀寔鎸夋湀浠姐�佷骇鍝佺被鍒�佹垚鏈被鍨嬬瓫閫�</span>
+        </div>
+      </template>
+      <div class="chart-wrap">
+        <div class="chart-tools chart-tools-inline" @click.stop>
+          <button class="chart-tool" type="button" @click="openLargeChart">鏌ョ湅澶у浘</button>
+          <button class="chart-tool" type="button" @click="downloadChartImage">涓嬭浇鍥捐〃</button>
+        </div>
+        <div ref="chartRef" class="chart-content"></div>
+      </div>
+    </el-card>
+
+    <el-dialog
+      v-model="largeChartVisible"
+      title="鏍囧噯/瀹為檯鎴愭湰瀵规瘮澶у浘"
+      width="88%"
+      top="6vh"
+      destroy-on-close
+      @opened="initLargeChart"
+      @closed="disposeLargeChart"
+    >
+      <div ref="largeChartRef" class="large-chart-content"></div>
+    </el-dialog>
+
+    <el-card class="table-card" shadow="never">
+      <template #header>
+        <div class="panel-head">
+          <span class="card-title">瀵规瘮鏄庣粏</span>
+          <span class="subtle">鍏� {{ tableData.length }} 鏉�</span>
+        </div>
+      </template>
+      <el-table :data="pagedTableData" stripe class="lux-table">
+        <el-table-column prop="month" label="鏈堜唤" width="110" />
+        <el-table-column prop="category" label="浜у搧绫诲埆" min-width="140" />
+        <el-table-column prop="costType" label="鎴愭湰绫诲瀷" min-width="120" />
+        <el-table-column prop="standardCost" label="鏍囧噯鎴愭湰(鍏�)" align="right">
+          <template #default="scope">楼{{ formatMoney(scope.row.standardCost) }}</template>
+        </el-table-column>
+        <el-table-column prop="actualCost" label="瀹為檯鎴愭湰(鍏�)" align="right">
+          <template #default="scope">楼{{ formatMoney(scope.row.actualCost) }}</template>
+        </el-table-column>
+        <el-table-column prop="diff" label="宸紓(鍏�)" 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="宸紓鐜�" 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>
+      <div class="pagination-container">
+        <el-pagination
+          v-model:current-page="page.current"
+          v-model:page-size="page.size"
+          :page-sizes="[10, 20, 50, 100]"
+          :total="tableData.length"
+          layout="total, sizes, prev, pager, next, jumper"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange"
+        />
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script setup>
+import { computed, nextTick, onMounted, onUnmounted, reactive, ref, watch } from "vue";
+import { ArrowDown, DataLine } from "@element-plus/icons-vue";
+import { ElMessage } from "element-plus";
+import * as echarts from "echarts";
+import * as XLSX from "xlsx";
+
+const getDefaultMonthRange = () => {
+  const end = new Date();
+  const start = new Date();
+  start.setMonth(start.getMonth() - 2);
+  return [start.toISOString().slice(0, 7), end.toISOString().slice(0, 7)];
+};
+
+const searchForm = reactive({
+  monthRange: getDefaultMonthRange(),
+  category: "",
+  costType: "",
+});
+
+const uploadRef = ref();
+const chartRef = ref(null);
+const largeChartRef = ref(null);
+let chartInstance = null;
+let largeChartInstance = null;
+const largeChartVisible = ref(false);
+const currentChartOption = ref(null);
+
+const actualCostSource = ref([
+  { month: "2026-01", category: "鐡风爾", costType: "鑳借�楁垚鏈�", actualCost: 182000 },
+  { month: "2026-01", category: "鐡风爾", costType: "鐢熶骇鎴愭湰", actualCost: 465000 },
+  { month: "2026-01", category: "姘存偿", costType: "鑳借�楁垚鏈�", actualCost: 138500 },
+  { month: "2026-01", category: "姘存偿", costType: "鐢熶骇鎴愭湰", actualCost: 398000 },
+  { month: "2026-02", category: "鐡风爾", costType: "鑳借�楁垚鏈�", actualCost: 191500 },
+  { month: "2026-02", category: "鐡风爾", costType: "鐢熶骇鎴愭湰", actualCost: 472500 },
+  { month: "2026-02", category: "姘存偿", costType: "鑳借�楁垚鏈�", actualCost: 142300 },
+  { month: "2026-02", category: "姘存偿", costType: "鐢熶骇鎴愭湰", actualCost: 407000 },
+  { month: "2026-03", category: "鐮傛祮", costType: "鑳借�楁垚鏈�", actualCost: 95800 },
+  { month: "2026-03", category: "鐮傛祮", costType: "鐢熶骇鎴愭湰", actualCost: 265400 },
+  { month: "2026-03", category: "鐡风爾", costType: "鑳借�楁垚鏈�", actualCost: 189800 },
+  { month: "2026-03", category: "鐡风爾", costType: "鐢熶骇鎴愭湰", actualCost: 469900 },
+]);
+
+const standardCostSource = ref([
+  { month: "2026-01", category: "鐡风爾", costType: "鑳借�楁垚鏈�", standardCost: 176000 },
+  { month: "2026-01", category: "鐡风爾", costType: "鐢熶骇鎴愭湰", standardCost: 452000 },
+  { month: "2026-01", category: "姘存偿", costType: "鑳借�楁垚鏈�", standardCost: 136000 },
+  { month: "2026-01", category: "姘存偿", costType: "鐢熶骇鎴愭湰", standardCost: 392000 },
+  { month: "2026-02", category: "鐡风爾", costType: "鑳借�楁垚鏈�", standardCost: 186000 },
+  { month: "2026-02", category: "鐡风爾", costType: "鐢熶骇鎴愭湰", standardCost: 458000 },
+  { month: "2026-02", category: "姘存偿", costType: "鑳借�楁垚鏈�", standardCost: 139000 },
+  { month: "2026-02", category: "姘存偿", costType: "鐢熶骇鎴愭湰", standardCost: 401000 },
+  { month: "2026-03", category: "鐮傛祮", costType: "鑳借�楁垚鏈�", standardCost: 93000 },
+  { month: "2026-03", category: "鐮傛祮", costType: "鐢熶骇鎴愭湰", standardCost: 259000 },
+  { month: "2026-03", category: "鐡风爾", costType: "鑳借�楁垚鏈�", standardCost: 185000 },
+  { month: "2026-03", category: "鐡风爾", costType: "鐢熶骇鎴愭湰", standardCost: 461000 },
+]);
+
+const categoryOptions = computed(() => {
+  const all = [...actualCostSource.value, ...standardCostSource.value];
+  return Array.from(new Set(all.map((item) => item.category)));
+});
+
+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(() =>
+  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,
+  size: 10,
+});
+
+const pagedTableData = computed(() => {
+  const start = (page.current - 1) * page.size;
+  return tableData.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 getChartData = () => {
+  const xAxis = tableData.value.map(
+    (item) => `${item.month}\n${item.category}-${item.costType.replace("鎴愭湰", "")}`
+  );
+  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 buildChartOption = () => {
+  const { xAxis, standard, actual, diffRate } = getChartData();
+  return {
+    tooltip: {
+      trigger: "axis",
+      axisPointer: { type: "shadow" },
+      formatter: (params) => {
+        const row = tableData.value[params[0]?.dataIndex] || {};
+        return [
+          `${row.month || ""} ${row.category || ""} ${row.costType || ""}`,
+          `鏍囧噯鎴愭湰锛毬�${formatMoney(row.standardCost || 0)}`,
+          `瀹為檯鎴愭湰锛毬�${formatMoney(row.actualCost || 0)}`,
+          `宸紓锛�${formatSignedMoney(row.diff || 0)}`,
+          `宸紓鐜囷細${formatPercent(row.diffRate || 0)}`,
+        ].join("<br/>");
+      },
+    },
+    legend: { data: ["鏍囧噯鎴愭湰", "瀹為檯鎴愭湰", "宸紓鐜�"] },
+    grid: { left: "4%", right: "4%", top: "16%", bottom: "16%", containLabel: true },
+    xAxis: {
+      type: "category",
+      data: xAxis,
+      axisLabel: { color: "rgba(15, 23, 42, 0.62)" },
+      axisLine: { lineStyle: { color: "rgba(15, 23, 42, 0.08)" } },
+    },
+    yAxis: [
+      {
+        type: "value",
+        name: "鎴愭湰(鍏�)",
+        axisLabel: { color: "rgba(15, 23, 42, 0.58)" },
+        splitLine: { lineStyle: { color: "rgba(15, 23, 42, 0.06)" } },
+      },
+      {
+        type: "value",
+        name: "宸紓鐜�(%)",
+        axisLabel: { color: "rgba(15, 23, 42, 0.58)" },
+        splitLine: { show: false },
+      },
+    ],
+    series: [
+      {
+        name: "鏍囧噯鎴愭湰",
+        type: "bar",
+        barMaxWidth: 24,
+        data: standard,
+        itemStyle: { color: "#5b8cff", borderRadius: [4, 4, 0, 0] },
+      },
+      {
+        name: "瀹為檯鎴愭湰",
+        type: "bar",
+        barMaxWidth: 24,
+        data: actual,
+        itemStyle: { color: "#f59e0b", borderRadius: [4, 4, 0, 0] },
+      },
+      {
+        name: "宸紓鐜�",
+        type: "line",
+        yAxisIndex: 1,
+        smooth: true,
+        data: diffRate,
+        itemStyle: { color: "#ef4444" },
+        lineStyle: { width: 2 },
+      },
+    ],
+  };
+};
+
+const updateChart = () => {
+  const option = buildChartOption();
+  currentChartOption.value = option;
+  chartInstance?.setOption(option);
+  largeChartInstance?.setOption(option);
+};
+
+const normalizeCostType = (value) => {
+  const text = String(value || "").trim();
+  if (!text) return "";
+  if (text.includes("鑳借��")) return "鑳借�楁垚鏈�";
+  if (text.includes("鐢熶骇")) return "鐢熶骇鎴愭湰";
+  return text;
+};
+
+const parseImportedRows = (rows) => {
+  const normalized = rows
+    .map((item) => {
+      const month = String(item["鏈堜唤"] || item.month || "").slice(0, 7);
+      const category = String(item["浜у搧绫诲埆"] || item.category || "").trim();
+      const costType = normalizeCostType(item["鎴愭湰绫诲瀷"] || item.costType);
+      const standardCost = Number(item["鏍囧噯鎴愭湰"] ?? item.standardCost ?? 0);
+      return { month, category, costType, standardCost };
+    })
+    .filter((item) => item.month && item.category && item.costType && Number.isFinite(item.standardCost));
+
+  return normalized;
+};
+
+const replaceStandardSourceByImport = (importRows) => {
+  const map = new Map();
+  for (const item of importRows) {
+    const k = `${item.month}__${item.category}__${item.costType}`;
+    map.set(k, item);
+  }
+  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 openUploadSelector = () => {
+  const input = uploadRef.value?.$el?.querySelector?.("input[type='file']");
+  if (!input) {
+    ElMessage.warning("涓婁紶缁勪欢灏氭湭灏辩华锛岃绋嶅悗閲嶈瘯");
+    return;
+  }
+  input.click();
+};
+
+const handleImportCommand = (command) => {
+  if (command === "template") {
+    downloadTemplate();
+    return;
+  }
+  if (command === "upload") {
+    openUploadSelector();
+  }
+};
+
+const downloadTemplate = () => {
+  const sample = [
+    { 鏈堜唤: "2026-03", 浜у搧绫诲埆: "鐡风爾", 鎴愭湰绫诲瀷: "鏍囧噯鑳借�楁垚鏈�", 鏍囧噯鎴愭湰: 185000 },
+    { 鏈堜唤: "2026-03", 浜у搧绫诲埆: "鐡风爾", 鎴愭湰绫诲瀷: "鏍囧噯鐢熶骇鎴愭湰", 鏍囧噯鎴愭湰: 461000 },
+    { 鏈堜唤: "2026-03", 浜у搧绫诲埆: "姘存偿", 鎴愭湰绫诲瀷: "鏍囧噯鑳借�楁垚鏈�", 鏍囧噯鎴愭湰: 140000 },
+    { 鏈堜唤: "2026-03", 浜у搧绫诲埆: "姘存偿", 鎴愭湰绫诲瀷: "鏍囧噯鐢熶骇鎴愭湰", 鏍囧噯鎴愭湰: 405000 },
+  ];
+  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("妯℃澘宸蹭笅杞�");
+};
+
+const handleQuery = () => {
+  updateChart();
+};
+
+const handleReset = () => {
+  searchForm.monthRange = getDefaultMonthRange();
+  searchForm.category = "";
+  searchForm.costType = "";
+  page.current = 1;
+  handleQuery();
+};
+
+const handleSizeChange = (val) => {
+  page.size = val;
+  page.current = 1;
+};
+
+const handleCurrentChange = (val) => {
+  page.current = val;
+};
+
+const formatMoney = (v) => {
+  const n = Number.parseFloat(v);
+  const value = Number.isFinite(n) ? n : 0;
+  return value.toLocaleString("zh-CN", {
+    minimumFractionDigits: 2,
+    maximumFractionDigits: 2,
+  });
+};
+
+const formatSignedMoney = (v) => {
+  const n = Number.parseFloat(v);
+  const value = Number.isFinite(n) ? n : 0;
+  const sign = value >= 0 ? "+" : "";
+  return `${sign}楼${formatMoney(value)}`;
+};
+
+const formatPercent = (v) => {
+  const n = Number.parseFloat(v);
+  const value = Number.isFinite(n) ? n : 0;
+  const sign = value >= 0 ? "+" : "";
+  return `${sign}${value.toFixed(2)}%`;
+};
+
+const handleResize = () => {
+  chartInstance?.resize?.();
+  largeChartInstance?.resize?.();
+};
+
+const openLargeChart = () => {
+  if (!tableData.value.length) {
+    ElMessage.warning("鏆傛棤鍥捐〃鏁版嵁鍙煡鐪�");
+    return;
+  }
+  largeChartVisible.value = true;
+};
+
+const initLargeChart = () => {
+  nextTick(() => {
+    if (!largeChartRef.value) return;
+    if (!largeChartInstance) {
+      largeChartInstance = echarts.init(largeChartRef.value);
+    }
+    if (currentChartOption.value) {
+      largeChartInstance.setOption(currentChartOption.value);
+    } else {
+      updateChart();
+    }
+  });
+};
+
+const disposeLargeChart = () => {
+  largeChartInstance?.dispose?.();
+  largeChartInstance = null;
+};
+
+const downloadChartImage = () => {
+  const sourceChart = chartInstance || largeChartInstance;
+  if (!sourceChart) {
+    ElMessage.warning("鍥捐〃灏氭湭鍔犺浇瀹屾垚");
+    return;
+  }
+  const url = sourceChart.getDataURL({
+    type: "png",
+    pixelRatio: 2,
+    backgroundColor: "#ffffff",
+  });
+  const link = document.createElement("a");
+  link.href = url;
+  link.download = `鏍囧噯瀹為檯鎴愭湰瀵规瘮鍥綺${new Date().toISOString().slice(0, 10)}.png`;
+  document.body.appendChild(link);
+  link.click();
+  document.body.removeChild(link);
+  ElMessage.success("鍥捐〃涓嬭浇鎴愬姛");
+};
+
+onMounted(() => {
+  nextTick(() => {
+    if (chartRef.value && !chartInstance) {
+      chartInstance = echarts.init(chartRef.value);
+    }
+    updateChart();
+  });
+  window.addEventListener("resize", handleResize);
+});
+
+onUnmounted(() => {
+  window.removeEventListener("resize", handleResize);
+  chartInstance?.dispose?.();
+  chartInstance = null;
+  disposeLargeChart();
+});
+
+watch(tableData, () => {
+  const maxPage = Math.max(1, Math.ceil(tableData.value.length / page.size));
+  if (page.current > maxPage) page.current = maxPage;
+  nextTick(updateChart);
+});
+</script>
+
+<style scoped lang="scss">
+.std-cost-page {
+  --lux-bg: #f6f7fb;
+  --lux-card: rgba(255, 255, 255, 0.86);
+  --lux-border: rgba(15, 23, 42, 0.08);
+  --lux-text: rgba(15, 23, 42, 0.92);
+  --lux-subtle: rgba(15, 23, 42, 0.58);
+  --lux-primary: #2f6fed;
+  --lux-success: #16a34a;
+  --lux-warning: #f59e0b;
+  --lux-danger: #ef4444;
+  --lux-shadow-soft: 0 10px 28px rgba(15, 23, 42, 0.06);
+  --lux-radius: 14px;
+
+  padding: 18px 22px 24px;
+  background: radial-gradient(
+      1200px 420px at 20% 0%,
+      rgba(47, 111, 237, 0.1),
+      transparent 55%
+    ),
+    linear-gradient(180deg, var(--lux-bg) 0%, #ffffff 58%);
+}
+
+.filter-card,
+.panel-card,
+.table-card {
+  border-radius: var(--lux-radius);
+  border-color: var(--lux-border);
+  background: var(--lux-card);
+  box-shadow: var(--lux-shadow-soft);
+}
+
+.filter-card,
+.panel-card,
+.table-card {
+  margin-bottom: 14px;
+}
+
+.filter-layout {
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+}
+
+.filter-form {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px 14px;
+  align-items: center;
+}
+
+.filter-form :deep(.el-form-item) {
+  margin: 0;
+}
+
+.filter-actions {
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 10px 14px;
+  padding-top: 10px;
+  border-top: 1px dashed rgba(15, 23, 42, 0.1);
+}
+
+.action-group {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  flex-wrap: wrap;
+}
+
+.filter-actions :deep(.el-upload) {
+  display: inline-flex;
+}
+
+.hidden-upload {
+  width: 0;
+  height: 0;
+  overflow: hidden;
+}
+
+.card-head,
+.panel-head {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 10px;
+}
+
+.card-head-left {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.card-icon {
+  color: var(--lux-primary);
+}
+
+.card-title {
+  font-weight: 760;
+  color: var(--lux-text);
+}
+
+.subtle {
+  color: var(--lux-subtle);
+  font-size: 12px;
+}
+
+.kpi-strip {
+  display: grid;
+  grid-template-columns: repeat(4, minmax(0, 1fr));
+  gap: 12px;
+}
+
+.kpi-item {
+  padding: 12px 14px;
+  border-radius: 12px;
+  border: 1px solid rgba(15, 23, 42, 0.08);
+}
+
+.kpi-std {
+  background: linear-gradient(135deg, rgba(47, 111, 237, 0.1), rgba(255, 255, 255, 0.86));
+}
+
+.kpi-act {
+  background: linear-gradient(135deg, rgba(245, 158, 11, 0.1), rgba(255, 255, 255, 0.86));
+}
+
+.kpi-diff {
+  background: linear-gradient(135deg, rgba(239, 68, 68, 0.08), rgba(255, 255, 255, 0.86));
+}
+
+.kpi-rate {
+  background: linear-gradient(135deg, rgba(22, 163, 74, 0.1), rgba(255, 255, 255, 0.86));
+}
+
+.kpi-label {
+  font-size: 12px;
+  color: var(--lux-subtle);
+}
+
+.kpi-value {
+  margin-top: 6px;
+  font-size: 22px;
+  font-weight: 780;
+  color: var(--lux-text);
+}
+
+.cost-value {
+  color: var(--lux-danger);
+  font-weight: 700;
+}
+
+.ok-value {
+  color: var(--lux-success);
+  font-weight: 700;
+}
+
+.chart-wrap {
+  position: relative;
+  padding-top: 34px;
+  border-radius: 12px;
+  overflow: hidden;
+}
+
+.chart-content {
+  height: 360px;
+}
+
+.chart-tools {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.chart-tools-inline {
+  position: absolute;
+  top: 4px;
+  right: 6px;
+  z-index: 2;
+}
+
+.chart-tool {
+  font-size: 11px;
+  font-weight: 650;
+  line-height: 1;
+  padding: 6px 10px;
+  border-radius: 10px;
+  border: 1px solid rgba(15, 23, 42, 0.1);
+  background: rgba(255, 255, 255, 0.78);
+  color: rgba(15, 23, 42, 0.72);
+  cursor: pointer;
+  transition: background-color 0.16s ease, border-color 0.16s ease, transform 0.16s ease;
+}
+
+.chart-tool:hover {
+  background: rgba(47, 111, 237, 0.08);
+  border-color: rgba(47, 111, 237, 0.22);
+  transform: translateY(-1px);
+}
+
+.large-chart-content {
+  height: 70vh;
+  min-height: 520px;
+}
+
+.pagination-container {
+  display: flex;
+  justify-content: flex-end;
+  padding-top: 12px;
+}
+
+.w-260 {
+  width: 260px;
+}
+
+.w-180 {
+  width: 180px;
+}
+
+::deep(.lux-table) {
+  border-radius: 12px;
+  overflow: hidden;
+}
+
+::deep(.lux-table th.el-table__cell) {
+  background: rgba(15, 23, 42, 0.03);
+}
+
+::deep(.lux-table .el-table__row:hover > td.el-table__cell) {
+  background-color: rgba(47, 111, 237, 0.06) !important;
+}
+
+@media (max-width: 1100px) {
+  .kpi-strip {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
+
+  .filter-actions {
+    justify-content: flex-start;
+    padding-top: 8px;
+  }
+}
+</style>
\ No newline at end of file
diff --git a/src/views/productionManagement/productionOrder/index.vue b/src/views/productionManagement/productionOrder/index.vue
index 3116256..4760456 100644
--- a/src/views/productionManagement/productionOrder/index.vue
+++ b/src/views/productionManagement/productionOrder/index.vue
@@ -91,7 +91,7 @@
                        :status="toProgressPercentage(row?.completionStatus) >= 100 ? 'success' : ''" />
         </template>
         <template #quantity="{ row }">
-          {{ row.quantity || '-' }}<span style="color:rgb(63, 95, 211)"> 鍧�</span>
+          {{ row.quantity || '-' }}<span style="color:rgb(63, 95, 211)"> 鏂�</span>
         </template>
         <template #completeQuantity="{ row }">
           {{ row.completeQuantity || '-' }}<span style="color:rgb(42, 169, 146)"> 鏂�</span>
@@ -312,7 +312,7 @@
     {
       label: "鐢熶骇璁㈠崟鍙�",
       prop: "npsNo",
-      width: "120px",
+      width: "150px",
     },
     {
       label: "浜у搧鍚嶇О",

--
Gitblit v1.9.3