From 80ef3a1fb42f37cde2e9753317e118c50379b095 Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期四, 10 九月 2026 16:35:44 +0800
Subject: [PATCH] 天津-潜宇塑胶 1.设备、生产、BOM、质量、产品、仓储物流模块需求更改

---
 src/views/basicData/product/index.vue                                                |   18 
 src/api/inventoryManagement/stockCheck.js                                            |  112 +
 src/views/productionManagement/workOrder/index.vue                                   |   29 
 src/views/qualityManagement/rawMaterialInspection/index.vue                          |   22 
 src/views/procurementManagement/procurementDemand/index.vue                          |  308 +++
 src/views/productionManagement/productStructure/KitCheckDialog.vue                   |  216 ++
 src/views/qualityManagement/finalInspection/index.vue                                |   22 
 src/api/productionManagement/pieceRateConfig.js                                      |   57 
 src/views/productionManagement/costAccounting/index.vue                              |  371 ++++
 src/api/inventoryManagement/stockInventory.js                                        |   19 
 src/views/qualityManagement/processInspection/index.vue                              |   22 
 src/views/productionManagement/productStructure/index.vue                            |   70 
 src/views/productionManagement/workOrderManagement/index.vue                         |   26 
 src/api/productionManagement/productionStat.js                                       |   40 
 src/api/productionManagement/materialConsumption.js                                  |   21 
 src/views/productionManagement/pieceRateConfig/index.vue                             |  450 +++++
 src/views/qualityManagement/qualityTraceability/components/ReverseTable.vue          |  171 ++
 src/views/productionManagement/productionTraceability/index.vue                      |    3 
 src/views/inventoryManagement/stockManagement/index.vue                              |    5 
 src/views/productionManagement/materialConsumption/index.vue                         |  199 ++
 vite.config.js                                                                       |    2 
 src/api/productionManagement/productBom.js                                           |   37 
 src/views/qualityManagement/nonconformingManagement/components/inspectionFormDia.vue |   24 
 src/views/qualityManagement/qualityTraceability/components/ForwardResult.vue         |  432 +++++
 src/views/qualityManagement/processInspection/components/formDia.vue                 |    4 
 src/views/qualityManagement/finalInspection/components/formDia.vue                   |    4 
 src/views/qualityManagement/qualityTraceability/index.vue                            |  305 +++
 src/views/productionManagement/productionCosting/index.vue                           |   13 
 src/views/inventoryManagement/stockCheck/components/CheckDetail.vue                  |  383 ++++
 src/views/inventoryManagement/stockManagement/StockWarn.vue                          |  190 ++
 docs/.analysis/build-code-docx.cjs                                                   |   77 
 src/api/qualityManagement/qualityTrace.js                                            |   48 
 src/views/qualityManagement/nonconformingManagement/components/formDia.vue           |   32 
 src/views/productionManagement/productionReporting/index.vue                         |   29 
 src/views/productionManagement/outputStatistics/index.vue                            |  350 ++++
 src/views/inventoryManagement/stockCheck/index.vue                                   |  511 ++++++
 src/views/qualityManagement/rawMaterialInspection/components/formDia.vue             |    4 
 src/api/productionManagement/costAccounting.js                                       |   57 
 src/api/procurementManagement/procurementDemand.js                                   |   42 
 39 files changed, 4,706 insertions(+), 19 deletions(-)

diff --git a/docs/.analysis/build-code-docx.cjs b/docs/.analysis/build-code-docx.cjs
new file mode 100644
index 0000000..1adc90e
--- /dev/null
+++ b/docs/.analysis/build-code-docx.cjs
@@ -0,0 +1,77 @@
+const fs = require('fs');
+const path = require('path');
+const { Document, Packer, Paragraph, TextRun, HeadingLevel } = require('docx');
+
+const ROOT = path.join(__dirname, '..', '..');
+const OUT = path.join(ROOT, '绯荤粺妯″潡婧愪唬鐮乢绾〉闈�.docx');
+
+const viewDirs = [
+  'src/views/basicData',
+  'src/views/salesManagement',
+  'src/views/procurementManagement',
+  'src/views/personnelManagement',
+  'src/views/inventoryManagement',
+  'src/views/equipmentManagement',
+];
+
+const CODE_FONT = { ascii: 'Consolas', hAnsi: 'Consolas', eastAsia: '寰蒋闆呴粦' };
+const PATH_FONT = { ascii: 'Consolas', hAnsi: 'Consolas', eastAsia: '寰蒋闆呴粦' };
+
+function collectVue(relDir) {
+  const abs = path.join(ROOT, relDir);
+  const result = [];
+  (function walk(d) {
+    for (const e of fs.readdirSync(d, { withFileTypes: true })) {
+      const full = path.join(d, e.name);
+      if (e.isDirectory()) walk(full);
+      else if (e.name.endsWith('.vue')) result.push(full);
+    }
+  })(abs);
+  return result;
+}
+
+// 鍘绘帀 <style> ... </style> 鏍峰紡鍧�
+function stripStyle(code) {
+  return code.replace(/<style[\s\S]*?<\/style\s*>/g, '');
+}
+
+function codeParagraph(code) {
+  const lines = code.split(/\r?\n/);
+  while (lines.length && lines[lines.length - 1] === '') lines.pop();
+  const runs = lines.map((line, i) => {
+    const last = i === lines.length - 1;
+    return new TextRun({ text: line, font: CODE_FONT, size: 16, break: last ? undefined : 1 });
+  });
+  return new Paragraph({ children: runs, spacing: { before: 40, after: 200 } });
+}
+
+async function main() {
+  const files = [];
+  for (const d of viewDirs) {
+    for (const abs of collectVue(d)) {
+      files.push({ abs, rel: path.relative(ROOT, abs).replace(/\\/g, '/') });
+    }
+  }
+  files.sort((a, b) => a.rel.localeCompare(b.rel));
+
+  const children = [];
+  for (const f of files) {
+    children.push(new Paragraph({
+      heading: HeadingLevel.HEADING_1,
+      children: [new TextRun({ text: f.rel, font: PATH_FONT })],
+    }));
+    children.push(codeParagraph(stripStyle(fs.readFileSync(f.abs, 'utf8'))));
+  }
+
+  const doc = new Document({
+    styles: { default: { document: { run: { font: CODE_FONT, size: 16 } } } },
+    sections: [{ properties: {}, children }],
+  });
+  const buf = await Packer.toBuffer(doc);
+  fs.writeFileSync(OUT, buf);
+  console.log('宸茬敓鎴�:', OUT);
+  console.log('鏂囦欢鏁�:', files.length);
+  console.log('澶у皬:', (buf.length / 1024).toFixed(0), 'KB');
+}
+
+main().catch((e) => { console.error(e); process.exit(1); });
diff --git a/src/api/inventoryManagement/stockCheck.js b/src/api/inventoryManagement/stockCheck.js
new file mode 100644
index 0000000..e42617f
--- /dev/null
+++ b/src/api/inventoryManagement/stockCheck.js
@@ -0,0 +1,112 @@
+// 搴撳瓨鐩樼偣鎺ュ彛
+import request from "@/utils/request";
+
+// 鍙戣捣鐩樼偣
+export function start(data) {
+  return request({
+    url: "/stockCheck/start",
+    method: "post",
+    data: data,
+  });
+}
+
+// 鐩樼偣鍗曞垎椤�
+export function listPage(query) {
+  return request({
+    url: "/stockCheck/listPage",
+    method: "get",
+    params: query,
+  });
+}
+
+// 鐩樼偣鍗曡鎯�
+export function getById(id) {
+  return request({
+    url: "/stockCheck/" + id,
+    method: "get",
+  });
+}
+
+// 鐩樼偣鏄庣粏鍒嗛〉
+export function detailPage(query) {
+  return request({
+    url: "/stockCheck/detailPage",
+    method: "get",
+    params: query,
+  });
+}
+
+// 褰曞叆瀹炵洏
+export function entry(data) {
+  return request({
+    url: "/stockCheck/entry",
+    method: "put",
+    data: data,
+  });
+}
+
+// 瀹屾垚鐩樼偣
+export function finish(data) {
+  return request({
+    url: "/stockCheck/finish",
+    method: "post",
+    data: data,
+  });
+}
+
+// 鍙栨秷鐩樼偣
+export function cancel(data) {
+  return request({
+    url: "/stockCheck/cancel",
+    method: "post",
+    data: data,
+  });
+}
+
+// 鍒犻櫎鐩樼偣鍗曪紙body 涓� id 鏁扮粍锛屽崟涓垹闄や紶 [id]锛�
+export function remove(ids) {
+  return request({
+    url: "/stockCheck/delete",
+    method: "delete",
+    data: ids,
+  });
+}
+
+// 宸紓鍒嗘瀽鍒楄〃
+export function diffList(query) {
+  return request({
+    url: "/stockCheck/diffList",
+    method: "get",
+    params: query,
+  });
+}
+
+// 宸紓鍒嗘瀽瀵煎嚭
+export function diffListExport(query) {
+  return request({
+    url: "/stockCheck/diffList/export",
+    method: "get",
+    params: query,
+    responseType: "blob",
+  });
+}
+
+// 鐩樼偣鏄庣粏瀵煎嚭
+export function detailExport(query) {
+  return request({
+    url: "/stockCheck/detailExport",
+    method: "get",
+    params: query,
+    responseType: "blob",
+  });
+}
+
+// 鐩樼偣鍗曞鍑�
+export function exportList(query) {
+  return request({
+    url: "/stockCheck/export",
+    method: "get",
+    params: query,
+    responseType: "blob",
+  });
+}
diff --git a/src/api/inventoryManagement/stockInventory.js b/src/api/inventoryManagement/stockInventory.js
index 539eedc..ec28652 100644
--- a/src/api/inventoryManagement/stockInventory.js
+++ b/src/api/inventoryManagement/stockInventory.js
@@ -103,3 +103,22 @@
     });
 };
 
+// 搴撳瓨棰勮鍒嗛〉
+export const warnPage = (params) => {
+    return request({
+        url: "/stockInventory/warnPage",
+        method: "get",
+        params,
+    });
+};
+
+// 搴撳瓨棰勮瀵煎嚭
+export const warnExport = (params) => {
+    return request({
+        url: "/stockInventory/warnExport",
+        method: "get",
+        params,
+        responseType: "blob",
+    });
+};
+
diff --git a/src/api/procurementManagement/procurementDemand.js b/src/api/procurementManagement/procurementDemand.js
new file mode 100644
index 0000000..a30b953
--- /dev/null
+++ b/src/api/procurementManagement/procurementDemand.js
@@ -0,0 +1,42 @@
+// 閲囪喘闇�姹傛帴鍙�
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ
+export function listPage(query) {
+  return request({
+    url: "/procurementDemand/listPage",
+    method: "get",
+    params: query,
+  });
+}
+
+// 鎵归噺鐘舵�佸彉鏇�
+export function changeStatus(ids, status) {
+  return request({
+    url: "/procurementDemand/changeStatus",
+    method: "put",
+    params: {
+      ids: Array.isArray(ids) ? ids.join(",") : ids,
+      status,
+    },
+  });
+}
+
+// 鍒犻櫎
+export function del(ids) {
+  return request({
+    url: "/procurementDemand/del",
+    method: "delete",
+    data: ids,
+  });
+}
+
+// 瀵煎嚭
+export function exportDemand(query) {
+  return request({
+    url: "/procurementDemand/export",
+    method: "post",
+    params: query,
+    responseType: "blob",
+  });
+}
diff --git a/src/api/productionManagement/costAccounting.js b/src/api/productionManagement/costAccounting.js
new file mode 100644
index 0000000..53e6343
--- /dev/null
+++ b/src/api/productionManagement/costAccounting.js
@@ -0,0 +1,57 @@
+// 鎴愭湰鏍哥畻鍒嗘瀽鎺ュ彛
+import request from "@/utils/request";
+
+// 1. 鎴愭湰鏍哥畻鏄庣粏
+export function costStat(query) {
+  return request({
+    url: "/costAccounting/stat",
+    method: "get",
+    params: query,
+  });
+}
+
+// 2. 鎴愭湰鏋勬垚姹囨��
+export function costSummary(query) {
+  return request({
+    url: "/costAccounting/summary",
+    method: "get",
+    params: query,
+  });
+}
+
+// 3. 缂轰环鐗╂枡娓呭崟
+export function missingPrice(query) {
+  return request({
+    url: "/costAccounting/missingPrice",
+    method: "get",
+    params: query,
+  });
+}
+
+// 瀵煎嚭锛堝潎涓� GET + blob锛�
+export function costStatExport(query) {
+  return request({
+    url: "/costAccounting/stat/export",
+    method: "get",
+    params: query,
+    responseType: "blob",
+  });
+}
+
+export function costSummaryExport(query) {
+  return request({
+    url: "/costAccounting/summary/export",
+    method: "get",
+    params: query,
+    responseType: "blob",
+  });
+}
+
+export function missingPriceExport(query) {
+  return request({
+    url: "/costAccounting/missingPrice/export",
+    method: "get",
+    params: query,
+    responseType: "blob",
+  });
+}
diff --git a/src/api/productionManagement/materialConsumption.js b/src/api/productionManagement/materialConsumption.js
new file mode 100644
index 0000000..11eda91
--- /dev/null
+++ b/src/api/productionManagement/materialConsumption.js
@@ -0,0 +1,21 @@
+// 鏉愭枡娑堣�楁牳绠楁帴鍙�
+import request from "@/utils/request";
+
+// 鏍哥畻鍒楄〃
+export function stat(query) {
+  return request({
+    url: "/materialConsumption/stat",
+    method: "get",
+    params: query,
+  });
+}
+
+// 鏍哥畻瀵煎嚭
+export function statExport(query) {
+  return request({
+    url: "/materialConsumption/stat/export",
+    method: "get",
+    params: query,
+    responseType: "blob",
+  });
+}
diff --git a/src/api/productionManagement/pieceRateConfig.js b/src/api/productionManagement/pieceRateConfig.js
new file mode 100644
index 0000000..e19d9b3
--- /dev/null
+++ b/src/api/productionManagement/pieceRateConfig.js
@@ -0,0 +1,57 @@
+// 璁′欢鍗曚环閰嶇疆鎺ュ彛
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ
+export function listPage(query) {
+  return request({
+    url: "/pieceRateConfig/listPage",
+    method: "get",
+    params: query,
+  });
+}
+
+// 鏂板
+export function add(data) {
+  return request({
+    url: "/pieceRateConfig/add",
+    method: "post",
+    data: data,
+  });
+}
+
+// 缂栬緫
+export function update(data) {
+  return request({
+    url: "/pieceRateConfig/update",
+    method: "put",
+    data: data,
+  });
+}
+
+// 鍚敤/鍋滅敤
+export function changeStatus(data) {
+  return request({
+    url: "/pieceRateConfig/changeStatus",
+    method: "put",
+    data: data,
+  });
+}
+
+// 鎵归噺鍒犻櫎
+export function batchDelete(ids) {
+  return request({
+    url: "/pieceRateConfig/batchDelete",
+    method: "delete",
+    data: ids,
+  });
+}
+
+// 瀵煎嚭
+export function exportConfig(query) {
+  return request({
+    url: "/pieceRateConfig/export",
+    method: "get",
+    params: query,
+    responseType: "blob",
+  });
+}
diff --git a/src/api/productionManagement/productBom.js b/src/api/productionManagement/productBom.js
index 517208b..4dc53f7 100644
--- a/src/api/productionManagement/productBom.js
+++ b/src/api/productionManagement/productBom.js
@@ -72,3 +72,40 @@
     responseType: "blob",
   });
 }
+
+// BOM鍚敤/鍋滅敤
+export function changeStatus(data) {
+  return request({
+    url: "/technologyBom/changeStatus",
+    method: "put",
+    data: data,
+  });
+}
+
+// BOM榻愬鍒嗘瀽
+export function kitCheck(data) {
+  return request({
+    url: "/technologyBom/kitCheck",
+    method: "post",
+    data: data,
+  });
+}
+
+// 榻愬鍒嗘瀽瀵煎嚭
+export function kitCheckExport(data) {
+  return request({
+    url: "/technologyBom/kitCheck/export",
+    method: "post",
+    data: data,
+    responseType: "blob",
+  });
+}
+
+// 缂哄彛鐢熸垚閲囪喘闇�姹�
+export function generateDemand(data) {
+  return request({
+    url: "/technologyBom/kitCheck/generateDemand",
+    method: "post",
+    data: data,
+  });
+}
diff --git a/src/api/productionManagement/productionStat.js b/src/api/productionManagement/productionStat.js
new file mode 100644
index 0000000..8b0ea5f
--- /dev/null
+++ b/src/api/productionManagement/productionStat.js
@@ -0,0 +1,40 @@
+// 鐢熶骇缁熻鎺ュ彛锛堣浠跺伐璧勬眹鎬� + 浜ч噺缁熻锛�
+import request from "@/utils/request";
+
+// 璁′欢宸ヨ祫姹囨��
+export function wageSummary(query) {
+  return request({
+    url: "/productionProductMain/wageSummary",
+    method: "get",
+    params: query,
+  });
+}
+
+// 璁′欢宸ヨ祫姹囨�诲鍑�
+export function wageSummaryExport(query) {
+  return request({
+    url: "/productionProductMain/wageSummary/export",
+    method: "get",
+    params: query,
+    responseType: "blob",
+  });
+}
+
+// 浜ч噺缁熻
+export function outputStat(query) {
+  return request({
+    url: "/productionProductMain/outputStat",
+    method: "get",
+    params: query,
+  });
+}
+
+// 浜ч噺缁熻瀵煎嚭
+export function outputStatExport(query) {
+  return request({
+    url: "/productionProductMain/outputStat/export",
+    method: "get",
+    params: query,
+    responseType: "blob",
+  });
+}
diff --git a/src/api/qualityManagement/qualityTrace.js b/src/api/qualityManagement/qualityTrace.js
new file mode 100644
index 0000000..e0c606a
--- /dev/null
+++ b/src/api/qualityManagement/qualityTrace.js
@@ -0,0 +1,48 @@
+import request from '@/utils/request'
+
+// 姝e悜杩芥函锛氭垚鍝佹壒鍙� -> 鍏ㄩ摼璺�
+export function forwardTrace(batchNo) {
+    return request({
+        url: '/quality/trace/forward',
+        method: 'get',
+        params: { batchNo },
+    })
+}
+
+// 鍙嶅悜杩芥函锛氬師鏂欐壒鍙� -> 鍙楀奖鍝嶆垚鍝�
+export function reverseTrace(batchNo) {
+    return request({
+        url: '/quality/trace/reverse',
+        method: 'get',
+        params: { batchNo },
+    })
+}
+
+// 鎵瑰彿鍊欓�夛紙鎼滅储涓嬫媺锛夛紝type: finished | material
+export function batchOptions(type, keyword, limit = 50) {
+    return request({
+        url: '/quality/trace/batchOptions',
+        method: 'get',
+        params: { type, keyword, limit },
+    })
+}
+
+// 姝e悜杩芥函瀵煎嚭
+export function forwardExport(batchNo) {
+    return request({
+        url: '/quality/trace/forward/export',
+        method: 'get',
+        params: { batchNo },
+        responseType: 'blob',
+    })
+}
+
+// 鍙嶅悜杩芥函瀵煎嚭
+export function reverseExport(batchNo) {
+    return request({
+        url: '/quality/trace/reverse/export',
+        method: 'get',
+        params: { batchNo },
+        responseType: 'blob',
+    })
+}
diff --git a/src/views/basicData/product/index.vue b/src/views/basicData/product/index.vue
index b05b215..b16c1ad 100644
--- a/src/views/basicData/product/index.vue
+++ b/src/views/basicData/product/index.vue
@@ -164,6 +164,17 @@
             </el-form-item>
           </el-col>
         </el-row>
+        <el-row>
+          <el-col :span="24">
+            <el-form-item label="鏉″舰鐮侊細"
+                          prop="barcode">
+              <el-input v-model="modelForm.barcode"
+                        placeholder="璇疯緭鍏ユ潯褰㈢爜锛堥�夊~锛�"
+                        clearable
+                        @keydown.enter.prevent />
+            </el-form-item>
+          </el-col>
+        </el-row>
       </el-form>
       <template #footer>
         <div class="dialog-footer">
@@ -305,6 +316,11 @@
       prop: "unit",
     },
     {
+      label: "鏉″舰鐮�",
+      prop: "barcode",
+      minWidth: 140,
+    },
+    {
       dataType: "action",
       label: "鎿嶄綔",
       align: "center",
@@ -342,6 +358,7 @@
       model: "",
       unit: "",
       productCode: "",
+      barcode: "",
     },
     modelRules: {
       model: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
@@ -420,6 +437,7 @@
     modelForm.value.model = "";
     modelForm.value.unit = "";
     modelForm.value.productCode = "";
+    modelForm.value.barcode = "";
     modelForm.value.id = "";
     if (type === "edit") {
       modelForm.value = { ...data };
diff --git a/src/views/inventoryManagement/stockCheck/components/CheckDetail.vue b/src/views/inventoryManagement/stockCheck/components/CheckDetail.vue
new file mode 100644
index 0000000..95c4f9e
--- /dev/null
+++ b/src/views/inventoryManagement/stockCheck/components/CheckDetail.vue
@@ -0,0 +1,383 @@
+<template>
+  <div class="check-detail">
+    <div class="detail-header">
+      <span class="detail-title">鐩樼偣璇︽儏</span>
+      <el-button link
+                 type="primary"
+                 @click="handleBack">杩斿洖鍒楄〃</el-button>
+    </div>
+
+    <el-descriptions :column="3"
+                     border
+                     style="margin-bottom: 16px">
+      <el-descriptions-item label="鐩樼偣鍗曞彿">
+        {{ main.checkNo || "-" }}
+      </el-descriptions-item>
+      <el-descriptions-item label="鐩樼偣鍚嶇О">
+        {{ main.checkName || "-" }}
+      </el-descriptions-item>
+      <el-descriptions-item label="鐩樼偣鏃ユ湡">
+        {{ main.checkDate || "-" }}
+      </el-descriptions-item>
+      <el-descriptions-item label="鐩樼偣浜�">
+        {{ main.checker || "-" }}
+      </el-descriptions-item>
+      <el-descriptions-item label="鐘舵��">
+        <el-tag :type="statusTypeMap[mainStatus] || 'info'">
+          {{ statusTextMap[mainStatus] || "鏈煡" }}
+        </el-tag>
+      </el-descriptions-item>
+      <el-descriptions-item label="绉嶇被鏁�/宸紓鏁�">
+        {{ main.totalKinds ?? 0 }} / {{ main.diffKinds ?? 0 }}
+      </el-descriptions-item>
+      <el-descriptions-item label="澶囨敞"
+                            :span="3">
+        {{ main.remark || "-" }}
+      </el-descriptions-item>
+    </el-descriptions>
+
+    <div class="detail-toolbar">
+      <div>
+        <span style="margin-right: 8px">鍙湅宸紓</span>
+        <el-switch v-model="onlyDiff"
+                   @change="handleQuery" />
+      </div>
+      <div>
+        <el-button type="primary"
+                   :disabled="readOnly"
+                   @click="saveEntry">淇濆瓨褰曞叆</el-button>
+        <el-button type="success"
+                   :disabled="readOnly"
+                   @click="finishCheck">瀹屾垚鐩樼偣</el-button>
+        <el-button @click="openDiffDialog">宸紓鍒嗘瀽</el-button>
+        <el-button @click="handleExportDetail">瀵煎嚭鏄庣粏</el-button>
+      </div>
+    </div>
+
+    <PIMTable rowKey="id"
+              :column="detailColumn"
+              :tableData="tableData"
+              :page="page"
+              :tableLoading="tableLoading"
+              @pagination="pagination">
+      <template #actualQty="{ row }">
+        <el-input-number v-if="!readOnly"
+                         v-model="row.actualQty"
+                         :min="0"
+                         :precision="4"
+                         :controls="false"
+                         size="small"
+                         style="width: 100%" />
+        <span v-else>{{ row.actualQty ?? "-" }}</span>
+      </template>
+      <template #diffQty="{ row }">
+        <span :style="{ color: diffColor(row) }">{{ diffText(row) }}</span>
+      </template>
+      <template #adjusted="{ row }">
+        <el-tag :type="row.adjusted == 1 ? 'success' : 'info'">
+          {{ row.adjusted == 1 ? "宸茶皟璐�" : "鏈皟璐�" }}
+        </el-tag>
+      </template>
+      <template #remark="{ row }">
+        <el-input v-if="!readOnly"
+                  v-model="row.remark"
+                  size="small"
+                  placeholder="澶囨敞" />
+        <span v-else>{{ row.remark || "-" }}</span>
+      </template>
+    </PIMTable>
+
+    <el-dialog v-model="diffDialogVisible"
+               title="宸紓鍒嗘瀽"
+               width="90%">
+      <div style="text-align: right; margin-bottom: 10px">
+        <el-button type="warning"
+                   plain
+                   icon="Download"
+                   :loading="diffExportLoading"
+                   @click="handleExportDiff">瀵煎嚭宸紓</el-button>
+      </div>
+      <el-table :data="diffData"
+                border
+                height="420">
+        <el-table-column type="index"
+                         label="搴忓彿"
+                         width="60"
+                         align="center" />
+        <el-table-column label="浜у搧鍚嶇О"
+                         prop="productName"
+                         show-overflow-tooltip />
+        <el-table-column label="瑙勬牸鍨嬪彿"
+                         prop="productModel"
+                         show-overflow-tooltip />
+        <el-table-column label="鍗曚綅"
+                         prop="unit"
+                         width="80" />
+        <el-table-column label="鎵瑰彿"
+                         prop="batchNo"
+                         show-overflow-tooltip />
+        <el-table-column label="璐﹂潰鏁伴噺"
+                         prop="bookQty"
+                         width="120" />
+        <el-table-column label="瀹炵洏鏁伴噺"
+                         prop="actualQty"
+                         width="120" />
+        <el-table-column label="宸紓鏁伴噺"
+                         prop="diffQty"
+                         width="120" />
+        <el-table-column label="宸茶皟璐�"
+                         width="100"
+                         align="center">
+          <template #default="scope">
+            <el-tag :type="scope.row.adjusted == 1 ? 'success' : 'info'">
+              {{ scope.row.adjusted == 1 ? "宸茶皟璐�" : "鏈皟璐�" }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="澶囨敞"
+                         prop="remark"
+                         show-overflow-tooltip />
+      </el-table>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+  import { ref, reactive, computed, getCurrentInstance, onMounted } from "vue";
+  import { ElMessage, ElMessageBox } from "element-plus";
+  import { saveAs } from "file-saver";
+  import { blobValidate } from "@/utils/ruoyi";
+  import {
+    getById,
+    detailPage,
+    entry,
+    finish,
+    diffList,
+    diffListExport,
+    detailExport,
+  } from "@/api/inventoryManagement/stockCheck.js";
+
+  const props = defineProps({
+    mainId: {
+      type: [Number, String],
+      default: "",
+    },
+  });
+  const emit = defineEmits(["back"]);
+
+  const { proxy } = getCurrentInstance();
+
+  const statusTextMap = { 0: "鐩樼偣涓�", 1: "宸插畬鎴�", 2: "宸插彇娑�" };
+  const statusTypeMap = { 0: "warning", 1: "success", 2: "info" };
+
+  const main = ref({});
+  const onlyDiff = ref(false);
+
+  const mainStatus = computed(() => main.value.status ?? main.value.checkStatus);
+  const readOnly = computed(() => mainStatus.value != 0);
+
+  const detailColumn = ref([
+    { label: "浜у搧鍚嶇О", prop: "productName", minWidth: 140 },
+    { label: "瑙勬牸鍨嬪彿", prop: "productModel", minWidth: 140 },
+    { label: "鍗曚綅", prop: "unit", width: 80 },
+    { label: "鎵瑰彿", prop: "batchNo", minWidth: 120 },
+    { label: "璐﹂潰鏁伴噺", prop: "bookQty", width: 110 },
+    { label: "瀹炵洏鏁伴噺", prop: "actualQty", width: 140, dataType: "slot", slot: "actualQty" },
+    { label: "宸紓", prop: "diffQty", width: 110, dataType: "slot", slot: "diffQty" },
+    { label: "宸茶皟璐�", prop: "adjusted", width: 100, dataType: "slot", slot: "adjusted" },
+    { label: "澶囨敞", prop: "remark", minWidth: 140, dataType: "slot", slot: "remark" },
+  ]);
+
+  const tableData = ref([]);
+  const tableLoading = ref(false);
+  const page = reactive({
+    current: 1,
+    size: 10,
+    total: 0,
+  });
+
+  const diffDialogVisible = ref(false);
+  const diffData = ref([]);
+  const diffExportLoading = ref(false);
+
+  const diffColor = row => {
+    const diff = diffValue(row);
+    if (diff === null) return "";
+    if (diff > 0) return "#67C23A";
+    if (diff < 0) return "#F56C6C";
+    return "";
+  };
+
+  const diffValue = row => {
+    const actual = row.actualQty;
+    const book = row.bookQty;
+    if (actual === null || actual === undefined || actual === "") return null;
+    return Number(actual) - Number(book ?? 0);
+  };
+
+  const diffText = row => {
+    const diff = diffValue(row);
+    return diff === null ? "-" : diff;
+  };
+
+  const loadMain = () => {
+    if (!props.mainId) return;
+    getById(props.mainId).then(res => {
+      main.value = res?.data || {};
+    });
+  };
+
+  const loadDetail = () => {
+    tableLoading.value = true;
+    detailPage({
+      mainId: props.mainId,
+      onlyDiff: onlyDiff.value,
+      current: page.current,
+      size: page.size,
+    })
+      .then(res => {
+        tableData.value = res?.data?.records || [];
+        page.total = res?.data?.total || 0;
+      })
+      .catch(() => {})
+      .finally(() => {
+        tableLoading.value = false;
+      });
+  };
+
+  const handleQuery = () => {
+    page.current = 1;
+    loadDetail();
+  };
+
+  const pagination = obj => {
+    page.current = obj.page;
+    page.size = obj.limit;
+    loadDetail();
+  };
+
+  const saveEntry = () => {
+    const rows = tableData.value
+      .filter(item => item.actualQty !== null && item.actualQty !== undefined)
+      .map(item => ({
+        id: item.id,
+        actualQty: item.actualQty,
+        remark: item.remark,
+      }));
+    if (rows.length === 0) {
+      proxy.$modal.msgWarning("璇峰厛褰曞叆瀹炵洏鏁伴噺");
+      return;
+    }
+    entry(rows)
+      .then(() => {
+        proxy.$modal.msgSuccess("淇濆瓨鎴愬姛");
+        loadMain();
+        loadDetail();
+      })
+      .catch(() => {});
+  };
+
+  const finishCheck = () => {
+    ElMessageBox.confirm(
+      "宸紓灏嗙敓鎴愯皟璐﹀崟鎹紝瀹℃牳閫氳繃鍚庤处闈㈢敓鏁堬紝纭瀹屾垚鐩樼偣锛�",
+      "鎻愮ず",
+      { confirmButtonText: "纭", cancelButtonText: "鍙栨秷", type: "warning" }
+    )
+      .then(() => {
+        finish({ id: props.mainId })
+          .then(res => {
+            proxy.$modal.msgSuccess(res?.data || "鎿嶄綔鎴愬姛");
+            loadMain();
+            loadDetail();
+          })
+          .catch(() => {});
+      })
+      .catch(() => {});
+  };
+
+  const openDiffDialog = () => {
+    diffDialogVisible.value = true;
+    diffList({ mainId: props.mainId }).then(res => {
+      diffData.value = res?.data || [];
+    });
+  };
+
+  const exportBlob = async (requestFn, query, filename) => {
+    const blobData = await requestFn(query);
+    if (blobValidate(blobData)) {
+      if (blobData.size === 0) {
+        ElMessage.warning("鏆傛棤鏁版嵁鍙鍑�");
+        return;
+      }
+      const blob = new Blob([blobData], {
+        type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+      });
+      saveAs(blob, filename);
+      ElMessage.success("瀵煎嚭鎴愬姛");
+    } else {
+      const resText = await blobData.text();
+      let rspObj = {};
+      try {
+        rspObj = JSON.parse(resText);
+      } catch (error) {
+        rspObj = {};
+      }
+      ElMessage.error(rspObj.msg || "瀵煎嚭澶辫触");
+    }
+  };
+
+  const handleExportDetail = async () => {
+    try {
+      await exportBlob(
+        detailExport,
+        { mainId: props.mainId },
+        `鐩樼偣鏄庣粏_${main.value.checkNo || props.mainId}.xlsx`
+      );
+    } catch (error) {
+      ElMessage.error("瀵煎嚭澶辫触锛岃绋嶅悗閲嶈瘯");
+    }
+  };
+
+  const handleExportDiff = async () => {
+    diffExportLoading.value = true;
+    try {
+      await exportBlob(
+        diffListExport,
+        { mainId: props.mainId },
+        `鐩樼偣宸紓_${main.value.checkNo || props.mainId}.xlsx`
+      );
+    } catch (error) {
+      ElMessage.error("瀵煎嚭澶辫触锛岃绋嶅悗閲嶈瘯");
+    } finally {
+      diffExportLoading.value = false;
+    }
+  };
+
+  const handleBack = () => {
+    emit("back");
+  };
+
+  onMounted(() => {
+    loadMain();
+    loadDetail();
+  });
+</script>
+
+<style scoped lang="scss">
+  .detail-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-bottom: 12px;
+  }
+  .detail-title {
+    font-size: 16px;
+    font-weight: 600;
+  }
+  .detail-toolbar {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-bottom: 12px;
+  }
+</style>
diff --git a/src/views/inventoryManagement/stockCheck/index.vue b/src/views/inventoryManagement/stockCheck/index.vue
new file mode 100644
index 0000000..99c5401
--- /dev/null
+++ b/src/views/inventoryManagement/stockCheck/index.vue
@@ -0,0 +1,511 @@
+<template>
+  <div class="app-container">
+    <template v-if="viewMode === 'list'">
+      <PageHeader content="搴撳瓨鐩樼偣" />
+      <div class="search_form">
+        <el-form :model="queryParams"
+                 inline
+                 style="margin-bottom: 0;">
+          <el-form-item label="鐩樼偣鍗曞彿锛�">
+            <el-input v-model="queryParams.checkNo"
+                      placeholder="璇疯緭鍏�"
+                      clearable
+                      style="width: 200px"
+                      @keyup.enter="handleQuery" />
+          </el-form-item>
+          <el-form-item label="鐩樼偣鍚嶇О锛�">
+            <el-input v-model="queryParams.checkName"
+                      placeholder="璇疯緭鍏�"
+                      clearable
+                      style="width: 200px"
+                      @keyup.enter="handleQuery" />
+          </el-form-item>
+          <el-form-item label="鐘舵�侊細">
+            <el-select v-model="queryParams.status"
+                       placeholder="鍏ㄩ儴"
+                       clearable
+                       style="width: 160px"
+                       @change="handleQuery">
+              <el-option label="鐩樼偣涓�"
+                         :value="0" />
+              <el-option label="宸插畬鎴�"
+                         :value="1" />
+              <el-option label="宸插彇娑�"
+                         :value="2" />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="鐩樼偣鏃堕棿锛�">
+            <el-date-picker v-model="dateRange"
+                            type="datetimerange"
+                            range-separator="鑷�"
+                            start-placeholder="寮�濮嬫椂闂�"
+                            end-placeholder="缁撴潫鏃堕棿"
+                            value-format="YYYY-MM-DD HH:mm:ss"
+                            @change="handleQuery" />
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary"
+                       @click="handleQuery">鏌ヨ</el-button>
+            <el-button @click="resetQuery">閲嶇疆</el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+      <div class="mb20"
+           style="text-align: right;">
+        <el-button type="primary"
+                   @click="openStartDialog">鍙戣捣鐩樼偣</el-button>
+        <el-button type="danger"
+                   plain
+                   :disabled="selectedRows.length === 0"
+                   @click="handleBatchDelete">鎵归噺鍒犻櫎</el-button>
+        <el-button type="warning"
+                   plain
+                   icon="Download"
+                   :loading="exportLoading"
+                   @click="handleExport">瀵煎嚭</el-button>
+      </div>
+      <div class="table_list">
+        <PIMTable rowKey="id"
+                  :column="tableColumn"
+                  :tableData="tableData"
+                  :page="page"
+                  :isSelection="true"
+                  :tableLoading="tableLoading"
+                  @selection-change="handleSelectionChange"
+                  @pagination="pagination">
+          <template #status="{ row }">
+            <el-tag :type="statusTypeMap[getStatus(row)] || 'info'">
+              {{ statusTextMap[getStatus(row)] || "鏈煡" }}
+            </el-tag>
+          </template>
+        </PIMTable>
+      </div>
+    </template>
+
+    <CheckDetail v-else
+                 :main-id="currentId"
+                 @back="backToList" />
+
+    <el-dialog v-model="startDialogVisible"
+               title="鍙戣捣鐩樼偣"
+               width="520px">
+      <el-form ref="startFormRef"
+               :model="startForm"
+               :rules="startRules"
+               label-width="100px">
+        <el-form-item label="鐩樼偣鍚嶇О锛�"
+                      prop="checkName">
+          <el-input v-model="startForm.checkName"
+                    placeholder="璇疯緭鍏ョ洏鐐瑰悕绉�"
+                    clearable />
+        </el-form-item>
+        <el-form-item label="鐩樼偣鏃ユ湡锛�">
+          <el-date-picker v-model="startForm.checkDate"
+                          type="date"
+                          placeholder="璇烽�夋嫨鏃ユ湡"
+                          value-format="YYYY-MM-DD"
+                          style="width: 100%" />
+        </el-form-item>
+        <el-form-item label="鐩樼偣浜猴細">
+          <el-input v-model="startForm.checker"
+                    placeholder="璇疯緭鍏ョ洏鐐逛汉"
+                    clearable />
+        </el-form-item>
+        <el-form-item label="瑙勬牸鑼冨洿锛�">
+          <el-select v-model="startForm.productModelIds"
+                     multiple
+                     filterable
+                     clearable
+                     placeholder="鐣欑┖琛ㄧず鍏ㄩ儴搴撳瓨"
+                     style="width: 100%">
+            <el-option v-for="item in modelOptions"
+                       :key="item.id"
+                       :label="item.productName ? item.productName + ' ' + item.model : item.model"
+                       :value="item.id" />
+          </el-select>
+          <el-button link
+                     type="primary"
+                     @click="openProductSelect"
+                     style="margin-top: 6px">
+            閫夋嫨浜у搧瑙勬牸
+          </el-button>
+        </el-form-item>
+        <el-form-item label="澶囨敞锛�">
+          <el-input v-model="startForm.remark"
+                    type="textarea"
+                    :rows="2"
+                    placeholder="璇疯緭鍏ュ娉�"
+                    clearable />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary"
+                     @click="submitStart">纭</el-button>
+          <el-button @click="startDialogVisible = false">鍙栨秷</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <ProductSelectDialog v-model="productSelectVisible"
+                         @confirm="handleProductSelected" />
+  </div>
+</template>
+
+<script setup>
+  import { ref, reactive, getCurrentInstance, onMounted } from "vue";
+  import { ElMessage, ElMessageBox } from "element-plus";
+  import { saveAs } from "file-saver";
+  import { blobValidate } from "@/utils/ruoyi";
+  import {
+    start,
+    listPage,
+    finish,
+    cancel,
+    remove,
+    exportList,
+  } from "@/api/inventoryManagement/stockCheck.js";
+  import CheckDetail from "./components/CheckDetail.vue";
+  import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
+
+  const { proxy } = getCurrentInstance();
+
+  const statusTextMap = { 0: "鐩樼偣涓�", 1: "宸插畬鎴�", 2: "宸插彇娑�" };
+  const statusTypeMap = { 0: "warning", 1: "success", 2: "info" };
+
+  const getStatus = row => row.status ?? row.checkStatus;
+
+  const tableColumn = ref([
+    { label: "鐩樼偣鍗曞彿", prop: "checkNo", minWidth: 150 },
+    { label: "鐩樼偣鍚嶇О", prop: "checkName", minWidth: 140 },
+    { label: "鐩樼偣鏃ユ湡", prop: "checkDate", width: 120 },
+    { label: "鐩樼偣浜�", prop: "checker", width: 100 },
+    {
+      label: "鐘舵��",
+      prop: "status",
+      dataType: "slot",
+      slot: "status",
+      width: 100,
+    },
+    { label: "鐩樼偣绉嶇被鏁�", prop: "totalKinds", width: 110 },
+    { label: "宸紓绉嶇被鏁�", prop: "diffKinds", width: 110 },
+    { label: "澶囨敞", prop: "remark", minWidth: 140 },
+    { label: "鍒涘缓鏃堕棿", prop: "createTime", width: 170 },
+    {
+      dataType: "action",
+      label: "鎿嶄綔",
+      align: "center",
+      fixed: "right",
+      width: 300,
+      operation: [
+        {
+          name: "缁х画鐩樼偣",
+          type: "text",
+          showHide: row => getStatus(row) == 0,
+          clickFun: row => {
+            toDetail(row);
+          },
+        },
+        {
+          name: "瀹屾垚",
+          type: "text",
+          color: "#67C23A",
+          showHide: row => getStatus(row) == 0,
+          clickFun: row => {
+            handleFinish(row);
+          },
+        },
+        {
+          name: "鍙栨秷",
+          type: "text",
+          color: "#909399",
+          showHide: row => getStatus(row) == 0,
+          clickFun: row => {
+            handleCancel(row);
+          },
+        },
+        {
+          name: "璇︽儏",
+          type: "text",
+          showHide: row => getStatus(row) != 0,
+          clickFun: row => {
+            toDetail(row);
+          },
+        },
+        {
+          name: "鍒犻櫎",
+          type: "text",
+          clickFun: row => {
+            handleDelete(row);
+          },
+        },
+      ],
+    },
+  ]);
+
+  const tableData = ref([]);
+  const selectedRows = ref([]);
+  const tableLoading = ref(false);
+  const exportLoading = ref(false);
+  const page = reactive({
+    current: 1,
+    size: 10,
+    total: 0,
+  });
+  const queryParams = reactive({
+    checkNo: "",
+    checkName: "",
+    status: "",
+  });
+  const dateRange = ref([]);
+
+  const viewMode = ref("list");
+  const currentId = ref("");
+
+  const startDialogVisible = ref(false);
+  const startFormRef = ref(null);
+  const productSelectVisible = ref(false);
+  const modelOptions = ref([]);
+  const startForm = reactive({
+    checkName: "",
+    checkDate: "",
+    checker: "",
+    remark: "",
+    productModelIds: [],
+  });
+  const startRules = {
+    checkName: [{ required: true, message: "璇疯緭鍏ョ洏鐐瑰悕绉�", trigger: "blur" }],
+  };
+
+  const buildQuery = () => ({
+    checkNo: queryParams.checkNo || undefined,
+    checkName: queryParams.checkName || undefined,
+    status:
+      queryParams.status === "" ||
+      queryParams.status === null ||
+      queryParams.status === undefined
+        ? undefined
+        : queryParams.status,
+    beginTime: dateRange.value?.[0] || undefined,
+    endTime: dateRange.value?.[1] || undefined,
+  });
+
+  const handleSelectionChange = selection => {
+    selectedRows.value = selection;
+  };
+
+  const getList = () => {
+    tableLoading.value = true;
+    listPage({
+      pageNum: page.current,
+      pageSize: page.size,
+      ...buildQuery(),
+    })
+      .then(res => {
+        tableData.value = res?.data?.records || [];
+        selectedRows.value = [];
+        page.total = res?.data?.total || 0;
+      })
+      .catch(() => {})
+      .finally(() => {
+        tableLoading.value = false;
+      });
+  };
+
+  const handleQuery = () => {
+    page.current = 1;
+    getList();
+  };
+
+  const resetQuery = () => {
+    queryParams.checkNo = "";
+    queryParams.checkName = "";
+    queryParams.status = "";
+    dateRange.value = [];
+    handleQuery();
+  };
+
+  const pagination = obj => {
+    page.current = obj.page;
+    page.size = obj.limit;
+    getList();
+  };
+
+  const toDetail = row => {
+    currentId.value = row.id;
+    viewMode.value = "detail";
+  };
+
+  const backToList = () => {
+    viewMode.value = "list";
+    getList();
+  };
+
+  // 鍙戣捣鐩樼偣
+  const openStartDialog = () => {
+    startForm.checkName = "";
+    startForm.checkDate = "";
+    startForm.checker = "";
+    startForm.remark = "";
+    startForm.productModelIds = [];
+    modelOptions.value = [];
+    startDialogVisible.value = true;
+  };
+
+  const openProductSelect = () => {
+    productSelectVisible.value = true;
+  };
+
+  const handleProductSelected = rows => {
+    (rows || []).forEach(row => {
+      if (!startForm.productModelIds.includes(row.id) && !modelOptions.value.some(m => m.id === row.id)) {
+        modelOptions.value.push(row);
+      }
+    });
+    const ids = (rows || []).map(row => row.id);
+    ids.forEach(id => {
+      if (!startForm.productModelIds.includes(id)) {
+        startForm.productModelIds.push(id);
+      }
+    });
+  };
+
+  const submitStart = () => {
+    startFormRef.value?.validate(valid => {
+      if (!valid) {
+        return;
+      }
+      const submitData = {
+        checkName: startForm.checkName,
+        checkDate: startForm.checkDate || undefined,
+        checker: startForm.checker || undefined,
+        remark: startForm.remark || undefined,
+        productModelIds:
+          startForm.productModelIds.length > 0
+            ? startForm.productModelIds
+            : undefined,
+      };
+      start(submitData)
+        .then(() => {
+          proxy.$modal.msgSuccess("鍙戣捣鎴愬姛");
+          startDialogVisible.value = false;
+          getList();
+        })
+        .catch(() => {});
+    });
+  };
+
+  // 瀹屾垚
+  const handleFinish = row => {
+    ElMessageBox.confirm(
+      "宸紓灏嗙敓鎴愯皟璐﹀崟鎹紝瀹℃牳閫氳繃鍚庤处闈㈢敓鏁堬紝纭瀹屾垚鐩樼偣锛�",
+      "鎻愮ず",
+      { confirmButtonText: "纭", cancelButtonText: "鍙栨秷", type: "warning" }
+    )
+      .then(() => {
+        finish({ id: row.id })
+          .then(res => {
+            proxy.$modal.msgSuccess(res?.data || "鎿嶄綔鎴愬姛");
+            getList();
+          })
+          .catch(() => {});
+      })
+      .catch(() => {});
+  };
+
+  // 鍙栨秷
+  const handleCancel = row => {
+    ElMessageBox.confirm("纭鍙栨秷璇ョ洏鐐瑰崟锛�", "鎻愮ず", {
+      confirmButtonText: "纭",
+      cancelButtonText: "鍙栨秷",
+      type: "warning",
+    })
+      .then(() => {
+        cancel({ id: row.id })
+          .then(() => {
+            proxy.$modal.msgSuccess("鎿嶄綔鎴愬姛");
+            getList();
+          })
+          .catch(() => {});
+      })
+      .catch(() => {});
+  };
+
+  // 鍒犻櫎
+  const handleDelete = row => {
+    ElMessageBox.confirm(
+      `纭鍒犻櫎鐩樼偣鍗曘��${row.checkNo || row.checkName || ""}銆嶏紵`,
+      "鎻愮ず",
+      { confirmButtonText: "纭", cancelButtonText: "鍙栨秷", type: "warning" }
+    )
+      .then(() => {
+        remove([row.id])
+          .then(() => {
+            proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+            getList();
+          })
+          .catch(() => {});
+      })
+      .catch(() => {});
+  };
+
+  // 鎵归噺鍒犻櫎
+  const handleBatchDelete = () => {
+    if (selectedRows.value.length === 0) {
+      proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+      return;
+    }
+    ElMessageBox.confirm(
+      `閫変腑鐨� ${selectedRows.value.length} 鏉$洏鐐瑰崟灏嗚鍒犻櫎锛屾槸鍚︾‘璁ゅ垹闄わ紵`,
+      "鍒犻櫎鎻愮ず",
+      { confirmButtonText: "纭", cancelButtonText: "鍙栨秷", type: "warning" }
+    )
+      .then(() => {
+        remove(selectedRows.value.map(item => item.id))
+          .then(() => {
+            proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+            getList();
+          })
+          .catch(() => {});
+      })
+      .catch(() => {});
+  };
+
+  // 瀵煎嚭
+  const handleExport = async () => {
+    exportLoading.value = true;
+    try {
+      const blobData = await exportList(buildQuery());
+      if (blobValidate(blobData)) {
+        if (blobData.size === 0) {
+          ElMessage.warning("鏆傛棤鏁版嵁鍙鍑�");
+          return;
+        }
+        const blob = new Blob([blobData], {
+          type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+        });
+        saveAs(blob, "搴撳瓨鐩樼偣.xlsx");
+        ElMessage.success("瀵煎嚭鎴愬姛");
+      } else {
+        const resText = await blobData.text();
+        let rspObj = {};
+        try {
+          rspObj = JSON.parse(resText);
+        } catch (error) {
+          rspObj = {};
+        }
+        ElMessage.error(rspObj.msg || "瀵煎嚭澶辫触");
+      }
+    } catch (error) {
+      ElMessage.error("瀵煎嚭澶辫触锛岃绋嶅悗閲嶈瘯");
+    } finally {
+      exportLoading.value = false;
+    }
+  };
+
+  onMounted(() => {
+    getList();
+  });
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/views/inventoryManagement/stockManagement/StockWarn.vue b/src/views/inventoryManagement/stockManagement/StockWarn.vue
new file mode 100644
index 0000000..17fd68a
--- /dev/null
+++ b/src/views/inventoryManagement/stockManagement/StockWarn.vue
@@ -0,0 +1,190 @@
+<template>
+  <div class="stock-warn">
+    <div class="search_form">
+      <el-form :model="searchForm"
+               inline
+               style="margin-bottom: 0;">
+        <el-form-item label="浜у搧澶х被锛�">
+          <el-input v-model="searchForm.productName"
+                    placeholder="璇疯緭鍏�"
+                    clearable
+                    style="width: 240px"
+                    @keyup.enter="handleQuery" />
+        </el-form-item>
+        <el-form-item label="瑙勬牸鍨嬪彿锛�">
+          <el-input v-model="searchForm.model"
+                    placeholder="璇疯緭鍏�"
+                    clearable
+                    style="width: 200px"
+                    @keyup.enter="handleQuery" />
+        </el-form-item>
+        <el-form-item label="鎵瑰彿锛�">
+          <el-input v-model="searchForm.batchNo"
+                    placeholder="璇疯緭鍏�"
+                    clearable
+                    style="width: 200px"
+                    @keyup.enter="handleQuery" />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary"
+                     @click="handleQuery">鏌ヨ</el-button>
+          <el-button @click="resetQuery">閲嶇疆</el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+    <div class="mb20"
+         style="text-align: right;">
+      <el-button type="warning"
+                 plain
+                 icon="Download"
+                 :loading="exportLoading"
+                 @click="handleExport">瀵煎嚭</el-button>
+    </div>
+    <div class="table_list">
+      <PIMTable rowKey="id"
+                :column="tableColumn"
+                :tableData="tableData"
+                :page="page"
+                :tableLoading="tableLoading"
+                :rowClassName="tableRowClassName"
+                @pagination="pagination" />
+    </div>
+  </div>
+</template>
+
+<script setup>
+  import { ref, reactive, onMounted } from "vue";
+  import { ElMessage } from "element-plus";
+  import { saveAs } from "file-saver";
+  import { blobValidate } from "@/utils/ruoyi";
+  import { warnPage, warnExport } from "@/api/inventoryManagement/stockInventory.js";
+
+  const tableColumn = ref([
+    { label: "浜у搧澶х被", prop: "productName", minWidth: 140 },
+    { label: "瑙勬牸鍨嬪彿", prop: "model", minWidth: 140 },
+    { label: "鎵瑰彿", prop: "batchNo", minWidth: 120 },
+    { label: "鍗曚綅", prop: "unit", width: 80 },
+    { label: "搴撳瓨鏁伴噺", prop: "qualitity", width: 110 },
+    { label: "鍐荤粨鏁伴噺", prop: "lockedQuantity", width: 110 },
+    { label: "鍙敤鏁伴噺", prop: "unLockedQuantity", width: 110 },
+    { label: "棰勮鍊�", prop: "warnNum", width: 100 },
+    { label: "澶囨敞", prop: "remark", minWidth: 140 },
+    { label: "鏈�杩戞洿鏂版椂闂�", prop: "updateTime", width: 170 },
+  ]);
+
+  const tableData = ref([]);
+  const tableLoading = ref(false);
+  const exportLoading = ref(false);
+  const page = reactive({
+    current: 1,
+    size: 10,
+    total: 0,
+  });
+  const searchForm = reactive({
+    productName: "",
+    model: "",
+    batchNo: "",
+  });
+
+  const getUnlocked = row => row?.unLockedQuantity ?? row?.unLocked_quantity ?? 0;
+
+  const buildQuery = () => ({
+    productName: searchForm.productName || undefined,
+    model: searchForm.model || undefined,
+    batchNo: searchForm.batchNo || undefined,
+  });
+
+  const getList = () => {
+    tableLoading.value = true;
+    warnPage({
+      // 鍏煎 current/size 涓� pageNum/pageSize 涓ょ鍒嗛〉鍙傛暟
+      current: page.current,
+      size: page.size,
+      pageNum: page.current,
+      pageSize: page.size,
+      ...buildQuery(),
+    })
+      .then(res => {
+        tableData.value = res?.data?.records || [];
+        page.total = res?.data?.total || 0;
+      })
+      .catch(() => {})
+      .finally(() => {
+        tableLoading.value = false;
+      });
+  };
+
+  const handleQuery = () => {
+    page.current = 1;
+    getList();
+  };
+
+  const resetQuery = () => {
+    searchForm.productName = "";
+    searchForm.model = "";
+    searchForm.batchNo = "";
+    handleQuery();
+  };
+
+  const pagination = obj => {
+    page.current = obj.page;
+    page.size = obj.limit;
+    getList();
+  };
+
+  // 鍙敤鏁伴噺 <= 棰勮鍊� 涓� 棰勮鍊� > 0 鏃堕珮浜�
+  const tableRowClassName = ({ row }) => {
+    const unlocked = Number(getUnlocked(row));
+    const warn = Number(row?.warnNum ?? 0);
+    if (!Number.isFinite(unlocked) || !Number.isFinite(warn)) {
+      return "";
+    }
+    return warn > 0 && unlocked <= warn ? "row-low-stock" : "";
+  };
+
+  const handleExport = async () => {
+    exportLoading.value = true;
+    try {
+      const blobData = await warnExport(buildQuery());
+      if (blobValidate(blobData)) {
+        if (blobData.size === 0) {
+          ElMessage.warning("鏆傛棤鏁版嵁鍙鍑�");
+          return;
+        }
+        const blob = new Blob([blobData], {
+          type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+        });
+        saveAs(blob, "搴撳瓨棰勮.xlsx");
+        ElMessage.success("瀵煎嚭鎴愬姛");
+      } else {
+        const resText = await blobData.text();
+        let rspObj = {};
+        try {
+          rspObj = JSON.parse(resText);
+        } catch (error) {
+          rspObj = {};
+        }
+        ElMessage.error(rspObj.msg || "瀵煎嚭澶辫触");
+      }
+    } catch (error) {
+      ElMessage.error("瀵煎嚭澶辫触锛岃绋嶅悗閲嶈瘯");
+    } finally {
+      exportLoading.value = false;
+    }
+  };
+
+  onMounted(() => {
+    getList();
+  });
+</script>
+
+<style scoped lang="scss">
+  :deep(.row-low-stock td) {
+    background-color: #fde2e2;
+    color: #c45656;
+  }
+
+  :deep(.row-low-stock:hover > td) {
+    background-color: #fcd4d4;
+  }
+</style>
diff --git a/src/views/inventoryManagement/stockManagement/index.vue b/src/views/inventoryManagement/stockManagement/index.vue
index b3aa7ee..6a17fdb 100644
--- a/src/views/inventoryManagement/stockManagement/index.vue
+++ b/src/views/inventoryManagement/stockManagement/index.vue
@@ -8,6 +8,10 @@
                      :key="tab.id">
           <Record :product-id="tab.id" v-if="tab.id === activeTab" />
         </el-tab-pane>
+        <el-tab-pane label="搴撳瓨棰勮"
+                     name="__warn__">
+          <StockWarn v-if="activeTab === '__warn__'" />
+        </el-tab-pane>
       </el-tabs>
     </div>
   </div>
@@ -17,6 +21,7 @@
 import { ref, onMounted } from 'vue';
 import { productTreeList } from "@/api/basicData/product.js";
 import Record from "@/views/inventoryManagement/stockManagement/Record.vue";
+import StockWarn from "@/views/inventoryManagement/stockManagement/StockWarn.vue";
 const products = ref([])
 const activeTab = ref(null)
 const loading = ref(false)
diff --git a/src/views/procurementManagement/procurementDemand/index.vue b/src/views/procurementManagement/procurementDemand/index.vue
new file mode 100644
index 0000000..e43475f
--- /dev/null
+++ b/src/views/procurementManagement/procurementDemand/index.vue
@@ -0,0 +1,308 @@
+<template>
+  <div class="app-container">
+    <PageHeader content="閲囪喘闇�姹�" />
+    <div class="search_form">
+      <el-form :model="queryParams"
+               inline
+               style="margin-bottom: 0;">
+        <el-form-item label="浜у搧鍚嶇О锛�">
+          <el-input v-model="queryParams.productName"
+                    placeholder="璇疯緭鍏ヤ骇鍝佸悕绉�"
+                    clearable
+                    style="width: 200px"
+                    @keyup.enter="handleQuery" />
+        </el-form-item>
+        <el-form-item label="瑙勬牸鍨嬪彿锛�">
+          <el-input v-model="queryParams.productModel"
+                    placeholder="璇疯緭鍏ヨ鏍煎瀷鍙�"
+                    clearable
+                    style="width: 200px"
+                    @keyup.enter="handleQuery" />
+        </el-form-item>
+        <el-form-item label="鐘舵�侊細">
+          <el-select v-model="queryParams.status"
+                     placeholder="鍏ㄩ儴"
+                     clearable
+                     style="width: 160px"
+                     @change="handleQuery">
+            <el-option label="寰呭鐞�"
+                       :value="0" />
+            <el-option label="宸茶浆閲囪喘"
+                       :value="1" />
+            <el-option label="宸插彇娑�"
+                       :value="2" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary"
+                     @click="handleQuery">鏌ヨ</el-button>
+          <el-button @click="resetQuery">閲嶇疆</el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+    <div class="mb20"
+         style="text-align: right;">
+      <el-button type="warning"
+                 plain
+                 icon="Download"
+                 :loading="exportLoading"
+                 @click="handleExport">瀵煎嚭</el-button>
+    </div>
+    <div class="table_list">
+      <PIMTable rowKey="id"
+                :column="tableColumn"
+                :tableData="tableData"
+                :page="page"
+                :isSelection="true"
+                @selection-change="handleSelectionChange"
+                :tableLoading="tableLoading"
+                @pagination="pagination" />
+    </div>
+  </div>
+</template>
+
+<script setup>
+  import { ref, reactive, getCurrentInstance, onMounted } from "vue";
+  import { ElMessage, ElMessageBox } from "element-plus";
+  import { saveAs } from "file-saver";
+  import { blobValidate } from "@/utils/ruoyi";
+  import {
+    listPage,
+    changeStatus,
+    del,
+    exportDemand,
+  } from "@/api/procurementManagement/procurementDemand.js";
+
+  const { proxy } = getCurrentInstance();
+
+  const statusTextMap = { 0: "寰呭鐞�", 1: "宸茶浆閲囪喘", 2: "宸插彇娑�" };
+  const statusTypeMap = { 0: "warning", 1: "success", 2: "info" };
+
+  const tableColumn = ref([
+    {
+      label: "浜у搧鍚嶇О",
+      prop: "productName",
+      minWidth: 140,
+    },
+    {
+      label: "瑙勬牸鍨嬪彿",
+      prop: "productModel",
+      minWidth: 140,
+    },
+    {
+      label: "鍗曚綅",
+      prop: "unit",
+      width: 80,
+    },
+    {
+      label: "缂哄彛闇�姹傞噺",
+      prop: "demandQuantity",
+      width: 120,
+    },
+    {
+      label: "鐢熸垚鏃跺簱瀛�",
+      prop: "stockQuantity",
+      width: 120,
+    },
+    {
+      label: "鏉ユ簮BOM缂栧彿",
+      prop: "sourceBomNo",
+      minWidth: 160,
+    },
+    {
+      label: "鐘舵��",
+      prop: "status",
+      dataType: "tag",
+      width: 100,
+      formatData: v => statusTextMap[v] || "寰呭鐞�",
+      formatType: v => statusTypeMap[v] || "warning",
+    },
+    {
+      label: "澶囨敞",
+      prop: "remark",
+      minWidth: 140,
+    },
+    {
+      label: "鍒涘缓鏃堕棿",
+      prop: "createTime",
+      width: 170,
+    },
+    {
+      dataType: "action",
+      label: "鎿嶄綔",
+      align: "center",
+      fixed: "right",
+      width: 240,
+      operation: [
+        {
+          name: "鏍囪宸茶浆閲囪喘",
+          type: "text",
+          color: "#67C23A",
+          showHide: row => row.status == 0,
+          clickFun: row => {
+            handleChangeStatus(row, 1);
+          },
+        },
+        {
+          name: "鍙栨秷",
+          type: "text",
+          color: "#E6A23C",
+          showHide: row => row.status == 0,
+          clickFun: row => {
+            handleChangeStatus(row, 2);
+          },
+        },
+        {
+          name: "鍒犻櫎",
+          type: "danger",
+          link: true,
+          showHide: row => row.status == 0,
+          clickFun: row => {
+            handleDelete(row);
+          },
+        },
+      ],
+    },
+  ]);
+
+  const tableData = ref([]);
+  const selectedRows = ref([]);
+  const tableLoading = ref(false);
+  const exportLoading = ref(false);
+  const page = reactive({
+    current: 1,
+    size: 10,
+    total: 0,
+  });
+  const queryParams = reactive({
+    productName: "",
+    productModel: "",
+    status: "",
+  });
+
+  const buildQuery = () => ({
+    productName: queryParams.productName || undefined,
+    productModel: queryParams.productModel || undefined,
+    status:
+      queryParams.status === "" ||
+      queryParams.status === null ||
+      queryParams.status === undefined
+        ? undefined
+        : queryParams.status,
+  });
+
+  // 鏌ヨ鍒楄〃
+  const getList = () => {
+    tableLoading.value = true;
+    listPage({
+      pageNum: page.current,
+      pageSize: page.size,
+      ...buildQuery(),
+    })
+      .then(res => {
+        tableData.value = res?.data?.records || [];
+        page.total = res?.data?.total || 0;
+      })
+      .catch(() => {})
+      .finally(() => {
+        tableLoading.value = false;
+      });
+  };
+
+  const handleQuery = () => {
+    page.current = 1;
+    getList();
+  };
+
+  const resetQuery = () => {
+    queryParams.productName = "";
+    queryParams.productModel = "";
+    queryParams.status = "";
+    handleQuery();
+  };
+
+  const pagination = obj => {
+    page.current = obj.page;
+    page.size = obj.limit;
+    getList();
+  };
+
+  const handleSelectionChange = selection => {
+    selectedRows.value = selection;
+  };
+
+  // 鐘舵�佸彉鏇�
+  const handleChangeStatus = (row, status) => {
+    const text = status == 1 ? "鏍囪涓哄凡杞噰璐�" : "鍙栨秷璇ラ噰璐渶姹�";
+    ElMessageBox.confirm(`纭${text}锛焋, "鎻愮ず", {
+      confirmButtonText: "纭",
+      cancelButtonText: "鍙栨秷",
+      type: "warning",
+    })
+      .then(() => {
+        changeStatus([row.id], status)
+          .then(() => {
+            proxy.$modal.msgSuccess("鎿嶄綔鎴愬姛");
+            getList();
+          })
+          .catch(() => {});
+      })
+      .catch(() => {});
+  };
+
+  // 鍒犻櫎
+  const handleDelete = row => {
+    ElMessageBox.confirm("纭鍒犻櫎璇ラ噰璐渶姹傦紵浠呭緟澶勭悊鏁版嵁鍙垹闄ゃ��", "鍒犻櫎鎻愮ず", {
+      confirmButtonText: "纭",
+      cancelButtonText: "鍙栨秷",
+      type: "warning",
+    })
+      .then(() => {
+        del([row.id])
+          .then(() => {
+            proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+            getList();
+          })
+          .catch(() => {});
+      })
+      .catch(() => {});
+  };
+
+  // 瀵煎嚭
+  const handleExport = async () => {
+    exportLoading.value = true;
+    try {
+      const blobData = await exportDemand(buildQuery());
+      if (blobValidate(blobData)) {
+        if (blobData.size === 0) {
+          ElMessage.warning("鏆傛棤鏁版嵁鍙鍑�");
+          return;
+        }
+        const blob = new Blob([blobData], {
+          type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+        });
+        saveAs(blob, "閲囪喘闇�姹�.xlsx");
+        ElMessage.success("瀵煎嚭鎴愬姛");
+      } else {
+        const resText = await blobData.text();
+        let rspObj = {};
+        try {
+          rspObj = JSON.parse(resText);
+        } catch (error) {
+          rspObj = {};
+        }
+        ElMessage.error(rspObj.msg || "瀵煎嚭澶辫触");
+      }
+    } catch (error) {
+      ElMessage.error("瀵煎嚭澶辫触锛岃绋嶅悗閲嶈瘯");
+    } finally {
+      exportLoading.value = false;
+    }
+  };
+
+  onMounted(() => {
+    getList();
+  });
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/views/productionManagement/costAccounting/index.vue b/src/views/productionManagement/costAccounting/index.vue
new file mode 100644
index 0000000..3e43100
--- /dev/null
+++ b/src/views/productionManagement/costAccounting/index.vue
@@ -0,0 +1,371 @@
+<template>
+  <div class="app-container">
+    <PageHeader content="鎴愭湰鏍哥畻鍒嗘瀽" />
+    <div class="search_form">
+      <el-form :model="queryParams"
+               inline
+               style="margin-bottom: 0;">
+        <el-form-item label="璁㈠崟鏃ユ湡锛�">
+          <el-date-picker v-model="dateRange"
+                          type="daterange"
+                          range-separator="鑷�"
+                          start-placeholder="寮�濮嬫棩鏈�"
+                          end-placeholder="缁撴潫鏃ユ湡"
+                          value-format="YYYY-MM-DD"
+                          @change="handleQuery" />
+        </el-form-item>
+        <el-form-item label="鐢熶骇璁㈠崟鍙凤細">
+          <el-input v-model="queryParams.npsNo"
+                    placeholder="璇疯緭鍏�"
+                    clearable
+                    style="width: 200px"
+                    @keyup.enter="handleQuery" />
+        </el-form-item>
+        <el-form-item label="鎴愬搧锛�">
+          <el-input v-model="queryParams.productName"
+                    placeholder="璇疯緭鍏�"
+                    clearable
+                    style="width: 200px"
+                    @keyup.enter="handleQuery" />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary"
+                     @click="handleQuery">鏌ヨ</el-button>
+          <el-button @click="resetQuery">閲嶇疆</el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+
+    <el-tabs v-model="activeTab"
+             @tab-change="handleTabChange">
+      <!-- 鏍哥畻鏄庣粏 -->
+      <el-tab-pane label="鏍哥畻鏄庣粏"
+                   name="stat">
+        <div class="mb20"
+             style="text-align: right;">
+          <el-button type="warning"
+                     plain
+                     icon="Download"
+                     :loading="statExportLoading"
+                     @click="handleStatExport">瀵煎嚭</el-button>
+        </div>
+        <PIMTable rowKey="productionOrderId"
+                  :column="statColumn"
+                  :tableData="statData"
+                  :isShowPagination="false"
+                  :tableLoading="statLoading">
+          <template #laborCost="{ row }">
+            <el-tooltip v-if="row.noRateRows > 0"
+                        content="瀛樺湪鏃犲伐璧勬爣鍑嗙殑鎶ュ伐璁板綍锛屼汉宸ユ垚鏈彲鑳戒笉鍑嗙‘锛岃琛ュ綍宸ユ椂鎴栫淮鎶よ浠跺崟浠�"
+                        placement="top">
+              <span style="color: #e6a23c">{{ formatNum(row.laborCost) }}</span>
+            </el-tooltip>
+            <span v-else>{{ formatNum(row.laborCost) }}</span>
+          </template>
+          <template #grossProfitRate="{ row }">
+            <span :style="{ color: isNegative(row.grossProfitRate) ? '#f56c6c' : '' }">
+              {{ formatRate(row.grossProfitRate) }}
+            </span>
+          </template>
+          <template #missingPriceKinds="{ row }">
+            <span :style="{ color: row.missingPriceKinds > 0 ? '#e6a23c' : '' }">
+              {{ row.missingPriceKinds ?? 0 }}
+            </span>
+          </template>
+        </PIMTable>
+      </el-tab-pane>
+
+      <!-- 鏋勬垚姹囨�� -->
+      <el-tab-pane label="鏋勬垚姹囨��"
+                   name="summary">
+        <div class="mb20"
+             style="display: flex; align-items: center; justify-content: space-between;">
+          <el-radio-group v-model="groupBy"
+                          @change="getSummary">
+            <el-radio label="month">鎸夋湀浠�</el-radio>
+            <el-radio label="product">鎸夋垚鍝�</el-radio>
+          </el-radio-group>
+          <el-button type="warning"
+                     plain
+                     icon="Download"
+                     :loading="summaryExportLoading"
+                     @click="handleSummaryExport">瀵煎嚭</el-button>
+        </div>
+        <PIMTable rowKey="groupKey"
+                  :column="summaryColumn"
+                  :tableData="summaryData"
+                  :isShowPagination="false"
+                  :tableLoading="summaryLoading" />
+      </el-tab-pane>
+
+      <!-- 缂轰环鐗╂枡 -->
+      <el-tab-pane label="缂轰环鐗╂枡"
+                   name="missing">
+        <div class="mb20"
+             style="text-align: right;">
+          <el-button type="warning"
+                     plain
+                     icon="Download"
+                     :loading="missingExportLoading"
+                     @click="handleMissingExport">瀵煎嚭</el-button>
+        </div>
+        <PIMTable rowKey="productModelId"
+                  :column="missingColumn"
+                  :tableData="missingData"
+                  :isShowPagination="false"
+                  :tableLoading="missingLoading" />
+      </el-tab-pane>
+    </el-tabs>
+  </div>
+</template>
+
+<script setup>
+  import { ref, reactive, onMounted } from "vue";
+  import { ElMessage } from "element-plus";
+  import dayjs from "dayjs";
+  import { saveAs } from "file-saver";
+  import { blobValidate } from "@/utils/ruoyi";
+  import {
+    costStat,
+    costSummary,
+    missingPrice,
+    costStatExport,
+    costSummaryExport,
+    missingPriceExport,
+  } from "@/api/productionManagement/costAccounting.js";
+
+  // 榛樿鏌ヨ褰撳墠鏈�
+  const currentMonthRange = () => [
+    dayjs().startOf("month").format("YYYY-MM-DD"),
+    dayjs().endOf("month").format("YYYY-MM-DD"),
+  ];
+
+  const activeTab = ref("stat");
+  const dateRange = ref(currentMonthRange());
+  const queryParams = reactive({
+    npsNo: "",
+    productName: "",
+  });
+  const groupBy = ref("month");
+
+  const statColumn = ref([
+    { label: "鐢熶骇璁㈠崟鍙�", prop: "npsNo", minWidth: 150 },
+    { label: "鎴愬搧", prop: "finishedProduct", minWidth: 140 },
+    { label: "鍗曚綅", prop: "unit", width: 80 },
+    { label: "璁㈠崟鏃ユ湡", prop: "orderDate", width: 120 },
+    { label: "浜ч噺", prop: "outputQty", width: 100 },
+    { label: "鏉愭枡鎴愭湰", prop: "materialCost", width: 110 },
+    { label: "浜哄伐鎴愭湰", prop: "laborCost", width: 110, dataType: "slot", slot: "laborCost" },
+    { label: "鑳借�楁垚鏈�", prop: "energyCost", width: 110 },
+    { label: "鎬绘垚鏈�", prop: "totalCost", width: 110 },
+    { label: "鍗曚綅鎴愭湰", prop: "unitCost", width: 110 },
+    { label: "閿�鍞崟浠�", prop: "salePrice", width: 110, formatData: v => formatNum(v) },
+    { label: "姣涘埄", prop: "grossProfit", width: 110, formatData: v => formatNum(v) },
+    {
+      label: "姣涘埄鐜�(%)",
+      prop: "grossProfitRate",
+      width: 110,
+      dataType: "slot",
+      slot: "grossProfitRate",
+    },
+    { label: "浠锋牸瑕嗙洊鐜�(%)", prop: "priceCoverageRate", width: 120 },
+    {
+      label: "缂轰环鐗╂枡鏁�",
+      prop: "missingPriceKinds",
+      width: 110,
+      dataType: "slot",
+      slot: "missingPriceKinds",
+    },
+    { label: "鏃犲伐璧勬爣鍑嗚褰�", prop: "noRateRows", width: 130 },
+  ]);
+
+  const summaryColumn = ref([
+    { label: "鍒嗙粍", prop: "groupKey", minWidth: 160 },
+    { label: "璁㈠崟鏁�", prop: "orderCount", width: 100 },
+    { label: "浜ч噺", prop: "outputQty", width: 110 },
+    { label: "鏉愭枡鎴愭湰", prop: "materialCost", width: 110 },
+    { label: "鏉愭枡鍗犳瘮(%)", prop: "materialRate", width: 120 },
+    { label: "浜哄伐鎴愭湰", prop: "laborCost", width: 110 },
+    { label: "浜哄伐鍗犳瘮(%)", prop: "laborRate", width: 120 },
+    { label: "鑳借�楁垚鏈�", prop: "energyCost", width: 110 },
+    { label: "鑳借�楀崰姣�(%)", prop: "energyRate", width: 120 },
+    { label: "鎬绘垚鏈�", prop: "totalCost", width: 110 },
+    { label: "鍗曚綅鎴愭湰", prop: "unitCost", width: 110 },
+    { label: "姣涘埄", prop: "grossProfit", width: 110 },
+    { label: "姣涘埄鐜�(%)", prop: "grossProfitRate", width: 110 },
+  ]);
+
+  const missingColumn = ref([
+    { label: "鐗╂枡鍚嶇О", prop: "materialName", minWidth: 160 },
+    { label: "瑙勬牸鍨嬪彿", prop: "materialModel", minWidth: 160 },
+    { label: "鍗曚綅", prop: "unit", width: 80 },
+    { label: "鎶曞叆鏁伴噺", prop: "inputQty", width: 120 },
+    { label: "娑夊強璁㈠崟鏁�", prop: "orderCount", width: 110 },
+    { label: "鏈�杩戞姇鍏ユ椂闂�", prop: "lastInputTime", width: 170 },
+  ]);
+
+  const statData = ref([]);
+  const statLoading = ref(false);
+  const statExportLoading = ref(false);
+  const summaryData = ref([]);
+  const summaryLoading = ref(false);
+  const summaryExportLoading = ref(false);
+  const missingData = ref([]);
+  const missingLoading = ref(false);
+  const missingExportLoading = ref(false);
+
+  const buildQuery = () => ({
+    startDate: dateRange.value?.[0] || undefined,
+    endDate: dateRange.value?.[1] || undefined,
+    npsNo: queryParams.npsNo || undefined,
+    productName: queryParams.productName || undefined,
+  });
+
+  const buildSummaryQuery = () => ({
+    groupBy: groupBy.value,
+    ...buildQuery(),
+  });
+
+  const formatNum = v => {
+    if (v === null || v === undefined || v === "") {
+      return "鈥�";
+    }
+    return v;
+  };
+
+  const isNegative = v => {
+    const num = Number(v);
+    return Number.isFinite(num) && num < 0;
+  };
+
+  const formatRate = v => {
+    if (v === null || v === undefined || v === "") {
+      return "鈥�";
+    }
+    return v;
+  };
+
+  const getStat = () => {
+    statLoading.value = true;
+    costStat(buildQuery())
+      .then(res => {
+        statData.value = res?.data || [];
+      })
+      .catch(() => {})
+      .finally(() => {
+        statLoading.value = false;
+      });
+  };
+
+  const getSummary = () => {
+    summaryLoading.value = true;
+    costSummary(buildSummaryQuery())
+      .then(res => {
+        summaryData.value = res?.data || [];
+      })
+      .catch(() => {})
+      .finally(() => {
+        summaryLoading.value = false;
+      });
+  };
+
+  const getMissing = () => {
+    missingLoading.value = true;
+    missingPrice(buildQuery())
+      .then(res => {
+        missingData.value = res?.data || [];
+      })
+      .catch(() => {})
+      .finally(() => {
+        missingLoading.value = false;
+      });
+  };
+
+  const loadCurrentTab = () => {
+    if (activeTab.value === "stat") {
+      getStat();
+    } else if (activeTab.value === "summary") {
+      getSummary();
+    } else {
+      getMissing();
+    }
+  };
+
+  const handleQuery = () => {
+    loadCurrentTab();
+  };
+
+  const resetQuery = () => {
+    dateRange.value = currentMonthRange();
+    queryParams.npsNo = "";
+    queryParams.productName = "";
+    loadCurrentTab();
+  };
+
+  const handleTabChange = () => {
+    loadCurrentTab();
+  };
+
+  const exportBlob = async (requestFn, query, filename) => {
+    const blobData = await requestFn(query);
+    if (blobValidate(blobData)) {
+      if (blobData.size === 0) {
+        ElMessage.warning("鏆傛棤鏁版嵁鍙鍑�");
+        return;
+      }
+      const blob = new Blob([blobData], {
+        type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+      });
+      saveAs(blob, filename);
+      ElMessage.success("瀵煎嚭鎴愬姛");
+    } else {
+      const resText = await blobData.text();
+      let rspObj = {};
+      try {
+        rspObj = JSON.parse(resText);
+      } catch (error) {
+        rspObj = {};
+      }
+      ElMessage.error(rspObj.msg || "瀵煎嚭澶辫触");
+    }
+  };
+
+  const handleStatExport = async () => {
+    statExportLoading.value = true;
+    try {
+      await exportBlob(costStatExport, buildQuery(), "鎴愭湰鏍哥畻鏄庣粏.xlsx");
+    } catch (error) {
+      ElMessage.error("瀵煎嚭澶辫触锛岃绋嶅悗閲嶈瘯");
+    } finally {
+      statExportLoading.value = false;
+    }
+  };
+
+  const handleSummaryExport = async () => {
+    summaryExportLoading.value = true;
+    try {
+      await exportBlob(costSummaryExport, buildSummaryQuery(), "鎴愭湰鏋勬垚姹囨��.xlsx");
+    } catch (error) {
+      ElMessage.error("瀵煎嚭澶辫触锛岃绋嶅悗閲嶈瘯");
+    } finally {
+      summaryExportLoading.value = false;
+    }
+  };
+
+  const handleMissingExport = async () => {
+    missingExportLoading.value = true;
+    try {
+      await exportBlob(missingPriceExport, buildQuery(), "缂轰环鐗╂枡娓呭崟.xlsx");
+    } catch (error) {
+      ElMessage.error("瀵煎嚭澶辫触锛岃绋嶅悗閲嶈瘯");
+    } finally {
+      missingExportLoading.value = false;
+    }
+  };
+
+  onMounted(() => {
+    getStat();
+  });
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/views/productionManagement/materialConsumption/index.vue b/src/views/productionManagement/materialConsumption/index.vue
new file mode 100644
index 0000000..c46f197
--- /dev/null
+++ b/src/views/productionManagement/materialConsumption/index.vue
@@ -0,0 +1,199 @@
+<template>
+  <div class="app-container">
+    <PageHeader content="鏉愭枡娑堣�楁牳绠�" />
+    <div class="search_form">
+      <el-form :model="queryParams"
+               inline
+               style="margin-bottom: 0;">
+        <el-form-item label="璁㈠崟鏃ユ湡锛�">
+          <el-date-picker v-model="dateRange"
+                          type="daterange"
+                          range-separator="鑷�"
+                          start-placeholder="寮�濮嬫棩鏈�"
+                          end-placeholder="缁撴潫鏃ユ湡"
+                          value-format="YYYY-MM-DD"
+                          @change="handleQuery" />
+        </el-form-item>
+        <el-form-item label="鐢熶骇璁㈠崟鍙凤細">
+          <el-input v-model="queryParams.npsNo"
+                    placeholder="璇疯緭鍏�"
+                    clearable
+                    style="width: 200px"
+                    @keyup.enter="handleQuery" />
+        </el-form-item>
+        <el-form-item label="鎴愬搧锛�">
+          <el-input v-model="queryParams.productName"
+                    placeholder="璇疯緭鍏�"
+                    clearable
+                    style="width: 200px"
+                    @keyup.enter="handleQuery" />
+        </el-form-item>
+        <el-form-item label="鍙湅宸紓锛�">
+          <el-switch v-model="queryParams.onlyDiff"
+                     @change="handleQuery" />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary"
+                     @click="handleQuery">鏌ヨ</el-button>
+          <el-button @click="resetQuery">閲嶇疆</el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+    <div class="mb20"
+         style="text-align: right;">
+      <el-button type="warning"
+                 plain
+                 icon="Download"
+                 :loading="exportLoading"
+                 @click="handleExport">瀵煎嚭</el-button>
+    </div>
+    <div class="table_list">
+      <PIMTable rowKey="rowKey"
+                :column="tableColumn"
+                :tableData="tableData"
+                :isShowPagination="false"
+                :tableLoading="tableLoading">
+        <template #diffRate="{ row }">
+          <span :style="{ color: diffRateColor(row.diffRate) }">
+            {{ formatDiffRate(row.diffRate) }}
+          </span>
+        </template>
+      </PIMTable>
+    </div>
+  </div>
+</template>
+
+<script setup>
+  import { ref, reactive, getCurrentInstance, onMounted } from "vue";
+  import { ElMessage } from "element-plus";
+  import dayjs from "dayjs";
+  import { saveAs } from "file-saver";
+  import { blobValidate } from "@/utils/ruoyi";
+  import { stat, statExport } from "@/api/productionManagement/materialConsumption.js";
+
+  const { proxy } = getCurrentInstance();
+
+  const tableColumn = ref([
+    { label: "鐢熶骇璁㈠崟鍙�", prop: "npsNo", minWidth: 150 },
+    { label: "鎴愬搧", prop: "finishedProduct", minWidth: 140 },
+    { label: "鐗╂枡鍚嶇О", prop: "materialName", minWidth: 140 },
+    { label: "瑙勬牸鍨嬪彿", prop: "materialModel", minWidth: 140 },
+    { label: "鍗曚綅", prop: "unit", width: 80 },
+    { label: "BOM鍗曡��", prop: "unitQuantity", width: 110 },
+    { label: "瀹屽伐鏁伴噺", prop: "completeQty", width: 110 },
+    { label: "鏍囧噯娑堣��", prop: "standardQty", width: 110 },
+    { label: "瀹為檯娑堣��", prop: "actualQty", width: 110 },
+    { label: "宸紓鏁伴噺", prop: "diffQty", width: 110 },
+    {
+      label: "宸紓鐜�(%)",
+      prop: "diffRate",
+      width: 110,
+      dataType: "slot",
+      slot: "diffRate",
+    },
+  ]);
+
+  const tableData = ref([]);
+  const tableLoading = ref(false);
+  const exportLoading = ref(false);
+  // 榛樿鏌ヨ褰撳墠鏈�
+  const currentMonthRange = () => [
+    dayjs().startOf("month").format("YYYY-MM-DD"),
+    dayjs().endOf("month").format("YYYY-MM-DD"),
+  ];
+
+  const dateRange = ref(currentMonthRange());
+  const queryParams = reactive({
+    npsNo: "",
+    productName: "",
+    onlyDiff: false,
+  });
+
+  const buildQuery = () => ({
+    startDate: dateRange.value?.[0] || undefined,
+    endDate: dateRange.value?.[1] || undefined,
+    npsNo: queryParams.npsNo || undefined,
+    productName: queryParams.productName || undefined,
+    onlyDiff: queryParams.onlyDiff ? true : undefined,
+  });
+
+  const formatDiffRate = rate => {
+    if (rate === null || rate === undefined || rate === "") {
+      return "-";
+    }
+    return rate;
+  };
+
+  const diffRateColor = rate => {
+    const num = Number(rate);
+    if (rate === null || rate === undefined || rate === "" || !Number.isFinite(num)) {
+      return "";
+    }
+    return Math.abs(num) > 10 ? "#F56C6C" : "";
+  };
+
+  const getList = () => {
+    tableLoading.value = true;
+    stat(buildQuery())
+      .then(res => {
+        const list = res?.data || [];
+        tableData.value = list.map((item, index) => ({
+          ...item,
+          rowKey: `${item.productionOrderId}_${item.materialModelId}_${index}`,
+        }));
+      })
+      .catch(() => {})
+      .finally(() => {
+        tableLoading.value = false;
+      });
+  };
+
+  const handleQuery = () => {
+    getList();
+  };
+
+  const resetQuery = () => {
+    dateRange.value = currentMonthRange();
+    queryParams.npsNo = "";
+    queryParams.productName = "";
+    queryParams.onlyDiff = false;
+    getList();
+  };
+
+  const handleExport = async () => {
+    exportLoading.value = true;
+    try {
+      const blobData = await statExport(buildQuery());
+      if (blobValidate(blobData)) {
+        if (blobData.size === 0) {
+          ElMessage.warning("鏆傛棤鏁版嵁鍙鍑�");
+          return;
+        }
+        const blob = new Blob([blobData], {
+          type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+        });
+        saveAs(blob, "鏉愭枡娑堣�楁牳绠�.xlsx");
+        ElMessage.success("瀵煎嚭鎴愬姛");
+      } else {
+        const resText = await blobData.text();
+        let rspObj = {};
+        try {
+          rspObj = JSON.parse(resText);
+        } catch (error) {
+          rspObj = {};
+        }
+        ElMessage.error(rspObj.msg || "瀵煎嚭澶辫触");
+      }
+    } catch (error) {
+      ElMessage.error("瀵煎嚭澶辫触锛岃绋嶅悗閲嶈瘯");
+    } finally {
+      exportLoading.value = false;
+    }
+  };
+
+  onMounted(() => {
+    getList();
+  });
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/views/productionManagement/outputStatistics/index.vue b/src/views/productionManagement/outputStatistics/index.vue
new file mode 100644
index 0000000..5a7cd7f
--- /dev/null
+++ b/src/views/productionManagement/outputStatistics/index.vue
@@ -0,0 +1,350 @@
+<template>
+  <div class="app-container">
+    <PageHeader content="鐢熶骇缁熻" />
+    <el-tabs v-model="activeTab"
+             @tab-change="handleTabChange">
+      <!-- 璁′欢宸ヨ祫姹囨�� -->
+      <el-tab-pane label="璁′欢宸ヨ祫姹囨��"
+                   name="wage">
+        <div class="search_form">
+          <el-form :model="wageQuery"
+                   inline
+                   style="margin-bottom: 0;">
+            <el-form-item label="鏃ユ湡鑼冨洿锛�">
+              <el-date-picker v-model="wageDateRange"
+                              type="daterange"
+                              range-separator="鑷�"
+                              start-placeholder="寮�濮嬫棩鏈�"
+                              end-placeholder="缁撴潫鏃ユ湡"
+                              value-format="YYYY-MM-DD"
+                              @change="handleWageQuery" />
+            </el-form-item>
+            <el-form-item label="浜哄憳濮撳悕锛�">
+              <el-input v-model="wageQuery.userName"
+                        placeholder="璇疯緭鍏�"
+                        clearable
+                        style="width: 200px"
+                        @keyup.enter="handleWageQuery" />
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary"
+                         @click="handleWageQuery">鏌ヨ</el-button>
+              <el-button @click="resetWageQuery">閲嶇疆</el-button>
+            </el-form-item>
+          </el-form>
+        </div>
+        <div class="mb20"
+             style="text-align: right;">
+          <el-button type="warning"
+                     plain
+                     icon="Download"
+                     :loading="wageExportLoading"
+                     @click="handleWageExport">瀵煎嚭</el-button>
+        </div>
+        <div class="table_list">
+          <PIMTable rowKey="userId"
+                    :column="wageColumn"
+                    :tableData="wageData"
+                    :isShowPagination="false"
+                    :tableLoading="wageLoading" />
+        </div>
+      </el-tab-pane>
+
+      <!-- 浜ч噺缁熻 -->
+      <el-tab-pane label="浜ч噺缁熻"
+                   name="output">
+        <div class="search_form">
+          <el-form :model="outputQuery"
+                   inline
+                   style="margin-bottom: 0;">
+            <el-form-item label="缁熻缁村害锛�">
+              <el-radio-group v-model="outputQuery.dimension"
+                              @change="handleDimensionChange">
+                <el-radio label="device">璁惧</el-radio>
+                <el-radio label="operation">宸ュ簭</el-radio>
+                <el-radio label="product">浜у搧</el-radio>
+              </el-radio-group>
+            </el-form-item>
+            <el-form-item label="鏃ユ湡鑼冨洿锛�">
+              <el-date-picker v-model="outputDateRange"
+                              type="daterange"
+                              range-separator="鑷�"
+                              start-placeholder="寮�濮嬫棩鏈�"
+                              end-placeholder="缁撴潫鏃ユ湡"
+                              value-format="YYYY-MM-DD"
+                              @change="handleOutputQuery" />
+            </el-form-item>
+            <el-form-item v-if="outputQuery.dimension === 'device'"
+                          label="璁惧锛�">
+              <el-select v-model="outputQuery.deviceId"
+                         placeholder="鍏ㄩ儴"
+                         clearable
+                         filterable
+                         style="width: 200px"
+                         @change="handleOutputQuery">
+                <el-option v-for="item in deviceOptions"
+                           :key="item.id"
+                           :label="item.deviceName"
+                           :value="item.id" />
+              </el-select>
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary"
+                         @click="handleOutputQuery">鏌ヨ</el-button>
+              <el-button @click="resetOutputQuery">閲嶇疆</el-button>
+            </el-form-item>
+          </el-form>
+        </div>
+        <div class="mb20"
+             style="text-align: right;">
+          <el-button type="warning"
+                     plain
+                     icon="Download"
+                     :loading="outputExportLoading"
+                     @click="handleOutputExport">瀵煎嚭</el-button>
+        </div>
+        <div class="table_list">
+          <PIMTable rowKey="groupId"
+                    :column="outputColumn"
+                    :tableData="outputData"
+                    :isShowPagination="false"
+                    :tableLoading="outputLoading" />
+        </div>
+      </el-tab-pane>
+    </el-tabs>
+  </div>
+</template>
+
+<script setup>
+  import { ref, reactive, getCurrentInstance, onMounted } from "vue";
+  import { ElMessage } from "element-plus";
+  import { saveAs } from "file-saver";
+  import { blobValidate } from "@/utils/ruoyi";
+  import {
+    wageSummary,
+    wageSummaryExport,
+    outputStat,
+    outputStatExport,
+  } from "@/api/productionManagement/productionStat.js";
+  import { getDeviceLedger } from "@/api/equipmentManagement/ledger.js";
+
+  const { proxy } = getCurrentInstance();
+
+  const activeTab = ref("wage");
+  const deviceOptions = ref([]);
+
+  const wageColumn = ref([
+    {
+      label: "鐢熶骇浜�",
+      prop: "userName",
+      minWidth: 120,
+    },
+    {
+      label: "鎶ュ伐鍗曟暟",
+      prop: "workCount",
+      width: 110,
+    },
+    {
+      label: "鐢熶骇鏁伴噺",
+      prop: "quantity",
+      width: 120,
+    },
+    {
+      label: "鎬诲伐鏃�",
+      prop: "workHour",
+      width: 110,
+    },
+    {
+      label: "璁′欢宸ヨ祫",
+      prop: "pieceWage",
+      width: 120,
+    },
+    {
+      label: "搴斿彂鍚堣",
+      prop: "wages",
+      width: 120,
+    },
+  ]);
+
+  const outputColumn = ref([
+    {
+      label: "鍒嗙粍鍚嶇О",
+      prop: "groupName",
+      minWidth: 160,
+    },
+    {
+      label: "鎶ュ伐鍗曟暟",
+      prop: "workCount",
+      width: 110,
+    },
+    {
+      label: "浜ч噺",
+      prop: "quantity",
+      width: 120,
+    },
+    {
+      label: "鎶ュ簾鏁伴噺",
+      prop: "scrapQty",
+      width: 120,
+    },
+    {
+      label: "宸ユ椂",
+      prop: "workHour",
+      width: 110,
+    },
+  ]);
+
+  const wageData = ref([]);
+  const wageLoading = ref(false);
+  const wageExportLoading = ref(false);
+  const wageDateRange = ref([]);
+  const wageQuery = reactive({
+    userName: "",
+  });
+
+  const outputData = ref([]);
+  const outputLoading = ref(false);
+  const outputExportLoading = ref(false);
+  const outputDateRange = ref([]);
+  const outputQuery = reactive({
+    dimension: "device",
+    deviceId: "",
+  });
+
+  const buildWageQuery = () => ({
+    startDate: wageDateRange.value?.[0] || undefined,
+    endDate: wageDateRange.value?.[1] || undefined,
+    userName: wageQuery.userName || undefined,
+  });
+
+  const buildOutputQuery = () => ({
+    dimension: outputQuery.dimension,
+    startDate: outputDateRange.value?.[0] || undefined,
+    endDate: outputDateRange.value?.[1] || undefined,
+    deviceId:
+      outputQuery.dimension === "device" && outputQuery.deviceId
+        ? outputQuery.deviceId
+        : undefined,
+  });
+
+  // 璁′欢宸ヨ祫姹囨��
+  const getWageList = () => {
+    wageLoading.value = true;
+    wageSummary(buildWageQuery())
+      .then(res => {
+        wageData.value = res?.data || [];
+      })
+      .catch(() => {})
+      .finally(() => {
+        wageLoading.value = false;
+      });
+  };
+
+  const handleWageQuery = () => {
+    getWageList();
+  };
+
+  const resetWageQuery = () => {
+    wageQuery.userName = "";
+    wageDateRange.value = [];
+    getWageList();
+  };
+
+  // 浜ч噺缁熻
+  const getOutputList = () => {
+    outputLoading.value = true;
+    outputStat(buildOutputQuery())
+      .then(res => {
+        outputData.value = res?.data || [];
+      })
+      .catch(() => {})
+      .finally(() => {
+        outputLoading.value = false;
+      });
+  };
+
+  const handleOutputQuery = () => {
+    getOutputList();
+  };
+
+  const resetOutputQuery = () => {
+    outputQuery.dimension = "device";
+    outputQuery.deviceId = "";
+    outputDateRange.value = [];
+    getOutputList();
+  };
+
+  const handleDimensionChange = () => {
+    outputQuery.deviceId = "";
+    getOutputList();
+  };
+
+  const handleTabChange = name => {
+    if (name === "wage") {
+      getWageList();
+    } else {
+      getOutputList();
+    }
+  };
+
+  // 瀵煎嚭
+  const exportBlob = async (requestFn, query, filename) => {
+    const blobData = await requestFn(query);
+    if (blobValidate(blobData)) {
+      if (blobData.size === 0) {
+        ElMessage.warning("鏆傛棤鏁版嵁鍙鍑�");
+        return;
+      }
+      const blob = new Blob([blobData], {
+        type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+      });
+      saveAs(blob, filename);
+      ElMessage.success("瀵煎嚭鎴愬姛");
+    } else {
+      const resText = await blobData.text();
+      let rspObj = {};
+      try {
+        rspObj = JSON.parse(resText);
+      } catch (error) {
+        rspObj = {};
+      }
+      ElMessage.error(rspObj.msg || "瀵煎嚭澶辫触");
+    }
+  };
+
+  const handleWageExport = async () => {
+    wageExportLoading.value = true;
+    try {
+      await exportBlob(wageSummaryExport, buildWageQuery(), "璁′欢宸ヨ祫姹囨��.xlsx");
+    } catch (error) {
+      ElMessage.error("瀵煎嚭澶辫触锛岃绋嶅悗閲嶈瘯");
+    } finally {
+      wageExportLoading.value = false;
+    }
+  };
+
+  const handleOutputExport = async () => {
+    outputExportLoading.value = true;
+    try {
+      await exportBlob(outputStatExport, buildOutputQuery(), "浜ч噺缁熻.xlsx");
+    } catch (error) {
+      ElMessage.error("瀵煎嚭澶辫触锛岃绋嶅悗閲嶈瘯");
+    } finally {
+      outputExportLoading.value = false;
+    }
+  };
+
+  onMounted(() => {
+    getWageList();
+    getDeviceLedger()
+      .then(res => {
+        deviceOptions.value = res.data || [];
+      })
+      .catch(err => {
+        console.error("鑾峰彇璁惧鍒楄〃澶辫触", err);
+        deviceOptions.value = [];
+      });
+  });
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/views/productionManagement/pieceRateConfig/index.vue b/src/views/productionManagement/pieceRateConfig/index.vue
new file mode 100644
index 0000000..16b0691
--- /dev/null
+++ b/src/views/productionManagement/pieceRateConfig/index.vue
@@ -0,0 +1,450 @@
+<template>
+  <div class="app-container">
+    <PageHeader content="璁′欢鍗曚环閰嶇疆" />
+    <div class="search_form">
+      <el-form :model="queryParams"
+               inline
+               style="margin-bottom: 0;">
+        <el-form-item label="瑙勬牸鍨嬪彿锛�">
+          <el-input v-model="queryParams.productModel"
+                    placeholder="璇疯緭鍏ヨ鏍煎瀷鍙�"
+                    clearable
+                    style="width: 200px"
+                    @keyup.enter="handleQuery" />
+        </el-form-item>
+        <el-form-item label="宸ュ簭鍚嶇О锛�">
+          <el-input v-model="queryParams.operationName"
+                    placeholder="璇疯緭鍏ュ伐搴忓悕绉�"
+                    clearable
+                    style="width: 200px"
+                    @keyup.enter="handleQuery" />
+        </el-form-item>
+        <el-form-item label="鐘舵�侊細">
+          <el-select v-model="queryParams.status"
+                     placeholder="鍏ㄩ儴"
+                     clearable
+                     style="width: 160px"
+                     @change="handleQuery">
+            <el-option label="鍚敤"
+                       :value="1" />
+            <el-option label="鍋滅敤"
+                       :value="0" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary"
+                     @click="handleQuery">鏌ヨ</el-button>
+          <el-button @click="resetQuery">閲嶇疆</el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+    <div class="mb20"
+         style="text-align: right;">
+      <el-button type="primary"
+                 @click="openDialog('add')">鏂板</el-button>
+      <el-button type="danger"
+                 plain
+                 @click="handleBatchDelete">鍒犻櫎</el-button>
+      <el-button type="warning"
+                 plain
+                 icon="Download"
+                 :loading="exportLoading"
+                 @click="handleExport">瀵煎嚭</el-button>
+    </div>
+    <div class="table_list">
+      <PIMTable rowKey="id"
+                :column="tableColumn"
+                :tableData="tableData"
+                :page="page"
+                :isSelection="true"
+                @selection-change="handleSelectionChange"
+                :tableLoading="tableLoading"
+                @pagination="pagination" />
+    </div>
+
+    <el-dialog v-model="dialogVisible"
+               :title="operationType === 'add' ? '鏂板璁′欢鍗曚环' : '缂栬緫璁′欢鍗曚环'"
+               width="480px"
+               @close="closeDialog">
+      <el-form ref="formRef"
+               :model="form"
+               :rules="rules"
+               label-width="100px">
+        <el-form-item label="瑙勬牸鍨嬪彿锛�"
+                      prop="productModel">
+          <el-input v-model="form.productModel"
+                    placeholder="璇烽�夋嫨瑙勬牸鍨嬪彿"
+                    readonly
+                    @click="openProductSelect">
+            <template #append>
+              <el-button icon="Search"
+                         @click="openProductSelect" />
+            </template>
+          </el-input>
+        </el-form-item>
+        <el-form-item label="宸ュ簭鍚嶇О锛�"
+                      prop="operationName">
+          <el-input v-model="form.operationName"
+                    placeholder="璇疯緭鍏ュ伐搴忓悕绉�"
+                    clearable />
+        </el-form-item>
+        <el-form-item label="璁′欢鍗曚环锛�"
+                      prop="unitPrice">
+          <el-input-number v-model="form.unitPrice"
+                           :min="0.0001"
+                           :precision="4"
+                           :controls="false"
+                           placeholder="璇疯緭鍏ヨ浠跺崟浠�"
+                           style="width: 100%" />
+        </el-form-item>
+        <el-form-item label="澶囨敞锛�">
+          <el-input v-model="form.remark"
+                    type="textarea"
+                    :rows="2"
+                    placeholder="璇疯緭鍏ュ娉�"
+                    clearable />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary"
+                     @click="submitForm">纭</el-button>
+          <el-button @click="closeDialog">鍙栨秷</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <ProductSelectDialog v-model="productSelectVisible"
+                         single
+                         @confirm="handleProductSelected" />
+  </div>
+</template>
+
+<script setup>
+  import { ref, reactive, getCurrentInstance, onMounted } from "vue";
+  import { ElMessage, ElMessageBox } from "element-plus";
+  import { saveAs } from "file-saver";
+  import { blobValidate } from "@/utils/ruoyi";
+  import {
+    listPage,
+    add,
+    update,
+    changeStatus,
+    batchDelete,
+    exportConfig,
+  } from "@/api/productionManagement/pieceRateConfig.js";
+  import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
+
+  const { proxy } = getCurrentInstance();
+
+  const tableColumn = ref([
+    {
+      label: "瑙勬牸鍨嬪彿",
+      prop: "productModel",
+      minWidth: 160,
+    },
+    {
+      label: "宸ュ簭鍚嶇О",
+      prop: "operationName",
+      minWidth: 140,
+    },
+    {
+      label: "璁′欢鍗曚环",
+      prop: "unitPrice",
+      width: 120,
+    },
+    {
+      label: "鐘舵��",
+      prop: "status",
+      dataType: "tag",
+      width: 100,
+      formatData: v => (v == 1 ? "鍚敤" : "鍋滅敤"),
+      formatType: v => (v == 1 ? "success" : "info"),
+    },
+    {
+      label: "澶囨敞",
+      prop: "remark",
+      minWidth: 140,
+    },
+    {
+      label: "鍒涘缓鏃堕棿",
+      prop: "createTime",
+      width: 170,
+    },
+    {
+      dataType: "action",
+      label: "鎿嶄綔",
+      align: "center",
+      fixed: "right",
+      width: 200,
+      operation: [
+        {
+          name: "缂栬緫",
+          type: "text",
+          clickFun: row => {
+            openDialog("edit", row);
+          },
+        },
+        {
+          name: "鍚敤",
+          type: "text",
+          color: "#67C23A",
+          showHide: row => row.status != 1,
+          clickFun: row => {
+            handleChangeStatus(row, 1);
+          },
+        },
+        {
+          name: "鍋滅敤",
+          type: "text",
+          color: "#909399",
+          showHide: row => row.status == 1,
+          clickFun: row => {
+            handleChangeStatus(row, 0);
+          },
+        },
+        {
+          name: "鍒犻櫎",
+          type: "danger",
+          link: true,
+          clickFun: row => {
+            handleDelete(row);
+          },
+        },
+      ],
+    },
+  ]);
+
+  const tableData = ref([]);
+  const selectedRows = ref([]);
+  const tableLoading = ref(false);
+  const exportLoading = ref(false);
+  const page = reactive({
+    current: 1,
+    size: 10,
+    total: 0,
+  });
+  const queryParams = reactive({
+    productModel: "",
+    operationName: "",
+    status: "",
+  });
+
+  const dialogVisible = ref(false);
+  const operationType = ref("add");
+  const formRef = ref(null);
+  const productSelectVisible = ref(false);
+  const form = reactive({
+    id: "",
+    productModelId: "",
+    productModel: "",
+    operationName: "",
+    unitPrice: undefined,
+    remark: "",
+  });
+  const rules = {
+    productModel: [{ required: true, message: "璇烽�夋嫨瑙勬牸鍨嬪彿", trigger: "change" }],
+    operationName: [{ required: true, message: "璇疯緭鍏ュ伐搴忓悕绉�", trigger: "blur" }],
+    unitPrice: [{ required: true, message: "璇疯緭鍏ヨ浠跺崟浠�", trigger: "blur" }],
+  };
+
+  const buildQuery = () => ({
+    productModel: queryParams.productModel || undefined,
+    operationName: queryParams.operationName || undefined,
+    status:
+      queryParams.status === "" ||
+      queryParams.status === null ||
+      queryParams.status === undefined
+        ? undefined
+        : queryParams.status,
+  });
+
+  // 鏌ヨ鍒楄〃
+  const getList = () => {
+    tableLoading.value = true;
+    listPage({
+      pageNum: page.current,
+      pageSize: page.size,
+      ...buildQuery(),
+    })
+      .then(res => {
+        tableData.value = res?.data?.records || [];
+        page.total = res?.data?.total || 0;
+      })
+      .catch(() => {})
+      .finally(() => {
+        tableLoading.value = false;
+      });
+  };
+
+  const handleQuery = () => {
+    page.current = 1;
+    getList();
+  };
+
+  const resetQuery = () => {
+    queryParams.productModel = "";
+    queryParams.operationName = "";
+    queryParams.status = "";
+    handleQuery();
+  };
+
+  const pagination = obj => {
+    page.current = obj.page;
+    page.size = obj.limit;
+    getList();
+  };
+
+  const handleSelectionChange = selection => {
+    selectedRows.value = selection;
+  };
+
+  // 鎵撳紑寮规
+  const openDialog = (type, row) => {
+    operationType.value = type;
+    if (type === "edit" && row) {
+      form.id = row.id;
+      form.productModelId = row.productModelId;
+      form.productModel = row.productModel;
+      form.operationName = row.operationName;
+      form.unitPrice = row.unitPrice;
+      form.remark = row.remark;
+    } else {
+      form.id = "";
+      form.productModelId = "";
+      form.productModel = "";
+      form.operationName = "";
+      form.unitPrice = undefined;
+      form.remark = "";
+    }
+    dialogVisible.value = true;
+  };
+
+  const closeDialog = () => {
+    formRef.value?.resetFields();
+    dialogVisible.value = false;
+  };
+
+  const openProductSelect = () => {
+    productSelectVisible.value = true;
+  };
+
+  const handleProductSelected = rows => {
+    const row = rows && rows[0];
+    if (row) {
+      form.productModelId = row.id;
+      form.productModel = row.model;
+    }
+  };
+
+  // 鎻愪氦
+  const submitForm = () => {
+    formRef.value?.validate(valid => {
+      if (!valid) {
+        return;
+      }
+      const submitData = {
+        id: form.id,
+        productModelId: form.productModelId,
+        operationName: form.operationName,
+        unitPrice: form.unitPrice,
+        remark: form.remark,
+      };
+      const request = operationType.value === "edit" ? update : add;
+      request(submitData)
+        .then(() => {
+          proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+          closeDialog();
+          getList();
+        })
+        .catch(() => {});
+    });
+  };
+
+  // 鍚敤/鍋滅敤
+  const handleChangeStatus = (row, status) => {
+    const text = status == 1 ? "鍚敤" : "鍋滅敤";
+    ElMessageBox.confirm(`纭${text}璇ヨ浠跺崟浠凤紵`, "鎻愮ず", {
+      confirmButtonText: "纭",
+      cancelButtonText: "鍙栨秷",
+      type: "warning",
+    })
+      .then(() => {
+        changeStatus({ id: row.id, status })
+          .then(() => {
+            proxy.$modal.msgSuccess("鎿嶄綔鎴愬姛");
+            getList();
+          })
+          .catch(() => {});
+      })
+      .catch(() => {});
+  };
+
+  // 鍒犻櫎
+  const handleDelete = row => {
+    removeRows([row.id]);
+  };
+
+  const handleBatchDelete = () => {
+    if (selectedRows.value.length === 0) {
+      proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+      return;
+    }
+    removeRows(selectedRows.value.map(item => item.id));
+  };
+
+  const removeRows = ids => {
+    ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎鎻愮ず", {
+      confirmButtonText: "纭",
+      cancelButtonText: "鍙栨秷",
+      type: "warning",
+    })
+      .then(() => {
+        batchDelete(ids)
+          .then(() => {
+            proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+            getList();
+          })
+          .catch(() => {});
+      })
+      .catch(() => {});
+  };
+
+  // 瀵煎嚭
+  const handleExport = async () => {
+    exportLoading.value = true;
+    try {
+      const blobData = await exportConfig(buildQuery());
+      if (blobValidate(blobData)) {
+        if (blobData.size === 0) {
+          ElMessage.warning("鏆傛棤鏁版嵁鍙鍑�");
+          return;
+        }
+        const blob = new Blob([blobData], {
+          type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+        });
+        saveAs(blob, "璁′欢鍗曚环閰嶇疆.xlsx");
+        ElMessage.success("瀵煎嚭鎴愬姛");
+      } else {
+        const resText = await blobData.text();
+        let rspObj = {};
+        try {
+          rspObj = JSON.parse(resText);
+        } catch (error) {
+          rspObj = {};
+        }
+        ElMessage.error(rspObj.msg || "瀵煎嚭澶辫触");
+      }
+    } catch (error) {
+      ElMessage.error("瀵煎嚭澶辫触锛岃绋嶅悗閲嶈瘯");
+    } finally {
+      exportLoading.value = false;
+    }
+  };
+
+  onMounted(() => {
+    getList();
+  });
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/views/productionManagement/productStructure/KitCheckDialog.vue b/src/views/productionManagement/productStructure/KitCheckDialog.vue
new file mode 100644
index 0000000..d358737
--- /dev/null
+++ b/src/views/productionManagement/productStructure/KitCheckDialog.vue
@@ -0,0 +1,216 @@
+<template>
+  <el-dialog v-model="visible"
+             title="BOM榻愬鍒嗘瀽"
+             width="900px"
+             @close="handleClose">
+    <div class="bom-info">
+      <span>BOM缂栧彿锛歿{ bom.bomNo || '-' }}</span>
+      <span>浜у搧鍚嶇О锛歿{ bom.productName || '-' }}</span>
+      <span>瑙勬牸鍨嬪彿锛歿{ bom.productModelName || '-' }}</span>
+    </div>
+
+    <el-form :inline="true"
+             @submit.prevent>
+      <el-form-item label="闇�姹傛暟閲�">
+        <el-input-number v-model="demandQty"
+                         :min="0"
+                         :precision="4"
+                         :step="1"
+                         :controls="false"
+                         placeholder="璇疯緭鍏ラ渶姹傛暟閲�"
+                         style="width: 180px" />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary"
+                   :loading="loading"
+                   @click="handleAnalyze">寮�濮嬪垎鏋�</el-button>
+        <el-button :disabled="!resultList.length"
+                   :loading="exportLoading"
+                   @click="handleExport">瀵煎嚭</el-button>
+        <el-button type="success"
+                   :disabled="!resultList.length"
+                   :loading="genLoading"
+                   @click="handleGenerate">鐢熸垚閲囪喘闇�姹�</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-table v-loading="loading"
+              :data="resultList"
+              border
+              size="small"
+              max-height="420"
+              style="width: 100%">
+      <el-table-column type="index"
+                       label="搴忓彿"
+                       width="60"
+                       align="center" />
+      <el-table-column label="鐗╂枡鍚嶇О"
+                       prop="productName"
+                       min-width="140" />
+      <el-table-column label="瑙勬牸鍨嬪彿"
+                       prop="model"
+                       min-width="140" />
+      <el-table-column label="鍗曚綅"
+                       prop="unit"
+                       width="80"
+                       align="center" />
+      <el-table-column label="闇�姹傛暟閲�"
+                       prop="requiredQty"
+                       width="110"
+                       align="right" />
+      <el-table-column label="鍙敤搴撳瓨"
+                       prop="availableQty"
+                       width="110"
+                       align="right" />
+      <el-table-column label="缂哄彛鏁伴噺"
+                       prop="shortageQty"
+                       width="110"
+                       align="right">
+        <template #default="{ row }">
+          <span :class="{ shortage: Number(row.shortageQty) > 0 }">{{ row.shortageQty }}</span>
+        </template>
+      </el-table-column>
+    </el-table>
+  </el-dialog>
+</template>
+
+<script setup>
+  import { ref, computed, getCurrentInstance } from "vue";
+  import { saveAs } from "file-saver";
+  import { blobValidate } from "@/utils/ruoyi";
+  import {
+    kitCheck,
+    kitCheckExport,
+    generateDemand,
+  } from "@/api/productionManagement/productBom.js";
+
+  const props = defineProps({
+    showModel: {
+      type: Boolean,
+      default: false,
+    },
+    bom: {
+      type: Object,
+      default: () => ({}),
+    },
+  });
+
+  const emits = defineEmits(["update:showModel"]);
+  const { proxy } = getCurrentInstance();
+
+  const visible = computed({
+    get() {
+      return props.showModel;
+    },
+    set(val) {
+      emits("update:showModel", val);
+    },
+  });
+
+  const demandQty = ref(undefined);
+  const resultList = ref([]);
+  const loading = ref(false);
+  const exportLoading = ref(false);
+  const genLoading = ref(false);
+
+  // 鏍¢獙骞剁粍瑁呰姹傚弬鏁�
+  const buildParams = () => {
+    if (!props.bom?.id) {
+      proxy.$modal.msgWarning("鏈幏鍙栧埌BOM淇℃伅锛岃閲嶈瘯");
+      return null;
+    }
+    const qty = Number(demandQty.value);
+    if (!qty || qty <= 0) {
+      proxy.$modal.msgWarning("璇疯緭鍏ュぇ浜�0鐨勯渶姹傛暟閲�");
+      return null;
+    }
+    return { bomId: props.bom.id, demandQty: qty };
+  };
+
+  // 寮�濮嬪垎鏋�
+  const handleAnalyze = () => {
+    const params = buildParams();
+    if (!params) return;
+    loading.value = true;
+    kitCheck(params)
+      .then(res => {
+        resultList.value = res?.data || [];
+        if (!resultList.value.length) {
+          proxy.$modal.msgWarning("璇OM涓嬫棤鐗╂枡鑺傜偣");
+        }
+      })
+      .catch(() => {})
+      .finally(() => {
+        loading.value = false;
+      });
+  };
+
+  // 瀵煎嚭
+  const handleExport = async () => {
+    const params = buildParams();
+    if (!params) return;
+    exportLoading.value = true;
+    try {
+      const blobData = await kitCheckExport(params);
+      if (blobValidate(blobData)) {
+        if (blobData.size === 0) {
+          proxy.$modal.msgWarning("鏆傛棤鏁版嵁鍙鍑�");
+          return;
+        }
+        const blob = new Blob([blobData], {
+          type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+        });
+        saveAs(blob, `BOM榻愬鍒嗘瀽_${props.bom.bomNo || props.bom.id}.xlsx`);
+        proxy.$modal.msgSuccess("瀵煎嚭鎴愬姛");
+      } else {
+        const resText = await blobData.text();
+        let rspObj = {};
+        try {
+          rspObj = JSON.parse(resText);
+        } catch (error) {
+          rspObj = {};
+        }
+        proxy.$modal.msgError(rspObj.msg || "瀵煎嚭澶辫触");
+      }
+    } catch (error) {
+      proxy.$modal.msgError("瀵煎嚭澶辫触锛岃绋嶅悗閲嶈瘯");
+    } finally {
+      exportLoading.value = false;
+    }
+  };
+
+  // 缂哄彛鐢熸垚閲囪喘闇�姹�
+  const handleGenerate = () => {
+    const params = buildParams();
+    if (!params) return;
+    genLoading.value = true;
+    generateDemand(params)
+      .then(res => {
+        proxy.$modal.msgSuccess(res?.data || "鐢熸垚瀹屾垚");
+      })
+      .catch(() => {})
+      .finally(() => {
+        genLoading.value = false;
+      });
+  };
+
+  const handleClose = () => {
+    demandQty.value = undefined;
+    resultList.value = [];
+  };
+</script>
+
+<style scoped lang="scss">
+  .bom-info {
+    display: flex;
+    gap: 24px;
+    margin-bottom: 16px;
+    font-size: 13px;
+    color: #606266;
+  }
+
+  .shortage {
+    color: #d93025;
+    font-weight: 600;
+  }
+</style>
diff --git a/src/views/productionManagement/productStructure/index.vue b/src/views/productionManagement/productStructure/index.vue
index 269f098..d99fb48 100644
--- a/src/views/productionManagement/productStructure/index.vue
+++ b/src/views/productionManagement/productStructure/index.vue
@@ -63,6 +63,9 @@
     <StructureEdit v-if="showEdit"
                    v-model:show-model="showEdit"
                    :record="currentRow" />
+    <!-- 榻愬鍒嗘瀽寮圭獥 -->
+    <KitCheckDialog v-model:show-model="kitCheckVisible"
+                    :bom="kitCheckRow" />
     <!-- 鏂板/缂栬緫寮圭獥 -->
     <el-dialog v-model="dialogVisible"
                :title="operationType === 'add' ? '鏂板BOM' : '缂栬緫BOM'"
@@ -138,11 +141,13 @@
     batchDelete,
     exportBom,
     downloadTemplate,
+    changeStatus,
   } from "@/api/productionManagement/productBom.js";
   import { useRouter } from "vue-router";
   import { ElMessageBox } from "element-plus";
   import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
   import ImportDialog from "@/components/Dialog/ImportDialog.vue";
+  import KitCheckDialog from "@/views/productionManagement/productStructure/KitCheckDialog.vue";
 
   const router = useRouter();
   const { proxy } = getCurrentInstance();
@@ -175,6 +180,14 @@
       width: 100,
     },
     {
+      label: "鐘舵��",
+      prop: "status",
+      dataType: "tag",
+      width: 90,
+      formatData: v => (v == 1 ? "鍚敤" : "鍋滅敤"),
+      formatType: v => (v == 1 ? "success" : "info"),
+    },
+    {
       label: "澶囨敞",
       prop: "remark",
       minWidth: 160,
@@ -184,7 +197,7 @@
       label: "鎿嶄綔",
       align: "center",
       fixed: "right",
-      width: 250,
+      width: 330,
       operation: [
         {
           name: "澶嶅埗",
@@ -198,6 +211,31 @@
           type: "text",
           clickFun: row => {
             handleEdit(row);
+          },
+        },
+        {
+          name: "榻愬鍒嗘瀽",
+          type: "text",
+          clickFun: row => {
+            handleKitCheck(row);
+          },
+        },
+        {
+          name: "鍚敤",
+          type: "text",
+          color: "#67C23A",
+          showHide: row => row.status != 1,
+          clickFun: row => {
+            handleChangeStatus(row, 1);
+          },
+        },
+        {
+          name: "鍋滅敤",
+          type: "text",
+          color: "#909399",
+          showHide: row => row.status == 1,
+          clickFun: row => {
+            handleChangeStatus(row, 0);
           },
         },
         {
@@ -215,6 +253,8 @@
   const tableData = ref([]);
   const tableLoading = ref(false);
   const showEdit = ref(false);
+  const kitCheckVisible = ref(false);
+  const kitCheckRow = ref({});
   const selectedRows = ref([]);
   const currentRow = ref({});
   const dialogVisible = ref(false);
@@ -345,6 +385,34 @@
       .catch(() => {});
   };
 
+  // 鍚敤/鍋滅敤
+  const handleChangeStatus = (row, status) => {
+    const tip =
+      status == 1
+        ? "纭鍚敤璇OM鐗堟湰锛熷悓浜у搧瑙勬牸鐨勫叾浠栧惎鐢ㄧ増鏈皢鑷姩鍋滅敤銆�"
+        : "纭鍋滅敤璇OM鐗堟湰锛�";
+    ElMessageBox.confirm(tip, "鎻愮ず", {
+      confirmButtonText: "纭",
+      cancelButtonText: "鍙栨秷",
+      type: "warning",
+    })
+      .then(() => {
+        changeStatus({ id: row.id, status })
+          .then(() => {
+            proxy.$modal.msgSuccess(status == 1 ? "鍚敤鎴愬姛" : "鍋滅敤鎴愬姛");
+            getList();
+          })
+          .catch(() => {});
+      })
+      .catch(() => {});
+  };
+
+  // 榻愬鍒嗘瀽
+  const handleKitCheck = row => {
+    kitCheckRow.value = row;
+    kitCheckVisible.value = true;
+  };
+
   // 缂栬緫
   const handleEdit = row => {
     operationType.value = "edit";
diff --git a/src/views/productionManagement/productionCosting/index.vue b/src/views/productionManagement/productionCosting/index.vue
index 3e79b93..dfa51a3 100644
--- a/src/views/productionManagement/productionCosting/index.vue
+++ b/src/views/productionManagement/productionCosting/index.vue
@@ -125,6 +125,11 @@
       minWidth: 100,
     },
     {
+      label: "浣跨敤璁惧",
+      prop: "deviceName",
+      minWidth: 100,
+    },
+    {
       label: "宸ユ椂锛坔锛�",
       prop: "workHour",
       minWidth: 100,
@@ -140,6 +145,14 @@
       minWidth: 100,
     },
     {
+      label: "璁¤柂鏂瑰紡",
+      prop: "wageMode",
+      minWidth: 100,
+      dataType: "tag",
+      formatData: v => (v === "璁′欢" ? "璁′欢" : "璁℃椂"),
+      formatType: v => (v === "璁′欢" ? "success" : "info"),
+    },
+    {
       label: "宸ヨ祫",
       prop: "wages",
       minWidth: 100,
diff --git a/src/views/productionManagement/productionReporting/index.vue b/src/views/productionManagement/productionReporting/index.vue
index 28edda2..64e6355 100644
--- a/src/views/productionManagement/productionReporting/index.vue
+++ b/src/views/productionManagement/productionReporting/index.vue
@@ -19,6 +19,19 @@
                     style="width: 200px;"
                     @change="handleQuery" />
         </el-form-item>
+        <el-form-item label="浣跨敤璁惧:">
+          <el-select v-model="searchForm.deviceId"
+                     placeholder="璇烽�夋嫨"
+                     clearable
+                     filterable
+                     style="width: 200px;"
+                     @change="handleQuery">
+            <el-option v-for="item in deviceOptions"
+                       :key="item.id"
+                       :label="item.deviceName"
+                       :value="item.id" />
+          </el-select>
+        </el-form-item>
         <el-form-item>
           <el-button type="primary"
                      @click="handleQuery">鎼滅储</el-button>
@@ -162,6 +175,7 @@
   } from "@/api/productionManagement/productionReporting.js";
   import { productionProductMainListPage } from "@/api/productionManagement/productionProductMain.js";
   import { userListNoPageByTenantId } from "@/api/system/user.js";
+  import { getDeviceLedger } from "@/api/equipmentManagement/ledger.js";
   import InputModal from "@/views/productionManagement/productionReporting/Input.vue";
 
   const data = reactive({
@@ -169,12 +183,14 @@
       nickName: "",
       workOrderNo: "",
       workOrderStatus: "",
+      deviceId: "",
     },
   });
   const { searchForm } = toRefs(data);
   const expandedRowKeys = ref([]);
   const expandData = ref([]);
   const userList = ref([]);
+  const deviceOptions = ref([]);
   const tableColumn = ref([
     {
       label: "鎶ュ伐鍗曞彿",
@@ -195,6 +211,11 @@
       label: "宸ュ簭",
       prop: "process",
       width: 120,
+    },
+    {
+      label: "浣跨敤璁惧",
+      prop: "deviceName",
+      width: 140,
     },
     {
       label: "宸ュ崟缂栧彿",
@@ -459,6 +480,14 @@
   };
   onMounted(() => {
     getList();
+    getDeviceLedger()
+      .then(res => {
+        deviceOptions.value = res.data || [];
+      })
+      .catch(err => {
+        console.error("鑾峰彇璁惧鍒楄〃澶辫触", err);
+        deviceOptions.value = [];
+      });
   });
 </script>
 
diff --git a/src/views/productionManagement/productionTraceability/index.vue b/src/views/productionManagement/productionTraceability/index.vue
index 39db220..ec630bc 100644
--- a/src/views/productionManagement/productionTraceability/index.vue
+++ b/src/views/productionManagement/productionTraceability/index.vue
@@ -166,6 +166,9 @@
             <el-table-column label="鍒涘缓浜�"
                              prop="userName"
                              align="center" />
+            <el-table-column label="浣跨敤璁惧"
+                             prop="deviceName"
+                             align="center" />
             <el-table-column label="鍒涘缓鏃堕棿"
                              align="center">
               <template #default="{ row }">
diff --git a/src/views/productionManagement/workOrder/index.vue b/src/views/productionManagement/workOrder/index.vue
index d3b0f93..9ecf945 100644
--- a/src/views/productionManagement/workOrder/index.vue
+++ b/src/views/productionManagement/workOrder/index.vue
@@ -209,6 +209,18 @@
                        :value="user.userId" />
           </el-select>
         </el-form-item>
+        <el-form-item label="浣跨敤璁惧">
+          <el-select v-model="reportForm.deviceId"
+                     style="width: 300px"
+                     placeholder="璇烽�夋嫨浣跨敤璁惧"
+                     clearable
+                     filterable>
+            <el-option v-for="item in deviceOptions"
+                       :key="item.id"
+                       :label="item.deviceName"
+                       :value="item.id" />
+          </el-select>
+        </el-form-item>
       </el-form>
       <template #footer>
         <span class="dialog-footer">
@@ -233,6 +245,7 @@
     downProductWorkOrder,
   } from "@/api/productionManagement/workOrder.js";
   import { getUserProfile, userListNoPageByTenantId } from "@/api/system/user.js";
+  import { getDeviceLedger } from "@/api/equipmentManagement/ledger.js";
   import QRCode from "qrcode";
   import { getCurrentInstance, reactive, toRefs } from "vue";
   import FilesDia from "./components/filesDia.vue";
@@ -356,6 +369,7 @@
   const workOrderFilesRef = ref(null);
   const reportFormRef = ref(null);
   const userOptions = ref([]);
+  const deviceOptions = ref([]);
   const reportForm = reactive({
     planQuantity: 0,
     quantity: null,
@@ -365,6 +379,7 @@
     reportWork: "",
     productProcessRouteItemId: "",
     userId: "",
+    deviceId: "",
     productMainId: null,
   });
 
@@ -596,6 +611,7 @@
 
   const showReportDialog = row => {
     currentReportRowData.value = row;
+    reportForm.deviceId = "";
     reportForm.planQuantity = row.planQuantity - row.completeQuantity;
     reportForm.quantity =
       row.quantity !== undefined && row.quantity !== null ? row.quantity : null;
@@ -729,9 +745,22 @@
     }
   };
 
+  // 鑾峰彇璁惧涓嬫媺
+  const getDeviceOptions = () => {
+    getDeviceLedger()
+      .then(res => {
+        deviceOptions.value = res.data || [];
+      })
+      .catch(err => {
+        console.error("鑾峰彇璁惧鍒楄〃澶辫触", err);
+        deviceOptions.value = [];
+      });
+  };
+
   onMounted(() => {
     getList();
     getUserList();
+    getDeviceOptions();
   });
 </script>
 
diff --git a/src/views/productionManagement/workOrderManagement/index.vue b/src/views/productionManagement/workOrderManagement/index.vue
index 06b1dd9..723e013 100644
--- a/src/views/productionManagement/workOrderManagement/index.vue
+++ b/src/views/productionManagement/workOrderManagement/index.vue
@@ -168,6 +168,18 @@
                        :value="user.userId" />
           </el-select>
         </el-form-item>
+        <el-form-item label="浣跨敤璁惧">
+          <el-select v-model="reportForm.deviceId"
+                     style="width: 300px"
+                     placeholder="璇烽�夋嫨浣跨敤璁惧"
+                     clearable
+                     filterable>
+            <el-option v-for="item in deviceOptions"
+                       :key="item.id"
+                       :label="item.deviceName"
+                       :value="item.id" />
+          </el-select>
+        </el-form-item>
         <!-- 宸ユ椂 -->
         <el-form-item label="宸ユ椂"
                       v-if="currentReportRowData?.type == 0"
@@ -280,6 +292,7 @@
   import { listMaterialPickingDetail } from "@/api/productionManagement/productionOrder.js";
   import { findProcessParamListOrder } from "@/api/productionManagement/productProcessRoute.js";
   import { getUserProfile, userListNoPageByTenantId } from "@/api/system/user.js";
+  import { getDeviceLedger } from "@/api/equipmentManagement/ledger.js";
   import { getDicts } from "@/api/system/dict/data";
   import QRCode from "qrcode";
   import { getCurrentInstance, reactive, toRefs } from "vue";
@@ -426,6 +439,7 @@
   const currentWorkOrderId = ref(null);
   const reportFormRef = ref(null);
   const userOptions = ref([]);
+  const deviceOptions = ref([]);
   const reportForm = reactive({
     planQuantity: 0,
     quantity: null,
@@ -435,6 +449,7 @@
     reportWork: "",
     productProcessRouteItemId: "",
     userId: "",
+    deviceId: "",
     productMainId: null,
     productionOrderRoutingOperationId: "",
     productionOrderId: "",
@@ -694,6 +709,7 @@
       toQuantity(planQuantity - completeQuantity)
     );
     reportForm.planQuantity = remainingQuantity;
+    reportForm.deviceId = "";
     reportForm.quantity = remainingQuantity;
     reportForm.productProcessRouteItemId = row.productProcessRouteItemId;
     reportForm.workOrderId = row.id;
@@ -801,6 +817,7 @@
         scrapQty: isNaN(scrapQty) ? 0 : scrapQty,
         userId: reportForm.userId,
         userName: reportForm.userName,
+        deviceId: reportForm.deviceId || undefined,
         productionOperationTaskId: reportForm.workOrderId,
         productProcessRouteItemId: reportForm.productProcessRouteItemId,
         reportWork: reportForm.reportWork,
@@ -958,6 +975,15 @@
         userOptions.value = res.data;
       }
     });
+    // 鑾峰彇璁惧鍒楄〃
+    getDeviceLedger()
+      .then(res => {
+        deviceOptions.value = res.data || [];
+      })
+      .catch(err => {
+        console.error("鑾峰彇璁惧鍒楄〃澶辫触", err);
+        deviceOptions.value = [];
+      });
   });
 </script>
 
diff --git a/src/views/qualityManagement/finalInspection/components/formDia.vue b/src/views/qualityManagement/finalInspection/components/formDia.vue
index 94801ff..bc68189 100644
--- a/src/views/qualityManagement/finalInspection/components/formDia.vue
+++ b/src/views/qualityManagement/finalInspection/components/formDia.vue
@@ -301,6 +301,10 @@
   // 鎵撳紑寮规
   const openDialog = async (type, row) => {
     operationType.value = type;
+    // 宸叉彁浜ょ殑妫�楠屽崟寮哄埗鍙
+    if (row?.inspectState === 1) {
+      operationType.value = "view";
+    }
     dialogFormVisible.value = true;
     // 鍏堟竻绌鸿〃鍗曢獙璇佺姸鎬侊紝閬垮厤闂儊
     await nextTick();
diff --git a/src/views/qualityManagement/finalInspection/index.vue b/src/views/qualityManagement/finalInspection/index.vue
index f08ba37..6ffacc5 100644
--- a/src/views/qualityManagement/finalInspection/index.vue
+++ b/src/views/qualityManagement/finalInspection/index.vue
@@ -487,11 +487,23 @@
 
   // 鎻愪环
   const submit = async id => {
-    const res = await submitQualityInspect({ id: id });
-    if (res.code === 200) {
-      proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
-      getList();
-    }
+    ElMessageBox.confirm(
+      "鎻愪氦鍚庡皢鐢熸垚鍏ュ簱璁板綍涓斾笉鍙慨鏀癸紝纭鎻愪氦锛�",
+      "鎻愮ず",
+      { confirmButtonText: "纭", cancelButtonText: "鍙栨秷", type: "warning" }
+    )
+      .then(async () => {
+        const res = await submitQualityInspect({ id: id });
+        if (res.code === 200) {
+          proxy.$modal.msgSuccess(
+            "鎻愪氦鎴愬姛锛屽悎鏍奸儴鍒嗗凡鐢熸垚鍏ュ簱璁板綍锛堝緟瀹℃牳锛夛紝涓嶅悎鏍奸儴鍒嗗凡鐢熸垚澶勭悊鍗�"
+          );
+          getList();
+        }
+      })
+      .catch(() => {
+        proxy.$modal.msg("宸插彇娑�");
+      });
   };
 
   const handleBatchSubmit = () => {
diff --git a/src/views/qualityManagement/nonconformingManagement/components/formDia.vue b/src/views/qualityManagement/nonconformingManagement/components/formDia.vue
index 5e20709..c291ebe 100644
--- a/src/views/qualityManagement/nonconformingManagement/components/formDia.vue
+++ b/src/views/qualityManagement/nonconformingManagement/components/formDia.vue
@@ -124,10 +124,19 @@
                          clearable>
                 <el-option :label="item.label"
                            :value="item.value"
-                           v-for="item in rejection_handling"
+                           v-for="item in filteredRejectionHandling"
                            :key="item.value" />
               </el-select>
             </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row v-if="isReworkResult">
+          <el-col :span="24">
+            <el-alert title="鎻愪氦鍚庡皢鑷姩鍒涘缓杩斿伐鐢熶骇璁㈠崟锛團G鍗曞彿锛�"
+                      type="info"
+                      :closable="false"
+                      show-icon
+                      style="margin-bottom: 12px" />
           </el-col>
         </el-row>
         <el-row :gutter="30">
@@ -224,6 +233,27 @@
   const modelOptions = ref([]);
   const userList = ref([]); // 妫�楠屽憳/澶勭悊浜轰笅鎷夊垪琛�
 
+  // 鍘熸枡绫伙紙inspectType=0锛変笉鍚堟牸鍙厑璁�"鎶ュ簾/璁╂鏀捐"
+  const filteredRejectionHandling = computed(() => {
+    const data = rejection_handling.value || [];
+    if (form.value.inspectType === 0 || form.value.inspectType === "0") {
+      return data.filter(
+        item => item && (item.label === "鎶ュ簾" || item.label === "璁╂鏀捐")
+      );
+    }
+    return data;
+  });
+
+  // 閫変腑"杩斿伐/杩斾慨"鏃剁殑鎻愮ず
+  const isReworkResult = computed(() => {
+    const item = (rejection_handling.value || []).find(
+      i => i.value === form.value.dealResult
+    );
+    const label = item ? item.label : form.value.dealResult;
+    return label === "杩斿伐" || label === "杩斾慨";
+  });
+
+
   // 鎵撳紑寮规
   const openDialog = async (type, row) => {
     operationType.value = type;
diff --git a/src/views/qualityManagement/nonconformingManagement/components/inspectionFormDia.vue b/src/views/qualityManagement/nonconformingManagement/components/inspectionFormDia.vue
index 8f4492a..dc6c749 100644
--- a/src/views/qualityManagement/nonconformingManagement/components/inspectionFormDia.vue
+++ b/src/views/qualityManagement/nonconformingManagement/components/inspectionFormDia.vue
@@ -87,6 +87,17 @@
             </el-form-item>
           </el-col>
         </el-row>
+        <el-row v-if="isReworkResult">
+          <el-col :span="24">
+            <el-alert
+                title="鎻愪氦鍚庡皢鑷姩鍒涘缓杩斿伐鐢熶骇璁㈠崟锛團G鍗曞彿锛�"
+                type="info"
+                :closable="false"
+                show-icon
+                style="margin-bottom: 12px"
+            />
+          </el-col>
+        </el-row>
         <el-row :gutter="30">
           <el-col :span="12">
             <el-form-item label="澶勭悊浜猴細" prop="dealName">
@@ -174,13 +185,24 @@
 const userList = ref([]); // 澶勭悊浜轰笅鎷夊垪琛�
 
 const filteredRejectionHandling = computed(() => {
-  const data = rejection_handling.value;
+  let data = rejection_handling.value;
+  // 鍘熸枡绫讳笉鍚堟牸鍙厑璁�"鎶ュ簾/璁╂鏀捐"
+  if (form.value.inspectType === 0 || form.value.inspectType === "0") {
+    data = data.filter(item => item && (item.label === '鎶ュ簾' || item.label === '璁╂鏀捐'));
+  }
   if (form.value.method) {
     return data.filter(item => item && item.label && item.label !== '杩斿伐' && item.label !== '杩斾慨')
   }
   return data
 })
 
+// 閫変腑"杩斿伐/杩斾慨"鏃剁殑鎻愮ず
+const isReworkResult = computed(() => {
+  const item = (rejection_handling.value || []).find(i => i.value === form.value.dealResult);
+  const label = item ? item.label : form.value.dealResult;
+  return label === '杩斿伐' || label === '杩斾慨';
+})
+
 
 // 鎵撳紑寮规
 const openDialog = async (type, row) => {
diff --git a/src/views/qualityManagement/processInspection/components/formDia.vue b/src/views/qualityManagement/processInspection/components/formDia.vue
index 370b5f6..ef9f2f8 100644
--- a/src/views/qualityManagement/processInspection/components/formDia.vue
+++ b/src/views/qualityManagement/processInspection/components/formDia.vue
@@ -319,6 +319,10 @@
   // 鎵撳紑寮规
   const openDialog = async (type, row) => {
     operationType.value = type;
+    // 宸叉彁浜ょ殑妫�楠屽崟寮哄埗鍙
+    if (row?.inspectState === 1) {
+      operationType.value = "view";
+    }
     getOptions().then(res => {
       supplierList.value = res.data;
     });
diff --git a/src/views/qualityManagement/processInspection/index.vue b/src/views/qualityManagement/processInspection/index.vue
index 7cecaa2..b77d097 100644
--- a/src/views/qualityManagement/processInspection/index.vue
+++ b/src/views/qualityManagement/processInspection/index.vue
@@ -401,11 +401,23 @@
   };
   // 鎻愪环
   const submit = async id => {
-    const res = await submitQualityInspect({ id: id });
-    if (res.code === 200) {
-      proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
-      getList();
-    }
+    ElMessageBox.confirm(
+      "鎻愪氦鍚庡皢鐢熸垚鍏ュ簱璁板綍涓斾笉鍙慨鏀癸紝纭鎻愪氦锛�",
+      "鎻愮ず",
+      { confirmButtonText: "纭", cancelButtonText: "鍙栨秷", type: "warning" }
+    )
+      .then(async () => {
+        const res = await submitQualityInspect({ id: id });
+        if (res.code === 200) {
+          proxy.$modal.msgSuccess(
+            "鎻愪氦鎴愬姛锛屽悎鏍奸儴鍒嗗凡鐢熸垚鍏ュ簱璁板綍锛堝緟瀹℃牳锛夛紝涓嶅悎鏍奸儴鍒嗗凡鐢熸垚澶勭悊鍗�"
+          );
+          getList();
+        }
+      })
+      .catch(() => {
+        proxy.$modal.msg("宸插彇娑�");
+      });
   };
 
   const handleBatchSubmit = () => {
diff --git a/src/views/qualityManagement/qualityTraceability/components/ForwardResult.vue b/src/views/qualityManagement/qualityTraceability/components/ForwardResult.vue
new file mode 100644
index 0000000..56ead8c
--- /dev/null
+++ b/src/views/qualityManagement/qualityTraceability/components/ForwardResult.vue
@@ -0,0 +1,432 @@
+<template>
+  <div v-loading="loading"
+       class="forward-result">
+    <!-- 璁㈠崟鍗� -->
+    <div v-for="chain in data.chains"
+         :key="chain.productionOrderId"
+         class="order-card">
+      <div class="order-head">
+        <div class="order-title">
+          <el-link type="primary"
+                   @click="$emit('goto-order', chain.npsNo)">{{ chain.npsNo }}</el-link>
+          <span class="product-name">{{ chain.finishedProduct || '-' }}</span>
+        </div>
+        <el-tag :type="statusType(chain.orderStatus)">{{ statusText(chain.orderStatus) }}</el-tag>
+      </div>
+
+      <div class="order-meta">
+        <span>璁″垝鏁伴噺锛歿{ chain.planQty ?? '-' }} {{ chain.unit || '' }}</span>
+        <span>瀹屽伐鏁伴噺锛歿{ chain.completeQty ?? '-' }} {{ chain.unit || '' }}</span>
+        <span>涓嬪崟鏃堕棿锛歿{ chain.orderCreateTime || '-' }}</span>
+      </div>
+
+      <div v-if="chain.batches && chain.batches.length"
+           class="hit-batches">
+        <span class="hit-label">鍛戒腑鍏ュ簱鎵规锛�</span>
+        <el-tag v-for="batch in chain.batches"
+                :key="batch.batchNo"
+                type="success"
+                effect="plain">
+          {{ batch.batchNo }}锛坽{ batch.qty }} {{ chain.unit || '' }}锛寋{ batch.inTime }}锛寋{ recordTypeLabel(batch.recordType) }}锛�
+        </el-tag>
+      </div>
+
+      <el-timeline class="order-timeline">
+        <!-- 宸ュ簭鎶ュ伐 -->
+        <el-timeline-item v-for="report in sortReports(chain.reports)"
+                          :key="report.productMainId + '_' + report.workOrderNo"
+                          :timestamp="report.reportTime"
+                          type="primary"
+                          placement="top">
+          <div class="node-title">宸ュ簭鎶ュ伐锛歿{ report.operationName || '-' }}</div>
+          <el-descriptions :column="3"
+                           border
+                           size="small">
+            <el-descriptions-item label="宸ュ崟缂栧彿">{{ report.workOrderNo || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="鎶ュ伐鍗曞彿">{{ report.productNo || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="鎶ュ伐浜�">{{ report.userName || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="璁惧">{{ report.deviceName || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="浜у嚭鏁伴噺">{{ report.outputQty ?? '-' }}</el-descriptions-item>
+            <el-descriptions-item label="鎶ュ簾鏁伴噺">{{ report.scrapQty ?? '-' }}</el-descriptions-item>
+          </el-descriptions>
+
+          <el-collapse class="input-collapse"
+                       :model-value="expandedInputs[reportKey(chain, report)] || []"
+                       @update:model-value="val => (expandedInputs[reportKey(chain, report)] = val)">
+            <el-collapse-item name="inputs"
+                              :title="`鎶曞叆鐗╂枡锛�${inputsFor(chain, report).length}锛塦">
+              <el-table :data="inputsFor(chain, report)"
+                        border
+                        size="small">
+                <el-table-column label="鐗╂枡鍚嶇О"
+                                 prop="materialName"
+                                 align="center" />
+                <el-table-column label="鐗╂枡瑙勬牸"
+                                 prop="materialModel"
+                                 align="center" />
+                <el-table-column label="鎶曞叆鏁伴噺"
+                                 prop="inputQty"
+                                 align="center" />
+                <el-table-column label="鍗曚綅"
+                                 prop="unit"
+                                 align="center" />
+              </el-table>
+            </el-collapse-item>
+          </el-collapse>
+        </el-timeline-item>
+
+        <!-- 棰嗘枡鎵规锛堣鍗曠骇锛� -->
+        <el-timeline-item v-if="chain.picks && chain.picks.length"
+                          type="warning"
+                          placement="top">
+          <div class="node-title">棰嗘枡鎵规</div>
+          <el-table :data="chain.picks"
+                    border
+                    size="small">
+            <el-table-column label="鍘熸枡鎵瑰彿"
+                             align="center"
+                             min-width="200">
+              <template #default="{ row }">
+                <el-link type="primary"
+                         @click="$emit('pick-batch', row.batchNo)">{{ row.batchNo }}</el-link>
+              </template>
+            </el-table-column>
+            <el-table-column label="鐗╂枡"
+                             prop="materialName"
+                             align="center" />
+            <el-table-column label="鐗╂枡瑙勬牸"
+                             prop="materialModel"
+                             align="center" />
+            <el-table-column label="棰嗘枡鏁伴噺"
+                             align="center">
+              <template #default="{ row }">{{ row.quantity }} {{ row.unit || '' }}</template>
+            </el-table-column>
+            <el-table-column label="宸ュ簭"
+                             prop="operationName"
+                             align="center" />
+            <el-table-column label="棰嗘枡鏃堕棿"
+                             prop="pickTime"
+                             align="center" />
+            <el-table-column label="渚涘簲鍟�"
+                             align="center">
+              <template #default="{ row }">{{ row.supplierName || '-' }}</template>
+            </el-table-column>
+            <el-table-column label="閲囪喘鍚堝悓鍙�"
+                             align="center">
+              <template #default="{ row }">{{ row.contractNo || '-' }}</template>
+            </el-table-column>
+            <el-table-column label="鍘熸枡妫�楠岀粨璁�"
+                             align="center">
+              <template #default="{ row }">{{ row.checkResult || '-' }}</template>
+            </el-table-column>
+          </el-table>
+        </el-timeline-item>
+
+        <!-- 妫�楠屽崟 -->
+        <el-timeline-item v-if="chain.inspects && chain.inspects.length"
+                          type="success"
+                          placement="top">
+          <div class="node-title">妫�楠屽崟</div>
+          <el-table :data="chain.inspects"
+                    border
+                    size="small">
+            <el-table-column label="妫�楠岀被鍨�"
+                             prop="inspectTypeName"
+                             align="center" />
+            <el-table-column label="宸ュ簭"
+                             align="center">
+              <template #default="{ row }">{{ row.process || '-' }}</template>
+            </el-table-column>
+            <el-table-column label="妫�楠屽憳"
+                             prop="checkName"
+                             align="center" />
+            <el-table-column label="浜у搧鍚嶇О"
+                             prop="productName"
+                             align="center" />
+            <el-table-column label="瑙勬牸鍨嬪彿"
+                             prop="model"
+                             align="center" />
+            <el-table-column label="鏁伴噺"
+                             prop="quantity"
+                             align="center" />
+            <el-table-column label="鍚堟牸鏁伴噺"
+                             prop="qualifiedQuantity"
+                             align="center" />
+            <el-table-column label="涓嶅悎鏍兼暟閲�"
+                             prop="unqualifiedQuantity"
+                             align="center" />
+            <el-table-column label="妫�娴嬬粨鏋�"
+                             align="center">
+              <template #default="{ row }">
+                <el-tag :type="row.checkResult === '鍚堟牸' ? 'success' : 'danger'">
+                  {{ row.checkResult || '寰呮娴�' }}
+                </el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column label="妫�娴嬫棩鏈�"
+                             prop="checkTime"
+                             align="center" />
+            <el-table-column label="鎿嶄綔"
+                             align="center"
+                             width="100">
+              <template #default="{ row }">
+                <el-link type="primary"
+                         @click="openInspect(chain, row)">鏌ョ湅璇︽儏</el-link>
+              </template>
+            </el-table-column>
+          </el-table>
+        </el-timeline-item>
+      </el-timeline>
+    </div>
+
+    <!-- 鍑哄簱娴佸悜锛堥《灞傦紝鎸夋壒鍙疯仛鍚堬級 -->
+    <div v-if="data.outbounds && data.outbounds.length"
+         class="order-card">
+      <div class="order-head">
+        <div class="order-title">
+          <span class="product-name">鍑哄簱娴佸悜</span>
+        </div>
+      </div>
+      <el-table :data="data.outbounds"
+                border
+                size="small">
+        <el-table-column label="鎵瑰彿"
+                         prop="batchNo"
+                         align="center" />
+        <el-table-column label="浜у搧鍚嶇О"
+                         prop="productName"
+                         align="center" />
+        <el-table-column label="瑙勬牸鍨嬪彿"
+                         prop="productModel"
+                         align="center" />
+        <el-table-column label="鍑哄簱鏁伴噺"
+                         align="center">
+          <template #default="{ row }">{{ row.qty }} {{ row.unit || '' }}</template>
+        </el-table-column>
+        <el-table-column label="鍑哄簱绫诲瀷"
+                         align="center">
+          <template #default="{ row }">{{ recordTypeLabel(row.recordType) }}</template>
+        </el-table-column>
+        <el-table-column label="鍑哄簱鏃堕棿"
+                         prop="outTime"
+                         align="center" />
+        <el-table-column label="澶囨敞"
+                         align="center">
+          <template #default="{ row }">{{ row.remark || '-' }}</template>
+        </el-table-column>
+      </el-table>
+    </div>
+
+    <!-- 妫�楠屽崟璇︽儏 -->
+    <el-dialog v-model="inspectVisible"
+               title="妫�楠屽崟璇︽儏"
+               width="900px">
+      <div v-if="currentInspect"
+           class="inspect-detail">
+        <el-descriptions :column="3"
+                         border
+                         size="small">
+          <el-descriptions-item label="妫�楠岀被鍨�">{{ currentInspect.inspectTypeName || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="宸ュ簭">{{ currentInspect.process || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="妫�楠屽憳">{{ currentInspect.checkName || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="浜у搧鍚嶇О">{{ currentInspect.productName || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="瑙勬牸鍨嬪彿">{{ currentInspect.model || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="鍗曚綅">{{ currentInspect.unit || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="鏁伴噺">{{ currentInspect.quantity ?? '-' }}</el-descriptions-item>
+          <el-descriptions-item label="鍚堟牸鏁伴噺">{{ currentInspect.qualifiedQuantity ?? '-' }}</el-descriptions-item>
+          <el-descriptions-item label="涓嶅悎鏍兼暟閲�">{{ currentInspect.unqualifiedQuantity ?? '-' }}</el-descriptions-item>
+          <el-descriptions-item label="妫�娴嬬粨鏋�">
+            <el-tag :type="currentInspect.checkResult === '鍚堟牸' ? 'success' : 'danger'">
+              {{ currentInspect.checkResult || '寰呮娴�' }}
+            </el-tag>
+          </el-descriptions-item>
+          <el-descriptions-item label="妫�娴嬫棩鏈�">{{ currentInspect.checkTime || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="涓嶅悎鏍肩幇璞�">{{ currentInspect.defectivePhenomena || '-' }}</el-descriptions-item>
+        </el-descriptions>
+
+        <h4 class="sub-title">涓嶅悎鏍煎鐞�</h4>
+        <el-table :data="currentUnqualified"
+                  border
+                  size="small">
+          <el-table-column label="搴忓彿"
+                           type="index"
+                           width="60"
+                           align="center" />
+          <el-table-column label="浜у搧鍚嶇О"
+                           prop="productName"
+                           align="center" />
+          <el-table-column label="瑙勬牸鍨嬪彿"
+                           prop="model"
+                           align="center" />
+          <el-table-column label="鏁伴噺"
+                           prop="quantity"
+                           align="center" />
+          <el-table-column label="涓嶅悎鏍肩幇璞�"
+                           prop="defectivePhenomena"
+                           align="center" />
+          <el-table-column label="澶勭悊缁撴灉"
+                           prop="dealResult"
+                           align="center" />
+          <el-table-column label="澶勭悊浜�"
+                           prop="dealName"
+                           align="center" />
+          <el-table-column label="澶勭悊鏃ユ湡"
+                           prop="dealTime"
+                           align="center" />
+        </el-table>
+      </div>
+      <template #footer>
+        <el-button @click="inspectVisible = false">鍏抽棴</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+  import { ref, reactive } from "vue";
+
+  defineProps({
+    data: {
+      type: Object,
+      required: true,
+    },
+    loading: {
+      type: Boolean,
+      default: false,
+    },
+  });
+
+  defineEmits(["goto-order", "pick-batch"]);
+
+  const statusTypeMap = { 1: "primary", 2: "warning", 3: "success", 5: "danger" };
+  const statusTextMap = { 1: "寰呭紑濮�", 2: "杩涜涓�", 3: "宸插畬鎴�", 5: "宸茬粨鏉�" };
+  const statusType = status => statusTypeMap[status] || "info";
+  const statusText = status => statusTextMap[status] || "宸插彇娑�";
+
+  const inTypeMap = {
+    0: "鑷畾涔夊叆搴�",
+    2: "鐢熶骇鎶ュ伐鍏ュ簱",
+    6: "璐ㄦ鍚堟牸鍏ュ簱",
+    7: "閲囪喘鍏ュ簱",
+    10: "閲囪喘鍏ュ簱",
+  };
+  const outTypeMap = {
+    1: "鑷畾涔夊嚭搴�",
+    3: "鐢熶骇鎶ュ伐鍑哄簱",
+    8: "閿�鍞嚭搴�",
+    13: "閿�鍞彂璐у嚭搴�",
+  };
+  const recordTypeLabel = type => {
+    const key = String(type ?? "");
+    return inTypeMap[key] || outTypeMap[key] || key || "-";
+  };
+
+  const sortReports = reports =>
+    [...(reports || [])].sort((a, b) =>
+      String(a.reportTime || "").localeCompare(String(b.reportTime || ""))
+    );
+
+  const inputsFor = (chain, report) =>
+    (chain.inputs || []).filter(
+      item => String(item.productMainId) === String(report.productMainId)
+    );
+
+  const reportKey = (chain, report) =>
+    `${chain.productionOrderId}_${report.productMainId}_${report.workOrderNo}`;
+  const expandedInputs = reactive({});
+
+  // 妫�楠屽崟璇︽儏
+  const inspectVisible = ref(false);
+  const currentInspect = ref(null);
+  const currentUnqualified = ref([]);
+  const openInspect = (chain, inspect) => {
+    currentInspect.value = inspect;
+    const own = inspect.unqualifiedList || [];
+    currentUnqualified.value = own.length
+      ? own
+      : (chain.unqualifiedList || []).filter(
+          item => String(item.inspectId) === String(inspect.inspectId)
+        );
+    inspectVisible.value = true;
+  };
+</script>
+
+<style scoped lang="scss">
+  .forward-result {
+    min-height: 200px;
+  }
+
+  .order-card {
+    background-color: #ffffff;
+    border-radius: 10px;
+    padding: 20px;
+    margin-bottom: 20px;
+    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.08);
+  }
+
+  .order-head {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding-bottom: 12px;
+    border-bottom: 1px solid #ebeef5;
+  }
+
+  .order-title {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    font-size: 16px;
+  }
+
+  .product-name {
+    font-weight: 600;
+    color: #1a1a1a;
+  }
+
+  .order-meta {
+    display: flex;
+    gap: 28px;
+    padding: 12px 0;
+    font-size: 13px;
+    color: #606266;
+  }
+
+  .hit-batches {
+    display: flex;
+    flex-wrap: wrap;
+    align-items: center;
+    gap: 8px;
+    padding: 8px 12px;
+    background-color: #f0f9eb;
+    border-radius: 6px;
+
+    .hit-label {
+      font-size: 13px;
+      color: #606266;
+    }
+  }
+
+  .order-timeline {
+    margin-top: 20px;
+    padding-left: 4px;
+  }
+
+  .node-title {
+    font-size: 14px;
+    font-weight: 600;
+    color: #303133;
+    margin-bottom: 12px;
+  }
+
+  .input-collapse {
+    margin-top: 12px;
+  }
+
+  .sub-title {
+    margin: 20px 0 12px;
+    font-size: 14px;
+    font-weight: 600;
+    color: #303133;
+  }
+</style>
diff --git a/src/views/qualityManagement/qualityTraceability/components/ReverseTable.vue b/src/views/qualityManagement/qualityTraceability/components/ReverseTable.vue
new file mode 100644
index 0000000..b0f9f8f
--- /dev/null
+++ b/src/views/qualityManagement/qualityTraceability/components/ReverseTable.vue
@@ -0,0 +1,171 @@
+<template>
+  <div v-loading="loading"
+       class="reverse-result">
+    <!-- 鍘熸枡鏉ユ簮 -->
+    <div class="material-card">
+      <h3 class="section-title">鍘熸枡鏉ユ簮</h3>
+      <el-descriptions v-if="data.material"
+                       :column="4"
+                       border
+                       size="small">
+        <el-descriptions-item label="鍘熸枡鎵瑰彿">{{ data.material.batchNo || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="鐗╂枡">{{ data.material.materialName || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="鐗╂枡瑙勬牸">{{ data.material.materialModel || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="鍏ュ簱绫诲瀷">{{ recordTypeLabel(data.material.recordType) }}</el-descriptions-item>
+        <el-descriptions-item label="鍏ュ簱鏁伴噺">{{ data.material.qty ?? '-' }} {{ data.material.unit || '' }}</el-descriptions-item>
+        <el-descriptions-item label="鍏ュ簱鏃堕棿">{{ data.material.inTime || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="渚涘簲鍟�">{{ data.material.supplierName || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="閲囪喘鍚堝悓鍙�">{{ data.material.contractNo || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="閲囪喘鏃ユ湡">{{ data.material.entryDate || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="妫�楠屾暟閲�">{{ data.material.inspectQty ?? '-' }}</el-descriptions-item>
+        <el-descriptions-item label="鍚堟牸鏁伴噺">{{ data.material.qualifiedQty ?? '-' }}</el-descriptions-item>
+        <el-descriptions-item label="涓嶅悎鏍兼暟閲�">{{ data.material.unqualifiedQty ?? '-' }}</el-descriptions-item>
+        <el-descriptions-item label="妫�楠岀粨璁�">{{ data.material.checkResult || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="妫�楠屾棩鏈�">{{ data.material.checkTime || '-' }}</el-descriptions-item>
+      </el-descriptions>
+      <el-alert v-else
+                type="info"
+                :closable="false"
+                show-icon
+                description="璇ユ壒鍙锋棤鍏ュ簱/閲囪喘鏉ユ簮璁板綍" />
+    </div>
+
+    <!-- 鍙楀奖鍝嶈鍗� -->
+    <div class="order-card">
+      <h3 class="section-title">鍙楀奖鍝嶇敓浜ц鍗曪紙{{ data.orders.length }}锛�</h3>
+      <PIMTable rowKey="productionOrderId"
+                :column="tableColumn"
+                :tableData="data.orders"
+                :page="{ total: data.orders.length }"
+                :isShowPagination="false"
+                height="520"
+                :tableLoading="loading">
+        <template #npsNo="{ row }">
+          <el-link type="primary"
+                   @click="$emit('goto-order', row.npsNo)">{{ row.npsNo }}</el-link>
+        </template>
+        <template #orderStatus="{ row }">
+          <el-tag :type="statusType(row.orderStatus)">{{ statusText(row.orderStatus) }}</el-tag>
+        </template>
+        <template #finishedBatches="{ row }">
+          <template v-if="row.finishedBatches && row.finishedBatches.length">
+            <el-tag v-for="batch in row.finishedBatches"
+                    :key="batch.batchNo"
+                    class="batch-tag"
+                    @click="$emit('goto-forward', batch.batchNo)">{{ batch.batchNo }}</el-tag>
+          </template>
+          <span v-else>-</span>
+        </template>
+      </PIMTable>
+    </div>
+  </div>
+</template>
+
+<script setup>
+  import PIMTable from "@/components/PIMTable/PIMTable.vue";
+
+  defineProps({
+    data: {
+      type: Object,
+      required: true,
+    },
+    loading: {
+      type: Boolean,
+      default: false,
+    },
+  });
+
+  defineEmits(["goto-order", "goto-forward"]);
+
+  const tableColumn = [
+    {
+      label: "鐢熶骇璁㈠崟鍙�",
+      prop: "npsNo",
+      dataType: "slot",
+      slot: "npsNo",
+      minWidth: 160,
+    },
+    {
+      label: "鎴愬搧",
+      prop: "finishedProduct",
+      minWidth: 220,
+    },
+    {
+      label: "璁″垝鏁伴噺",
+      prop: "planQty",
+      width: 100,
+    },
+    {
+      label: "瀹屽伐鏁伴噺",
+      prop: "completeQty",
+      width: 100,
+    },
+    {
+      label: "璁㈠崟鐘舵��",
+      prop: "orderStatus",
+      dataType: "slot",
+      slot: "orderStatus",
+      width: 100,
+    },
+    {
+      label: "棰嗘枡鏁伴噺",
+      prop: "pickedQty",
+      width: 110,
+    },
+    {
+      label: "浜у嚭鎴愬搧鎵瑰彿",
+      prop: "finishedBatches",
+      dataType: "slot",
+      slot: "finishedBatches",
+      minWidth: 260,
+    },
+  ];
+
+  const statusTypeMap = { 1: "primary", 2: "warning", 3: "success", 5: "danger" };
+  const statusTextMap = { 1: "寰呭紑濮�", 2: "杩涜涓�", 3: "宸插畬鎴�", 5: "宸茬粨鏉�" };
+  const statusType = status => statusTypeMap[status] || "info";
+  const statusText = status => statusTextMap[status] || "宸插彇娑�";
+
+  const inTypeMap = {
+    0: "鑷畾涔夊叆搴�",
+    2: "鐢熶骇鎶ュ伐鍏ュ簱",
+    6: "璐ㄦ鍚堟牸鍏ュ簱",
+    7: "閲囪喘鍏ュ簱",
+    10: "閲囪喘鍏ュ簱",
+  };
+  const outTypeMap = {
+    1: "鑷畾涔夊嚭搴�",
+    3: "鐢熶骇鎶ュ伐鍑哄簱",
+    8: "閿�鍞嚭搴�",
+    13: "閿�鍞彂璐у嚭搴�",
+  };
+  const recordTypeLabel = type => {
+    const key = String(type ?? "");
+    return inTypeMap[key] || outTypeMap[key] || key || "-";
+  };
+</script>
+
+<style scoped lang="scss">
+  .material-card,
+  .order-card {
+    background-color: #ffffff;
+    border-radius: 10px;
+    padding: 20px;
+    margin-bottom: 20px;
+    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.08);
+  }
+
+  .section-title {
+    font-size: 16px;
+    font-weight: 600;
+    margin-bottom: 16px;
+    color: #1a1a1a;
+    border-bottom: 2px solid #409eff;
+    padding-bottom: 10px;
+  }
+
+  .batch-tag {
+    margin: 2px 6px 2px 0;
+    cursor: pointer;
+  }
+</style>
diff --git a/src/views/qualityManagement/qualityTraceability/index.vue b/src/views/qualityManagement/qualityTraceability/index.vue
new file mode 100644
index 0000000..f9fd777
--- /dev/null
+++ b/src/views/qualityManagement/qualityTraceability/index.vue
@@ -0,0 +1,305 @@
+<template>
+  <div class="app-container">
+    <PageHeader content="璐ㄩ噺杩芥函" />
+    <el-card class="trace-card">
+      <el-tabs v-model="activeTab"
+               @tab-change="handleTabChange">
+        <el-tab-pane label="姝e悜杩芥函锛堟垚鍝� 鈫� 鍘熸枡锛�"
+                     name="forward" />
+        <el-tab-pane label="鍙嶅悜杩芥函锛堝師鏂� 鈫� 鎴愬搧锛�"
+                     name="reverse" />
+      </el-tabs>
+
+      <div class="search-bar">
+        <el-select v-model="batchNo"
+                   filterable
+                   remote
+                   reserve-keyword
+                   clearable
+                   :placeholder="placeholder"
+                   :loading="batchLoading"
+                   :remote-method="handleBatchSearch"
+                   style="width: 460px;"
+                   @change="handleBatchChange">
+          <el-option v-for="option in batchOpts"
+                     :key="option.batchNo"
+                     :value="option.batchNo"
+                     :label="optionLabel(option)" />
+          <template #footer>
+            <div v-if="batchHasMore"
+                 class="select-load-more"
+                 @click="loadMoreBatch">
+              <span v-if="batchLoading">鍔犺浇涓�...</span>
+              <span v-else>鐐瑰嚮鍔犺浇鏇村</span>
+            </div>
+            <div v-else-if="batchOpts.length > 0"
+                 class="select-load-more no-more">
+              娌℃湁鏇村浜�
+            </div>
+          </template>
+        </el-select>
+        <el-button type="primary"
+                   :loading="loading"
+                   @click="handleQuery">鏌ヨ</el-button>
+        <el-button :loading="exportLoading"
+                   @click="handleExport">瀵煎嚭</el-button>
+      </div>
+
+      <el-empty v-if="!queried"
+                description="璇疯緭鍏ユ垨閫夋嫨鎵瑰彿鍚庣偣鍑绘煡璇�" />
+      <el-empty v-else-if="isEmptyResult"
+                description="鏈煡璇㈠埌璇ユ壒鍙风殑杩芥函璁板綍" />
+      <ForwardResult v-else-if="isForward"
+                     :data="forwardData"
+                     :loading="loading"
+                     @goto-order="gotoOrder"
+                     @pick-batch="goReverse" />
+      <ReverseTable v-else
+                    :data="reverseData"
+                    :loading="loading"
+                    @goto-order="gotoOrder"
+                    @goto-forward="goForward" />
+    </el-card>
+  </div>
+</template>
+
+<script setup>
+  import { ref, reactive, computed, onMounted } from "vue";
+  import { ElMessage } from "element-plus";
+  import { useRouter } from "vue-router";
+  import { saveAs } from "file-saver";
+  import { blobValidate } from "@/utils/ruoyi";
+  import ForwardResult from "./components/ForwardResult.vue";
+  import ReverseTable from "./components/ReverseTable.vue";
+  import {
+    forwardTrace,
+    reverseTrace,
+    batchOptions,
+    forwardExport,
+    reverseExport,
+  } from "@/api/qualityManagement/qualityTrace";
+
+  const router = useRouter();
+
+  const BATCH_PAGE = 50;
+  const BATCH_MAX = 200;
+
+  const activeTab = ref("forward");
+  const isForward = computed(() => activeTab.value === "forward");
+  const batchType = computed(() => (isForward.value ? "finished" : "material"));
+  const placeholder = computed(() =>
+    isForward.value ? "璇疯緭鍏ユ垚鍝佹壒鍙锋悳绱�" : "璇疯緭鍏ュ師鏂欐壒鍙锋悳绱�"
+  );
+
+  // 鎵瑰彿涓嬫媺
+  const batchNo = ref("");
+  const batchOpts = ref([]);
+  const batchLoading = ref(false);
+  const batchKeyword = ref("");
+  const batchLimit = ref(BATCH_PAGE);
+  const batchHasMore = computed(
+    () => batchOpts.value.length >= batchLimit.value && batchLimit.value < BATCH_MAX
+  );
+
+  const loading = ref(false);
+  const exportLoading = ref(false);
+  const queried = ref(false);
+
+  const forwardData = reactive({ batchNo: "", chains: [], outbounds: [] });
+  const reverseData = reactive({ batchNo: "", material: null, orders: [] });
+
+  const isEmptyResult = computed(() => {
+    if (!queried.value) return false;
+    return isForward.value
+      ? forwardData.chains.length === 0 && forwardData.outbounds.length === 0
+      : !reverseData.material && reverseData.orders.length === 0;
+  });
+
+  const optionLabel = option =>
+    [option.batchNo, option.productName, option.productModel]
+      .filter(Boolean)
+      .join(" | ");
+
+  // 鎺ュ彛鏃� offset锛屽彧鑳芥斁澶� limit 鍚庢暣浣撻噸鍙�
+  const fetchBatchOptions = async () => {
+    batchLoading.value = true;
+    try {
+      const res = await batchOptions(
+        batchType.value,
+        batchKeyword.value,
+        batchLimit.value
+      );
+      batchOpts.value = res.code === 200 ? res.data || [] : [];
+    } catch (error) {
+      batchOpts.value = [];
+    } finally {
+      batchLoading.value = false;
+    }
+  };
+
+  const handleBatchSearch = keyword => {
+    batchKeyword.value = keyword || "";
+    batchLimit.value = BATCH_PAGE;
+    fetchBatchOptions();
+  };
+
+  const loadMoreBatch = () => {
+    if (batchLoading.value || !batchHasMore.value) return;
+    batchLimit.value = Math.min(batchLimit.value + BATCH_PAGE, BATCH_MAX);
+    fetchBatchOptions();
+  };
+
+  const handleBatchChange = value => {
+    if (value) handleQuery();
+  };
+
+  const resetResult = () => {
+    forwardData.batchNo = "";
+    forwardData.chains = [];
+    forwardData.outbounds = [];
+    reverseData.batchNo = "";
+    reverseData.material = null;
+    reverseData.orders = [];
+  };
+
+  const handleTabChange = () => {
+    batchNo.value = "";
+    batchOpts.value = [];
+    queried.value = false;
+    resetResult();
+    handleBatchSearch("");
+  };
+
+  const handleQuery = async () => {
+    const keyword = (batchNo.value || "").trim();
+    if (!keyword) {
+      ElMessage.warning("璇疯緭鍏ユ垨閫夋嫨鎵瑰彿");
+      return;
+    }
+    queried.value = true;
+    loading.value = true;
+    try {
+      if (isForward.value) {
+        const res = await forwardTrace(keyword);
+        if (res.code === 200) {
+          forwardData.batchNo = res.data?.batchNo || keyword;
+          forwardData.chains = res.data?.chains || [];
+          forwardData.outbounds = res.data?.outbounds || [];
+        }
+      } else {
+        const res = await reverseTrace(keyword);
+        if (res.code === 200) {
+          reverseData.batchNo = res.data?.batchNo || keyword;
+          reverseData.material = res.data?.material || null;
+          reverseData.orders = res.data?.orders || [];
+        }
+      }
+    } catch (error) {
+      // 闈� 200锛堝"鎵瑰彿涓嶈兘涓虹┖"锛夊凡鐢卞搷搴旀嫤鎴櫒鎻愮ず
+    } finally {
+      loading.value = false;
+    }
+  };
+
+  const handleExport = async () => {
+    const keyword = (batchNo.value || "").trim();
+    if (!keyword) {
+      ElMessage.warning("璇峰厛閫夋嫨鎴栬緭鍏ユ壒鍙�");
+      return;
+    }
+    exportLoading.value = true;
+    try {
+      const blobData = isForward.value
+        ? await forwardExport(keyword)
+        : await reverseExport(keyword);
+      if (blobValidate(blobData)) {
+        if (blobData.size === 0) {
+          ElMessage.warning("鏆傛棤鏁版嵁鍙鍑�");
+          return;
+        }
+        const blob = new Blob([blobData], {
+          type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+        });
+        const name = isForward.value ? "鎴愬搧鎵规杩芥函" : "鍘熸枡鎵规娴佸悜杩芥函";
+        saveAs(blob, `${name}_${keyword}.xlsx`);
+        ElMessage.success("瀵煎嚭鎴愬姛");
+      } else {
+        const resText = await blobData.text();
+        let rspObj = {};
+        try {
+          rspObj = JSON.parse(resText);
+        } catch (error) {
+          rspObj = {};
+        }
+        ElMessage.error(rspObj.msg || "瀵煎嚭澶辫触");
+      }
+    } catch (error) {
+      ElMessage.error("瀵煎嚭澶辫触锛岃绋嶅悗閲嶈瘯");
+    } finally {
+      exportLoading.value = false;
+    }
+  };
+
+  const gotoOrder = npsNo => {
+    if (!npsNo) return;
+    router.push({
+      path: "/productionManagement/productionTraceability",
+      query: { npsNo },
+    });
+  };
+
+  const goReverse = batch => {
+    if (!batch) return;
+    activeTab.value = "reverse";
+    handleTabChange();
+    batchNo.value = batch;
+    handleQuery();
+  };
+
+  const goForward = batch => {
+    if (!batch) return;
+    activeTab.value = "forward";
+    handleTabChange();
+    batchNo.value = batch;
+    handleQuery();
+  };
+
+  onMounted(() => {
+    handleBatchSearch("");
+  });
+</script>
+
+<style scoped lang="scss">
+  .trace-card {
+    min-height: 82vh;
+  }
+
+  .search-bar {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    margin: 8px 0 20px;
+  }
+
+  .select-load-more {
+    text-align: center;
+    padding: 8px 0;
+    font-size: 13px;
+    color: #409eff;
+    cursor: pointer;
+    border-top: 1px solid #ebeef5;
+
+    &:hover {
+      background-color: #f5f7fa;
+    }
+
+    &.no-more {
+      color: #c0c4cc;
+      cursor: default;
+
+      &:hover {
+        background-color: transparent;
+      }
+    }
+  }
+</style>
diff --git a/src/views/qualityManagement/rawMaterialInspection/components/formDia.vue b/src/views/qualityManagement/rawMaterialInspection/components/formDia.vue
index 595bf80..44f1d53 100644
--- a/src/views/qualityManagement/rawMaterialInspection/components/formDia.vue
+++ b/src/views/qualityManagement/rawMaterialInspection/components/formDia.vue
@@ -327,6 +327,10 @@
   // 鎵撳紑寮规
   const openDialog = async (type, row) => {
     operationType.value = type;
+    // 宸叉彁浜ょ殑妫�楠屽崟寮哄埗鍙
+    if (row?.inspectState === 1) {
+      operationType.value = "view";
+    }
     getOptions().then(res => {
       supplierList.value = res.data;
     });
diff --git a/src/views/qualityManagement/rawMaterialInspection/index.vue b/src/views/qualityManagement/rawMaterialInspection/index.vue
index 6d2acd5..d4f479e 100644
--- a/src/views/qualityManagement/rawMaterialInspection/index.vue
+++ b/src/views/qualityManagement/rawMaterialInspection/index.vue
@@ -430,11 +430,23 @@
 
   // 鎻愪环
   const submit = async id => {
-    const res = await submitQualityInspect({ id: id });
-    if (res.code === 200) {
-      proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
-      getList();
-    }
+    ElMessageBox.confirm(
+      "鎻愪氦鍚庡皢鐢熸垚鍏ュ簱璁板綍涓斾笉鍙慨鏀癸紝纭鎻愪氦锛�",
+      "鎻愮ず",
+      { confirmButtonText: "纭", cancelButtonText: "鍙栨秷", type: "warning" }
+    )
+      .then(async () => {
+        const res = await submitQualityInspect({ id: id });
+        if (res.code === 200) {
+          proxy.$modal.msgSuccess(
+            "鎻愪氦鎴愬姛锛屽悎鏍奸儴鍒嗗凡鐢熸垚鍏ュ簱璁板綍锛堝緟瀹℃牳锛夛紝涓嶅悎鏍奸儴鍒嗗凡鐢熸垚澶勭悊鍗�"
+          );
+          getList();
+        }
+      })
+      .catch(() => {
+        proxy.$modal.msg("宸插彇娑�");
+      });
   };
 
   // 鍏抽棴寮规
diff --git a/vite.config.js b/vite.config.js
index 6b267a7..8e8b7d9 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -8,7 +8,7 @@
   const { VITE_APP_ENV } = env;
   const baseUrl =
       env.VITE_APP_ENV === "development"
-          ? "http://localhost:7005"
+          ? "http://192.168.0.10:7005"
           : env.VITE_BASE_API;
   const javaUrl =
       env.VITE_APP_ENV === "development"

--
Gitblit v1.9.3