From 743becb90e6f5e50d1331cad3b35253ed7fd693c Mon Sep 17 00:00:00 2001
From: chenhj <1263187585@qq.com>
Date: 星期四, 30 四月 2026 13:36:26 +0800
Subject: [PATCH] Merge branch 'dev_NEW_pro' of http://114.132.189.42:9002/r/product-inventory-management into dev_NEW_pro

---
 src/views/basicData/product/index.vue                           | 1132 ++++++++++++++++----------------
 src/api/productionManagement/productionProcess.js               |    6 
 src/views/productionManagement/productionOrder/index.vue        |   15 
 src/views/productionManagement/productionTraceability/index.vue |  831 ++++++++++++++++++++++++
 src/api/basicData/productProcess.js                             |   10 
 src/router/index.js                                             |   13 
 6 files changed, 1,435 insertions(+), 572 deletions(-)

diff --git a/src/api/basicData/productProcess.js b/src/api/basicData/productProcess.js
index e0208fa..46356fd 100644
--- a/src/api/basicData/productProcess.js
+++ b/src/api/basicData/productProcess.js
@@ -1,10 +1,10 @@
-import request from '@/utils/request'
+import request from "@/utils/request";
 
 // 宸ュ簭鍒楄〃鍒嗛〉鏌ヨ
 export function productProcessListPage(query) {
   return request({
-    url: '/productProcess/listPage',
-    method: 'get',
-    params: query
-  })
+    url: "/technologyOperation/listPage",
+    method: "get",
+    params: query,
+  });
 }
diff --git a/src/api/productionManagement/productionProcess.js b/src/api/productionManagement/productionProcess.js
index 7001997..783f584 100644
--- a/src/api/productionManagement/productionProcess.js
+++ b/src/api/productionManagement/productionProcess.js
@@ -4,7 +4,7 @@
 // 鍒嗛〉鏌ヨ
 export function listPage(query) {
   return request({
-    url: "/productProcess/listPage",
+    url: "/technologyOperation/listPage",
     method: "get",
     params: query,
   });
@@ -53,7 +53,7 @@
 // 瀵煎叆鏁版嵁
 export function importData(data) {
   return request({
-    url: "/productProcess/importData",
+    url: "/technologyOperation/importData",
     method: "post",
     data: data,
   });
@@ -62,7 +62,7 @@
 // 涓嬭浇妯℃澘
 export function downloadTemplate() {
   return request({
-    url: "/productProcess/downloadTemplate",
+    url: "/technologyOperation/downloadTemplate",
     method: "post",
     responseType: "blob",
   });
diff --git a/src/router/index.js b/src/router/index.js
index cc2b88c..6b89a17 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -106,6 +106,19 @@
     name: "DeviceInfo",
     meta: { title: "璁惧淇℃伅", icon: "monitor" },
   },
+  {
+    path: "/productionTraceability",
+    component: Layout,
+    hidden: true,
+    children: [
+      {
+        path: "index",
+        component: () => import("@/views/productionManagement/productionTraceability/index.vue"),
+        name: "ProductionTraceability",
+        meta: { title: "鐢熶骇杩芥函", activeMenu: "/productionManagement/productionOrder" },
+      },
+    ],
+  },
   // 娣诲姞椤圭洰璇︽儏椤甸潰璺敱閰嶇疆
   {
     path: "/oaSystem/projectManagement/projectDetail",
diff --git a/src/views/basicData/product/index.vue b/src/views/basicData/product/index.vue
index 10b51bf..99ab028 100644
--- a/src/views/basicData/product/index.vue
+++ b/src/views/basicData/product/index.vue
@@ -2,41 +2,34 @@
   <div class="app-container product-view">
     <div class="left">
       <div>
-        <el-input
-          v-model="search"
-          style="width: 210px"
-          placeholder="杈撳叆鍏抽敭瀛楄繘琛屾悳绱�"
-          @change="searchFilter"
-          @clear="searchFilter"
-          clearable
-          prefix-icon="Search"
-        />
-        <el-button
-          v-if="false"
-          type="primary"
-          @click="openProDia('addOne')"
-          style="margin-left: 10px"
-          >鏂板浜у搧澶х被</el-button
-        >
+        <el-input v-model="search"
+                  style="width: 210px"
+                  placeholder="杈撳叆鍏抽敭瀛楄繘琛屾悳绱�"
+                  @change="searchFilter"
+                  @clear="searchFilter"
+                  clearable
+                  prefix-icon="Search" />
+        <el-button v-if="false"
+                   type="primary"
+                   @click="openProDia('addOne')"
+                   style="margin-left: 10px">鏂板浜у搧澶х被</el-button>
       </div>
       <div ref="containerRef">
-        <el-tree
-          ref="tree"
-          v-loading="treeLoad"
-          :data="list"
-          @node-click="handleNodeClick"
-          :expand-on-click-node="false"
-          @node-expand="handleNodeExpand"
-          @node-collapse="handleNodeCollapse"
-          :key="treeKey"
-          :default-expanded-keys="expandedKeys"
-          :filter-node-method="filterNode"
-          :props="{ children: 'children', label: 'label' }"
-          highlight-current
-          node-key="id"
-          class="product-tree-scroll"
-          style="height: calc(100vh - 190px); overflow-y: auto"
-        >
+        <el-tree ref="tree"
+                 v-loading="treeLoad"
+                 :data="list"
+                 @node-click="handleNodeClick"
+                 :expand-on-click-node="false"
+                 @node-expand="handleNodeExpand"
+                 @node-collapse="handleNodeCollapse"
+                 :key="treeKey"
+                 :default-expanded-keys="expandedKeys"
+                 :filter-node-method="filterNode"
+                 :props="{ children: 'children', label: 'label' }"
+                 highlight-current
+                 node-key="id"
+                 class="product-tree-scroll"
+                 style="height: calc(100vh - 190px); overflow-y: auto">
           <template #default="{ node, data }">
             <div class="custom-tree-node">
               <span class="tree-node-content">
@@ -47,25 +40,23 @@
                 <span class="tree-node-label">{{ data.label }}</span>
               </span>
               <div>
-                <el-button
-                  type="primary"
-                  link
-                  :disabled="isTopLevelNode(data, node)"
-                  @click="openProDia('edit', data)"
-                >
+                <el-button type="primary"
+                           link
+                           :disabled="isTopLevelNode(data, node)"
+                           @click="openProDia('edit', data)">
                   缂栬緫
                 </el-button>
-                <el-button type="primary" link @click="openProDia('add', data)">
+                <el-button type="primary"
+                           link
+                           @click="openProDia('add', data)">
                   娣诲姞浜у搧
                 </el-button>
-                <el-button
-                  v-if="!node.childNodes.length"
-                  style="margin-left: 4px"
-                  type="danger"
-                  link
-                  :disabled="isTopLevelNode(data, node)"
-                  @click="remove(node, data)"
-                >
+                <el-button v-if="!node.childNodes.length"
+                           style="margin-left: 4px"
+                           type="danger"
+                           link
+                           :disabled="isTopLevelNode(data, node)"
+                           @click="remove(node, data)">
                   鍒犻櫎
                 </el-button>
               </div>
@@ -75,103 +66,109 @@
       </div>
     </div>
     <div class="right">
-      <div style="margin-bottom: 10px" v-if="isShowButton">
-        <el-button type="primary" @click="openModelDia('add')">
+      <div style="margin-bottom: 10px"
+           v-if="isShowButton">
+        <el-button type="primary"
+                   @click="openModelDia('add')">
           鏂板瑙勬牸鍨嬪彿
         </el-button>
-        <ImportExcel :product-id="currentId" @uploadSuccess="getModelList" />
-        <el-button
-          type="danger"
-          @click="handleDelete"
-          style="margin-left: 10px"
-          plain
-        >
+        <ImportExcel :product-id="currentId"
+                     @uploadSuccess="getModelList" />
+        <el-button type="danger"
+                   @click="handleDelete"
+                   style="margin-left: 10px"
+                   plain>
           鍒犻櫎
         </el-button>
       </div>
-      <PIMTable
-        rowKey="id"
-        :column="tableColumn"
-        :tableData="tableData"
-        :page="page"
-        :isSelection="true"
-        @selection-change="handleSelectionChange"
-        :tableLoading="tableLoading"
-        @pagination="pagination"
-      ></PIMTable>
+      <PIMTable rowKey="id"
+                :column="tableColumn"
+                :tableData="tableData"
+                :page="page"
+                :isSelection="true"
+                @selection-change="handleSelectionChange"
+                :tableLoading="tableLoading"
+                @pagination="pagination"></PIMTable>
     </div>
-    <el-dialog v-model="productDia" title="浜у搧" width="400px" @keydown.enter.prevent>
-      <el-form
-        :model="form"
-        label-width="140px"
-        label-position="top"
-        :rules="rules"
-        ref="formRef"
-      >
+    <el-dialog v-model="productDia"
+               title="浜у搧"
+               width="400px"
+               @keydown.enter.prevent>
+      <el-form :model="form"
+               label-width="140px"
+               label-position="top"
+               :rules="rules"
+               ref="formRef">
         <el-row :gutter="30">
           <el-col :span="24">
-            <el-form-item label="浜у搧鍚嶇О锛�" prop="productName">
-              <el-input
-                v-model="form.productName"
-                placeholder="璇疯緭鍏ヤ骇鍝佸悕绉�"
-                maxlength="20"
-                show-word-limit
-                clearable
-                @keydown.enter.prevent
-              />
+            <el-form-item label="浜у搧鍚嶇О锛�"
+                          prop="productName">
+              <el-input v-model="form.productName"
+                        placeholder="璇疯緭鍏ヤ骇鍝佸悕绉�"
+                        maxlength="20"
+                        show-word-limit
+                        clearable
+                        @keydown.enter.prevent />
             </el-form-item>
           </el-col>
         </el-row>
       </el-form>
       <template #footer>
         <div class="dialog-footer">
-          <el-button type="primary" @click="submitForm">纭</el-button>
+          <el-button type="primary"
+                     @click="submitForm">纭</el-button>
           <el-button @click="closeProDia">鍙栨秷</el-button>
         </div>
       </template>
     </el-dialog>
-    <el-dialog
-      v-model="modelDia"
-      title="瑙勬牸鍨嬪彿"
-      width="400px"
-      @close="closeModelDia"
-      @keydown.enter.prevent
-    >
-      <el-form
-        :model="modelForm"
-        label-width="140px"
-        label-position="top"
-        :rules="modelRules"
-        ref="modelFormRef"
-      >
+    <el-dialog v-model="modelDia"
+               title="瑙勬牸鍨嬪彿"
+               width="400px"
+               @close="closeModelDia"
+               @keydown.enter.prevent>
+      <el-form :model="modelForm"
+               label-width="140px"
+               label-position="top"
+               :rules="modelRules"
+               ref="modelFormRef">
         <el-row>
+          <el-row>
+            <el-col :span="24">
+              <el-form-item label="浜у搧缂栧彿锛�"
+                            prop="productCode">
+                <el-input v-model="modelForm.productCode"
+                          placeholder="璇疯緭鍏ヤ骇鍝佺紪鍙�"
+                          clearable
+                          @keydown.enter.prevent />
+              </el-form-item>
+            </el-col>
+          </el-row>
           <el-col :span="24">
-            <el-form-item label="瑙勬牸鍨嬪彿锛�" prop="model">
-              <el-input
-                v-model="modelForm.model"
-                placeholder="璇疯緭鍏ヨ鏍煎瀷鍙�"
-                clearable
-                @keydown.enter.prevent
-              />
+            <el-form-item label="瑙勬牸鍨嬪彿锛�"
+                          prop="model">
+              <el-input v-model="modelForm.model"
+                        placeholder="璇疯緭鍏ヨ鏍煎瀷鍙�"
+                        clearable
+                        @keydown.enter.prevent />
             </el-form-item>
           </el-col>
         </el-row>
         <el-row>
           <el-col :span="24">
-            <el-form-item label="鍗曚綅锛�" prop="unit">
-              <el-input
-                v-model="modelForm.unit"
-                placeholder="璇疯緭鍏ュ崟浣�"
-                clearable
-                @keydown.enter.prevent
-              />
+            <el-form-item label="鍗曚綅锛�"
+                          prop="unit">
+              <el-input v-model="modelForm.unit"
+                        placeholder="璇疯緭鍏ュ崟浣�"
+                        clearable
+                        @keydown.enter.prevent />
             </el-form-item>
           </el-col>
         </el-row>
       </el-form>
       <template #footer>
         <div class="dialog-footer">
-          <el-button type="primary" @click="submitModelForm">纭</el-button>
+          <el-button type="primary"
+                     @click="submitModelForm">纭</el-button>
           <el-button @click="closeModelDia">鍙栨秷</el-button>
         </div>
       </template>
@@ -180,473 +177,480 @@
 </template>
 
 <script setup>
-import { nextTick, ref } from "vue";
-import { ElMessageBox } from "element-plus";
-import {
-  addOrEditProduct,
-  addOrEditProductModel,
-  delProduct,
-  delProductModel,
-  modelListPage,
-  productTreeList,
-} from "@/api/basicData/product.js";
-import ImportExcel from "./ImportExcel/index.vue";
+  import { nextTick, ref } from "vue";
+  import { ElMessageBox } from "element-plus";
+  import {
+    addOrEditProduct,
+    addOrEditProductModel,
+    delProduct,
+    delProductModel,
+    modelListPage,
+    productTreeList,
+  } from "@/api/basicData/product.js";
+  import ImportExcel from "./ImportExcel/index.vue";
 
-const { proxy } = getCurrentInstance();
-const tree = ref(null);
-const containerRef = ref(null);
-const treeKey = ref(0);
-const expandedKeySet = new Set();
-const EXPANDED_STORAGE_KEY = "basicData_product_tree_expanded_keys_v2";
+  const { proxy } = getCurrentInstance();
+  const tree = ref(null);
+  const containerRef = ref(null);
+  const treeKey = ref(0);
+  const expandedKeySet = new Set();
+  const EXPANDED_STORAGE_KEY = "basicData_product_tree_expanded_keys_v2";
 
-const loadExpandedKeys = () => {
-  if (typeof window === "undefined") {
-    return [];
-  }
-  try {
-    const saved = localStorage.getItem(EXPANDED_STORAGE_KEY);
-    return saved ? JSON.parse(saved) : [];
-  } catch (error) {
-    console.error(error);
-    return [];
-  }
-};
-
-const saveExpandedKeys = () => {
-  if (typeof window === "undefined") {
-    return;
-  }
-  localStorage.setItem(
-    EXPANDED_STORAGE_KEY,
-    JSON.stringify(Array.from(expandedKeySet))
-  );
-};
-
-loadExpandedKeys().forEach((key) => expandedKeySet.add(key));
-
-const syncExpandedKeysFromTree = () => {
-  const keys = [];
-  const walk = (nodes) => {
-    (nodes || []).forEach((item) => {
-      if (item.expanded && item.data?.id !== undefined) {
-        keys.push(item.data.id);
-      }
-      if (item.childNodes && item.childNodes.length) {
-        walk(item.childNodes);
-      }
-    });
-  };
-
-  walk(tree.value?.root?.childNodes);
-  expandedKeySet.clear();
-  keys.forEach((key) => expandedKeySet.add(key));
-  expandedKeys.value = keys;
-  saveExpandedKeys();
-};
-
-const normalizeExpandedKeys = (treeData) => {
-  const parentMap = new Map();
-  const walk = (nodes, parentId = null) => {
-    (nodes || []).forEach((item) => {
-      parentMap.set(item.id, parentId);
-      if (item.children && item.children.length) {
-        walk(item.children, item.id);
-      }
-    });
-  };
-
-  walk(treeData);
-
-  const normalizedKeys = Array.from(expandedKeySet).filter((key) => {
-    if (!parentMap.has(key)) {
-      return false;
+  const loadExpandedKeys = () => {
+    if (typeof window === "undefined") {
+      return [];
     }
-    let currentId = key;
-    while (parentMap.has(currentId)) {
-      const parentId = parentMap.get(currentId);
-      if (!parentId) {
-        return true;
-      }
-      if (!expandedKeySet.has(parentId)) {
+    try {
+      const saved = localStorage.getItem(EXPANDED_STORAGE_KEY);
+      return saved ? JSON.parse(saved) : [];
+    } catch (error) {
+      console.error(error);
+      return [];
+    }
+  };
+
+  const saveExpandedKeys = () => {
+    if (typeof window === "undefined") {
+      return;
+    }
+    localStorage.setItem(
+      EXPANDED_STORAGE_KEY,
+      JSON.stringify(Array.from(expandedKeySet))
+    );
+  };
+
+  loadExpandedKeys().forEach(key => expandedKeySet.add(key));
+
+  const syncExpandedKeysFromTree = () => {
+    const keys = [];
+    const walk = nodes => {
+      (nodes || []).forEach(item => {
+        if (item.expanded && item.data?.id !== undefined) {
+          keys.push(item.data.id);
+        }
+        if (item.childNodes && item.childNodes.length) {
+          walk(item.childNodes);
+        }
+      });
+    };
+
+    walk(tree.value?.root?.childNodes);
+    expandedKeySet.clear();
+    keys.forEach(key => expandedKeySet.add(key));
+    expandedKeys.value = keys;
+    saveExpandedKeys();
+  };
+
+  const normalizeExpandedKeys = treeData => {
+    const parentMap = new Map();
+    const walk = (nodes, parentId = null) => {
+      (nodes || []).forEach(item => {
+        parentMap.set(item.id, parentId);
+        if (item.children && item.children.length) {
+          walk(item.children, item.id);
+        }
+      });
+    };
+
+    walk(treeData);
+
+    const normalizedKeys = Array.from(expandedKeySet).filter(key => {
+      if (!parentMap.has(key)) {
         return false;
       }
-      currentId = parentId;
-    }
-    return true;
-  });
-
-  if (normalizedKeys.length !== expandedKeySet.size) {
-    expandedKeySet.clear();
-    normalizedKeys.forEach((key) => expandedKeySet.add(key));
-    saveExpandedKeys();
-  }
-};
-
-const productDia = ref(false);
-const modelDia = ref(false);
-const modelOperationType = ref("");
-const search = ref("");
-const currentId = ref("");
-const currentParentId = ref("");
-const operationType = ref("");
-const treeLoad = ref(false);
-const list = ref([]);
-const expandedKeys = ref([]);
-const tableColumn = ref([
-  {
-    label: "瑙勬牸鍨嬪彿",
-    prop: "model",
-  },
-  {
-    label: "鍗曚綅",
-    prop: "unit",
-  },
-  {
-    dataType: "action",
-    label: "鎿嶄綔",
-    align: "center",
-    operation: [
-      {
-        name: "缂栬緫",
-        type: "text",
-        clickFun: (row) => {
-          openModelDia("edit", row);
-        },
-      },
-    ],
-  },
-]);
-const tableData = ref([]);
-const tableLoading = ref(false);
-const isShowButton = ref(false);
-const selectedRows = ref([]);
-const page = reactive({
-  current: 1,
-  size: 10,
-  total: 0,
-});
-const data = reactive({
-  form: {
-    productName: "",
-  },
-  rules: {
-    productName: [
-      { required: true, message: "璇疯緭鍏�", trigger: "blur" },
-      { max: 20, message: "浜у搧鍚嶇О涓嶈兘瓒呰繃20涓瓧绗�", trigger: "blur" },
-    ],
-  },
-  modelForm: {
-    model: "",
-    unit: "",
-  },
-  modelRules: {
-    model: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
-    unit: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
-  },
-});
-const { form, rules, modelForm, modelRules } = toRefs(data);
-// 鏌ヨ浜у搧鏍�
-const getProductTreeList = () => {
-  treeLoad.value = true;
-  productTreeList()
-    .then((res) => {
-      list.value = res || [];
-      normalizeExpandedKeys(list.value);
-      expandedKeys.value = Array.from(expandedKeySet);
-      treeKey.value += 1;
-      nextTick(() => {
-        tree.value?.setDefaultExpandedKeys?.(expandedKeys.value);
-      });
-    })
-    .catch((err) => {
-      console.error(err);
-    })
-    .finally(() => {
-      treeLoad.value = false;
-    });
-};
-const handleNodeExpand = (data) => {
-  nextTick(syncExpandedKeysFromTree);
-};
-const handleNodeCollapse = (data, node) => {
-  node?.eachNode?.((item) => {
-    item.collapse();
-  });
-  nextTick(syncExpandedKeysFromTree);
-};
-// 杩囨护浜у搧鏍�
-const searchFilter = () => {
-  proxy.$refs.tree.filter(search.value);
-};
-const isTopLevelNode = (data, node) => {
-  if (node?.level !== undefined) {
-    return node.level === 1;
-  }
-  return [null, undefined, "", 0, "0"].includes(data?.parentId);
-};
-// 鎵撳紑浜у搧寮规
-const openProDia = (type, data) => {
-  if (data && type === "edit" && isTopLevelNode(data)) {
-    proxy.$modal.msgWarning("涓�绾ц妭鐐逛笉鑳界紪杈戞垨鍒犻櫎");
-    return;
-  }
-  operationType.value = type;
-  productDia.value = true;
-  form.value.productName = "";
-  if (type === "edit") {
-    form.value.productName = data.productName;
-  }
-};
-// 鎵撳紑瑙勬牸鍨嬪彿寮规
-const openModelDia = (type, data) => {
-  modelOperationType.value = type;
-  modelDia.value = true;
-  modelForm.value.model = "";
-  modelForm.value.model = "";
-  modelForm.value.id = "";
-  if (type === "edit") {
-    modelForm.value = { ...data };
-  }
-};
-// 鎻愪氦浜у搧鍚嶇О淇敼
-const submitForm = () => {
-  proxy.$refs.formRef.validate((valid) => {
-    if (valid) {
-      if (operationType.value === "add") {
-        form.value.parentId = currentId.value;
-        form.value.id = "";
-      } else if (operationType.value === "addOne") {
-        form.value.id = "";
-        form.value.parentId = "";
-      } else {
-        form.value.id = currentId.value;
-        form.value.parentId = "";
+      let currentId = key;
+      while (parentMap.has(currentId)) {
+        const parentId = parentMap.get(currentId);
+        if (!parentId) {
+          return true;
+        }
+        if (!expandedKeySet.has(parentId)) {
+          return false;
+        }
+        currentId = parentId;
       }
-      addOrEditProduct(form.value).then((res) => {
-        proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
-        closeProDia();
-        getProductTreeList();
-      });
-    }
-  });
-};
-// 鍏抽棴浜у搧寮规
-const closeProDia = () => {
-  proxy.$refs.formRef.resetFields();
-  productDia.value = false;
-};
+      return true;
+    });
 
-// 鍒犻櫎浜у搧
-const remove = (node, data) => {
-  if (isTopLevelNode(data, node)) {
-    proxy.$modal.msgWarning("涓�绾ц妭鐐逛笉鑳界紪杈戞垨鍒犻櫎");
-    return;
-  }
-  let ids = [];
-  ids.push(data.id);
-  ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎鎻愮ず", {
-    confirmButtonText: "纭",
-    cancelButtonText: "鍙栨秷",
-    type: "warning",
-  })
-    .then(() => {
-      tableLoading.value = true;
-      delProduct(ids)
-        .then((res) => {
-          proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+    if (normalizedKeys.length !== expandedKeySet.size) {
+      expandedKeySet.clear();
+      normalizedKeys.forEach(key => expandedKeySet.add(key));
+      saveExpandedKeys();
+    }
+  };
+
+  const productDia = ref(false);
+  const modelDia = ref(false);
+  const modelOperationType = ref("");
+  const search = ref("");
+  const currentId = ref("");
+  const currentParentId = ref("");
+  const operationType = ref("");
+  const treeLoad = ref(false);
+  const list = ref([]);
+  const expandedKeys = ref([]);
+  const tableColumn = ref([
+    {
+      label: "浜у搧缂栧彿",
+      prop: "productCode",
+    },
+    {
+      label: "瑙勬牸鍨嬪彿",
+      prop: "model",
+    },
+    {
+      label: "鍗曚綅",
+      prop: "unit",
+    },
+    {
+      dataType: "action",
+      label: "鎿嶄綔",
+      align: "center",
+      operation: [
+        {
+          name: "缂栬緫",
+          type: "text",
+          clickFun: row => {
+            openModelDia("edit", row);
+          },
+        },
+      ],
+    },
+  ]);
+  const tableData = ref([]);
+  const tableLoading = ref(false);
+  const isShowButton = ref(false);
+  const selectedRows = ref([]);
+  const page = reactive({
+    current: 1,
+    size: 10,
+    total: 0,
+  });
+  const data = reactive({
+    form: {
+      productName: "",
+    },
+    rules: {
+      productName: [
+        { required: true, message: "璇疯緭鍏�", trigger: "blur" },
+        { max: 20, message: "浜у搧鍚嶇О涓嶈兘瓒呰繃20涓瓧绗�", trigger: "blur" },
+      ],
+    },
+    modelForm: {
+      model: "",
+      unit: "",
+      productCode: "",
+    },
+    modelRules: {
+      model: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+      unit: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+      productCode: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+    },
+  });
+  const { form, rules, modelForm, modelRules } = toRefs(data);
+  // 鏌ヨ浜у搧鏍�
+  const getProductTreeList = () => {
+    treeLoad.value = true;
+    productTreeList()
+      .then(res => {
+        list.value = res || [];
+        normalizeExpandedKeys(list.value);
+        expandedKeys.value = Array.from(expandedKeySet);
+        treeKey.value += 1;
+        nextTick(() => {
+          tree.value?.setDefaultExpandedKeys?.(expandedKeys.value);
+        });
+      })
+      .catch(err => {
+        console.error(err);
+      })
+      .finally(() => {
+        treeLoad.value = false;
+      });
+  };
+  const handleNodeExpand = data => {
+    nextTick(syncExpandedKeysFromTree);
+  };
+  const handleNodeCollapse = (data, node) => {
+    node?.eachNode?.(item => {
+      item.collapse();
+    });
+    nextTick(syncExpandedKeysFromTree);
+  };
+  // 杩囨护浜у搧鏍�
+  const searchFilter = () => {
+    proxy.$refs.tree.filter(search.value);
+  };
+  const isTopLevelNode = (data, node) => {
+    if (node?.level !== undefined) {
+      return node.level === 1;
+    }
+    return [null, undefined, "", 0, "0"].includes(data?.parentId);
+  };
+  // 鎵撳紑浜у搧寮规
+  const openProDia = (type, data) => {
+    if (data && type === "edit" && isTopLevelNode(data)) {
+      proxy.$modal.msgWarning("涓�绾ц妭鐐逛笉鑳界紪杈戞垨鍒犻櫎");
+      return;
+    }
+    operationType.value = type;
+    productDia.value = true;
+    form.value.productName = "";
+    if (type === "edit") {
+      form.value.productName = data.productName;
+    }
+  };
+  // 鎵撳紑瑙勬牸鍨嬪彿寮规
+  const openModelDia = (type, data) => {
+    modelOperationType.value = type;
+    modelDia.value = true;
+    modelForm.value.model = "";
+    modelForm.value.unit = "";
+    modelForm.value.productCode = "";
+    modelForm.value.id = "";
+    if (type === "edit") {
+      modelForm.value = { ...data };
+    }
+  };
+  // 鎻愪氦浜у搧鍚嶇О淇敼
+  const submitForm = () => {
+    proxy.$refs.formRef.validate(valid => {
+      if (valid) {
+        if (operationType.value === "add") {
+          form.value.parentId = currentId.value;
+          form.value.id = "";
+        } else if (operationType.value === "addOne") {
+          form.value.id = "";
+          form.value.parentId = "";
+        } else {
+          form.value.id = currentId.value;
+          form.value.parentId = "";
+        }
+        addOrEditProduct(form.value).then(res => {
+          proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+          closeProDia();
           getProductTreeList();
-        })
-        .finally(() => {
-          tableLoading.value = false;
         });
-    })
-    .catch(() => {
-      proxy.$modal.msg("宸插彇娑�");
+      }
     });
-};
-// 閫夋嫨浜у搧
-const handleNodeClick = (val, node, el) => {
-  // 鍒ゆ柇鏄惁涓哄彾瀛愯妭鐐�
-  isShowButton.value = !(val.children && val.children.length > 0);
-  // 鍙湁鍙跺瓙鑺傜偣鎵嶆墽琛屼互涓嬮�昏緫
-  currentId.value = val.id;
-  currentParentId.value = val.parentId;
-  getModelList();
-};
+  };
+  // 鍏抽棴浜у搧寮规
+  const closeProDia = () => {
+    proxy.$refs.formRef.resetFields();
+    productDia.value = false;
+  };
 
-// 鎻愪氦瑙勬牸鍨嬪彿淇敼
-const submitModelForm = () => {
-  proxy.$refs.modelFormRef.validate((valid) => {
-    if (valid) {
-      modelForm.value.productId = currentId.value;
-      addOrEditProductModel(modelForm.value).then((res) => {
-        proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
-        closeModelDia();
-        getModelList();
-      });
+  // 鍒犻櫎浜у搧
+  const remove = (node, data) => {
+    if (isTopLevelNode(data, node)) {
+      proxy.$modal.msgWarning("涓�绾ц妭鐐逛笉鑳界紪杈戞垨鍒犻櫎");
+      return;
     }
-  });
-};
-// 鍏抽棴鍨嬪彿寮规
-const closeModelDia = () => {
-  proxy.$refs.modelFormRef.resetFields();
-  modelDia.value = false;
-};
-// 琛ㄦ牸閫夋嫨鏁版嵁
-const handleSelectionChange = (selection) => {
-  selectedRows.value = selection;
-};
-
-// 鏌ヨ瑙勬牸鍨嬪彿
-const pagination = (obj) => {
-  page.current = obj.page;
-  page.size = obj.limit;
-  getModelList();
-};
-const getModelList = () => {
-  tableLoading.value = true;
-  modelListPage({
-    id: currentId.value,
-    current: page.current,
-    size: page.size,
-  }).then((res) => {
-    console.log("res", res);
-    tableData.value = res.records;
-    page.total = res.total;
-    tableLoading.value = false;
-  });
-};
-// 鍒犻櫎瑙勬牸鍨嬪彿
-const handleDelete = () => {
-  let ids = [];
-  if (selectedRows.value.length > 0) {
-    ids = selectedRows.value.map((item) => item.id);
-  } else {
-    proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
-    return;
-  }
-  ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎鎻愮ず", {
-    confirmButtonText: "纭",
-    cancelButtonText: "鍙栨秷",
-    type: "warning",
-  })
-    .then(() => {
-      tableLoading.value = true;
-      delProductModel(ids)
-        .then((res) => {
-          proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
-          getModelList();
-        })
-        .finally(() => {
-          tableLoading.value = false;
-        });
+    let ids = [];
+    ids.push(data.id);
+    ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎鎻愮ず", {
+      confirmButtonText: "纭",
+      cancelButtonText: "鍙栨秷",
+      type: "warning",
     })
-    .catch(() => {
-      proxy.$modal.msg("宸插彇娑�");
+      .then(() => {
+        tableLoading.value = true;
+        delProduct(ids)
+          .then(res => {
+            proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+            getProductTreeList();
+          })
+          .finally(() => {
+            tableLoading.value = false;
+          });
+      })
+      .catch(() => {
+        proxy.$modal.msg("宸插彇娑�");
+      });
+  };
+  // 閫夋嫨浜у搧
+  const handleNodeClick = (val, node, el) => {
+    // 鍒ゆ柇鏄惁涓哄彾瀛愯妭鐐�
+    isShowButton.value = !(val.children && val.children.length > 0);
+    // 鍙湁鍙跺瓙鑺傜偣鎵嶆墽琛屼互涓嬮�昏緫
+    currentId.value = val.id;
+    currentParentId.value = val.parentId;
+    getModelList();
+  };
+
+  // 鎻愪氦瑙勬牸鍨嬪彿淇敼
+  const submitModelForm = () => {
+    proxy.$refs.modelFormRef.validate(valid => {
+      if (valid) {
+        modelForm.value.productId = currentId.value;
+        addOrEditProductModel(modelForm.value).then(res => {
+          proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+          closeModelDia();
+          getModelList();
+        });
+      }
     });
-};
-// 璋冪敤tree杩囨护鏂规硶 涓枃鑻辫繃婊�
-const filterNode = (value, data, node) => {
-  if (!value) {
-    //濡傛灉鏁版嵁涓虹┖锛屽垯杩斿洖true,鏄剧ず鎵�鏈夌殑鏁版嵁椤�
-    return true;
-  }
-  // 鏌ヨ鍒楄〃鏄惁鏈夊尮閰嶆暟鎹紝灏嗗�煎皬鍐欙紝鍖归厤鑻辨枃鏁版嵁
-  let val = value.toLowerCase();
-  return chooseNode(val, data, node); // 璋冪敤杩囨护浜屽眰鏂规硶
-};
-// 杩囨护鐖惰妭鐐� / 瀛愯妭鐐� (濡傛灉杈撳叆鐨勫弬鏁版槸鐖惰妭鐐逛笖鑳藉尮閰嶏紝鍒欒繑鍥炶鑺傜偣浠ュ強鍏朵笅鐨勬墍鏈夊瓙鑺傜偣锛涘鏋滃弬鏁版槸瀛愯妭鐐癸紝鍒欒繑鍥炶鑺傜偣鐨勭埗鑺傜偣銆俷ame鏄腑鏂囧瓧绗︼紝enName鏄嫳鏂囧瓧绗�.
-const chooseNode = (value, data, node) => {
-  if (data.label.indexOf(value) !== -1) {
-    return true;
-  }
-  const level = node.level;
-  // 濡傛灉浼犲叆鐨勮妭鐐规湰韬氨鏄竴绾ц妭鐐瑰氨涓嶇敤鏍¢獙浜�
-  if (level === 1) {
-    return false;
-  }
-  // 鍏堝彇褰撳墠鑺傜偣鐨勭埗鑺傜偣
-  let parentData = node.parent;
-  // 閬嶅巻褰撳墠鑺傜偣鐨勭埗鑺傜偣
-  let index = 0;
-  while (index < level - 1) {
-    // 濡傛灉鍖归厤鍒扮洿鎺ヨ繑鍥烇紝姝ゅname鍊兼槸涓枃瀛楃锛宔nName鏄嫳鏂囧瓧绗︺�傚垽鏂尮閰嶄腑鑻辨枃杩囨护
-    if (parentData.data.label.indexOf(value) !== -1) {
+  };
+  // 鍏抽棴鍨嬪彿寮规
+  const closeModelDia = () => {
+    proxy.$refs.modelFormRef.resetFields();
+    modelDia.value = false;
+  };
+  // 琛ㄦ牸閫夋嫨鏁版嵁
+  const handleSelectionChange = selection => {
+    selectedRows.value = selection;
+  };
+
+  // 鏌ヨ瑙勬牸鍨嬪彿
+  const pagination = obj => {
+    page.current = obj.page;
+    page.size = obj.limit;
+    getModelList();
+  };
+  const getModelList = () => {
+    tableLoading.value = true;
+    modelListPage({
+      id: currentId.value,
+      current: page.current,
+      size: page.size,
+    }).then(res => {
+      console.log("res", res);
+      tableData.value = res.records;
+      page.total = res.total;
+      tableLoading.value = false;
+    });
+  };
+  // 鍒犻櫎瑙勬牸鍨嬪彿
+  const handleDelete = () => {
+    let ids = [];
+    if (selectedRows.value.length > 0) {
+      ids = selectedRows.value.map(item => item.id);
+    } else {
+      proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+      return;
+    }
+    ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎鎻愮ず", {
+      confirmButtonText: "纭",
+      cancelButtonText: "鍙栨秷",
+      type: "warning",
+    })
+      .then(() => {
+        tableLoading.value = true;
+        delProductModel(ids)
+          .then(res => {
+            proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+            getModelList();
+          })
+          .finally(() => {
+            tableLoading.value = false;
+          });
+      })
+      .catch(() => {
+        proxy.$modal.msg("宸插彇娑�");
+      });
+  };
+  // 璋冪敤tree杩囨护鏂规硶 涓枃鑻辫繃婊�
+  const filterNode = (value, data, node) => {
+    if (!value) {
+      //濡傛灉鏁版嵁涓虹┖锛屽垯杩斿洖true,鏄剧ず鎵�鏈夌殑鏁版嵁椤�
       return true;
     }
-    // 鍚﹀垯鐨勮瘽鍐嶅線涓婁竴灞傚仛鍖归厤
-    parentData = parentData.parent;
-    index++;
-  }
-  // 娌″尮閰嶅埌杩斿洖false
-  return false;
-};
-getProductTreeList();
+    // 鏌ヨ鍒楄〃鏄惁鏈夊尮閰嶆暟鎹紝灏嗗�煎皬鍐欙紝鍖归厤鑻辨枃鏁版嵁
+    let val = value.toLowerCase();
+    return chooseNode(val, data, node); // 璋冪敤杩囨护浜屽眰鏂规硶
+  };
+  // 杩囨护鐖惰妭鐐� / 瀛愯妭鐐� (濡傛灉杈撳叆鐨勫弬鏁版槸鐖惰妭鐐逛笖鑳藉尮閰嶏紝鍒欒繑鍥炶鑺傜偣浠ュ強鍏朵笅鐨勬墍鏈夊瓙鑺傜偣锛涘鏋滃弬鏁版槸瀛愯妭鐐癸紝鍒欒繑鍥炶鑺傜偣鐨勭埗鑺傜偣銆俷ame鏄腑鏂囧瓧绗︼紝enName鏄嫳鏂囧瓧绗�.
+  const chooseNode = (value, data, node) => {
+    if (data.label.indexOf(value) !== -1) {
+      return true;
+    }
+    const level = node.level;
+    // 濡傛灉浼犲叆鐨勮妭鐐规湰韬氨鏄竴绾ц妭鐐瑰氨涓嶇敤鏍¢獙浜�
+    if (level === 1) {
+      return false;
+    }
+    // 鍏堝彇褰撳墠鑺傜偣鐨勭埗鑺傜偣
+    let parentData = node.parent;
+    // 閬嶅巻褰撳墠鑺傜偣鐨勭埗鑺傜偣
+    let index = 0;
+    while (index < level - 1) {
+      // 濡傛灉鍖归厤鍒扮洿鎺ヨ繑鍥烇紝姝ゅname鍊兼槸涓枃瀛楃锛宔nName鏄嫳鏂囧瓧绗︺�傚垽鏂尮閰嶄腑鑻辨枃杩囨护
+      if (parentData.data.label.indexOf(value) !== -1) {
+        return true;
+      }
+      // 鍚﹀垯鐨勮瘽鍐嶅線涓婁竴灞傚仛鍖归厤
+      parentData = parentData.parent;
+      index++;
+    }
+    // 娌″尮閰嶅埌杩斿洖false
+    return false;
+  };
+  getProductTreeList();
 </script>
 
 <style scoped>
-.product-view {
-  display: flex;
-}
-.left {
-  width: 450px;
-  min-width: 450px;
-  padding: 16px;
-  background: #ffffff;
-}
-.right {
-  flex: 1;
-  min-width: 0;
-  padding: 16px;
-  margin-left: 20px;
-  background: #ffffff;
-}
-.custom-tree-node {
-  flex: 1;
-  min-width: 0;
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  font-size: 14px;
-  padding-right: 8px;
-}
-.tree-node-content {
-  flex: 1;
-  min-width: 0;
-  display: flex;
-  align-items: center;
-  height: 100%;
-  overflow: hidden;
-}
-.tree-node-content .orange-icon {
-  flex-shrink: 0;
-}
-.tree-node-label {
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-}
-.orange-icon {
-  color: orange;
-  font-size: 18px;
-  margin-right: 8px; /* 鍥炬爣涓庢枃瀛椾箣闂村姞鐐归棿璺� */
-}
-.product-tree-scroll {
-  scrollbar-width: thin;
-  scrollbar-color: #c0c4cc #f5f7fa;
-}
-.product-tree-scroll::-webkit-scrollbar {
-  width: 8px;
-}
-.product-tree-scroll::-webkit-scrollbar-track {
-  background: #f5f7fa;
-  border-radius: 4px;
-}
-.product-tree-scroll::-webkit-scrollbar-thumb {
-  background: #c0c4cc;
-  border-radius: 4px;
-}
-.product-tree-scroll::-webkit-scrollbar-thumb:hover {
-  background: #909399;
-}
+  .product-view {
+    display: flex;
+  }
+  .left {
+    width: 450px;
+    min-width: 450px;
+    padding: 16px;
+    background: #ffffff;
+  }
+  .right {
+    flex: 1;
+    min-width: 0;
+    padding: 16px;
+    margin-left: 20px;
+    background: #ffffff;
+  }
+  .custom-tree-node {
+    flex: 1;
+    min-width: 0;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    font-size: 14px;
+    padding-right: 8px;
+  }
+  .tree-node-content {
+    flex: 1;
+    min-width: 0;
+    display: flex;
+    align-items: center;
+    height: 100%;
+    overflow: hidden;
+  }
+  .tree-node-content .orange-icon {
+    flex-shrink: 0;
+  }
+  .tree-node-label {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+  .orange-icon {
+    color: orange;
+    font-size: 18px;
+    margin-right: 8px; /* 鍥炬爣涓庢枃瀛椾箣闂村姞鐐归棿璺� */
+  }
+  .product-tree-scroll {
+    scrollbar-width: thin;
+    scrollbar-color: #c0c4cc #f5f7fa;
+  }
+  .product-tree-scroll::-webkit-scrollbar {
+    width: 8px;
+  }
+  .product-tree-scroll::-webkit-scrollbar-track {
+    background: #f5f7fa;
+    border-radius: 4px;
+  }
+  .product-tree-scroll::-webkit-scrollbar-thumb {
+    background: #c0c4cc;
+    border-radius: 4px;
+  }
+  .product-tree-scroll::-webkit-scrollbar-thumb:hover {
+    background: #909399;
+  }
 </style>
diff --git a/src/views/productionManagement/productionOrder/index.vue b/src/views/productionManagement/productionOrder/index.vue
index 28356dd..4fd65f6 100644
--- a/src/views/productionManagement/productionOrder/index.vue
+++ b/src/views/productionManagement/productionOrder/index.vue
@@ -384,6 +384,21 @@
             handlePrint(row);
           },
         },
+        {
+          name: "鐢熶骇杩芥函",
+          type: "text",
+          color: "#409eff",
+          clickFun: row => {
+            router.push({
+              path: "/productionTraceability/index",
+              query: {
+                npsNo: row.npsNo,
+                productName: row.productName,
+                model: row.model,
+              },
+            });
+          },
+        },
       ],
     },
   ]);
diff --git a/src/views/productionManagement/productionTraceability/index.vue b/src/views/productionManagement/productionTraceability/index.vue
new file mode 100644
index 0000000..e2ac6df
--- /dev/null
+++ b/src/views/productionManagement/productionTraceability/index.vue
@@ -0,0 +1,831 @@
+<template>
+  <div class="app-container">
+    <el-card style="height:82vh;overflow:auto;">
+      <template #header>
+        <div class="card-header">
+          <el-form :inline="true"
+                   :model="searchForm"
+                   class="search-form">
+            <el-form-item label="鐢熶骇璁㈠崟鍙�">
+              <el-select v-model="selectedNpsNo"
+                         filterable
+                         remote
+                         reserve-keyword
+                         placeholder="璇疯緭鍏ョ敓浜ц鍗曞彿"
+                         :loading="npsNoLoading"
+                         :remote-method="handleNpsNoSearch"
+                         @change="handleSearch"
+                         style="width: 400px;">
+                <el-option v-for="option in npsNoOptions"
+                           :key="option.id"
+                           :label="option.npsNo + '-' + option.productName + '-' + option.model"
+                           :value="option.id" />
+              </el-select>
+            </el-form-item>
+            <el-form-item>
+              <el-button @click="handleBack">杩斿洖</el-button>
+            </el-form-item>
+          </el-form>
+        </div>
+      </template>
+      <!-- 鍩虹淇℃伅 -->
+      <div v-if="rowData.productionOrderDto"
+           class="detail-section">
+        <h3 class="section-title">鍩虹淇℃伅</h3>
+        <el-descriptions :column="3"
+                         border>
+          <el-descriptions-item label="鐢熶骇璁㈠崟鍙�">{{ rowData.productionOrderDto?.npsNo || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="浜у搧鍚嶇О">{{ rowData.productionOrderDto?.productName || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="浜у搧瑙勬牸">{{ rowData.productionOrderDto?.model || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="鐗╂枡缂栫爜">{{ rowData.productionOrderDto?.materialCode || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="璁″垝鏁伴噺">{{ rowData.productionOrderDto?.quantity || 0 }} <span class="unit">{{ rowData.productionOrderDto?.unit || '-' }}</span></el-descriptions-item>
+          <el-descriptions-item label="褰撳墠鐘舵��">
+            <el-tag :type="getStatusType(rowData.productionOrderDto?.status)">
+              {{ getStatusText(rowData.productionOrderDto?.status) }}
+            </el-tag>
+          </el-descriptions-item>
+          <el-descriptions-item label="瀹㈡埛鍚嶇О">{{ rowData.productionOrderDto?.customerName || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="寮�濮嬫棩鏈�">{{ rowData.productionOrderDto?.startTime || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="瀹屾垚杩涘害">
+            <el-progress :percentage="rowData.productionOrderDto?.completionStatus"
+                         :color="customColors(rowData.productionOrderDto?.completionStatus)"
+                         :status="rowData.productionOrderDto?.completionStatus === 100 ? 'success' : ''"
+                         style="width: 120px;" />
+          </el-descriptions-item>
+        </el-descriptions>
+      </div>
+      <el-empty v-else
+                description="璇锋悳绱㈢敓浜ц鍗曞彿" />
+      <!-- 鐢熶骇鎶ュ伐璁板綍 -->
+      <div v-if="rowData.productionRecords && rowData.productionRecords.length > 0"
+           class="progress-container">
+        <div class="progress-section">
+          <h3 class="section-title">宸ュ崟淇℃伅</h3>
+          <div class="order-item">
+            <el-table :data="rowData.productionRecords"
+                      border
+                      style="width: 100%">
+              <el-table-column prop="productNo"
+                               label="宸ュ崟缂栧彿"
+                               align="center">
+              </el-table-column>
+              <el-table-column prop="productName"
+                               label="浜у搧鍚嶇О"
+                               align="center" />
+              <el-table-column prop="model"
+                               label="瑙勬牸"
+                               align="center" />
+              <el-table-column prop="processName"
+                               label="宸ュ簭鍚嶇О"
+                               align="center" />
+              <el-table-column prop="requiredQuantity"
+                               label="闇�姹傛暟閲�"
+                               align="center" />
+              <el-table-column prop="completedQuantity"
+                               label="瀹屾垚鏁伴噺"
+                               align="center" />
+              <el-table-column label="璇︽儏"
+                               align="center"
+                               width="200">
+                <template #default="{ row }">
+                  <el-link @click="handleClickStep(row)"
+                           type="primary">鎶ュ伐璁板綍</el-link>
+                  <el-link @click="handleClickQuality(row)"
+                           style="margin-left:20px"
+                           type="primary">璐ㄦ淇℃伅</el-link>
+                </template>
+              </el-table-column>
+            </el-table>
+          </div>
+        </div>
+      </div>
+      <el-empty v-else-if="rowData.productionOrderDto"
+                description="鏆傛棤鎶ュ伐璁板綍" />
+    </el-card>
+    <!-- 鐢熶骇鎶ュ伐璇︽儏寮圭獥 -->
+    <el-dialog v-model="detailDialogVisible"
+               title="鐢熶骇鎶ュ伐璇︽儏"
+               width="1000px"
+               :close-on-click-modal="false"
+               custom-class="custom-dialog">
+      <div class="detail-container">
+        <!-- 鍩虹淇℃伅 -->
+        <div class="detail-section">
+          <h3 class="section-title">鍩虹淇℃伅</h3>
+          <el-descriptions :column="3"
+                           border>
+            <el-descriptions-item label="鐢熶骇宸ュ崟鍙�">{{ detailData.npsNo || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="鐝粍">
+              <el-tag :type="detailData.schedule === '鐧界彮' ? 'primary' : 'warning'">{{ detailData.schedule || '-' }}</el-tag>
+            </el-descriptions-item>
+            <el-descriptions-item label="浜у搧缂栫爜">{{ detailData.materialCode || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="浜у搧鍚嶇О">{{ detailData.productName || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="瑙勬牸">{{ detailData.model || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="鍚堟牸鏁伴噺"><span class="num2">{{ detailData.qualifiedQuantity || 0 }}</span> <span class="unit">{{ detailData.unit || '-' }}</span></el-descriptions-item>
+            <el-descriptions-item label="涓嶅悎鏍兼暟閲�"><span class="num3">{{ detailData.unqualifiedQuantity || 0 }}</span> <span class="unit">{{ detailData.unit || '-' }}</span></el-descriptions-item>
+            <el-descriptions-item label="鎬绘暟閲�"><span class="num1">{{ detailData.quantity || 0 }}</span> <span class="unit">{{ detailData.unit || '-' }}</span></el-descriptions-item>
+            <el-descriptions-item label="寮�濮嬫椂闂�">{{ detailData.reportingTime || '-' }}</el-descriptions-item>
+          </el-descriptions>
+        </div>
+        <div class="detail-section">
+          <h3 class="section-title">鎶ュ伐鏄庣粏</h3>
+          <el-table :data="[detailData]"
+                    border
+                    style="width: 100%">
+            <el-table-column label="鎶ュ伐鍗曞彿"
+                             prop="productNo"
+                             align="center" />
+            <el-table-column label="浜у嚭鏁伴噺"
+                             prop="qualifiedQuantity"
+                             align="center" />
+            <el-table-column label="鎶ュ簾鏁伴噺"
+                             prop="unqualifiedQuantity"
+                             align="center" />
+            <el-table-column label="鍒涘缓鏃堕棿"
+                             prop="reportingTime"
+                             align="center" />
+            <el-table-column label="鎿嶄綔"
+                             align="center"
+                             width="200">
+              <template #default="{ row }">
+                <el-button type="primary"
+                           link
+                           @click="showInput(row.id)">鏌ョ湅鎶曞叆</el-button>
+                <el-button type="primary"
+                           link
+                           @click="showParamDetail(row.productionOperationParamList)">鍙傛暟璇︽儏</el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+        </div>
+      </div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="detailDialogVisible = false">鍏抽棴</el-button>
+        </div>
+      </template>
+    </el-dialog>
+    <!-- 鎶曞叆妯℃�佹 -->
+    <input-modal v-if="isShowInput"
+                 v-model:visible="isShowInput"
+                 :production-product-main-id="isShowingId" />
+    <!-- 鍙傛暟璇︽儏寮圭獥 -->
+    <el-dialog v-model="paramDetailVisible"
+               title="鍙傛暟璇︽儏"
+               width="600px">
+      <div v-if="currentParams && currentParams.length > 0"
+           class="param-detail-list">
+        <el-descriptions :column="1"
+                         border>
+          <el-descriptions-item v-for="param in currentParams"
+                                :key="param.id"
+                                :label="param.paramName">
+            {{ param.inputValue }}
+            <span v-if="param.unit && param.unit !== '/'"
+                  class="unit-text">({{ param.unit }})</span>
+          </el-descriptions-item>
+        </el-descriptions>
+      </div>
+      <el-empty v-else
+                description="鏆傛棤鍙傛暟鏁版嵁" />
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="paramDetailVisible = false">鍏抽棴</el-button>
+        </span>
+      </template>
+    </el-dialog>
+    <!-- 璐ㄦ淇℃伅寮圭獥 -->
+    <el-dialog v-model="qualityDialogVisible"
+               title="璐ㄦ璇︽儏"
+               width="1000px"
+               :close-on-click-modal="false"
+               custom-class="custom-dialog">
+      <div class="detail-container">
+        <div v-for="(record, index) in qualityRecords"
+             :key="record.id"
+             class="quality-record-block">
+          <div class="detail-section">
+            <h3 class="section-title">妫�娴嬭褰� {{ index + 1 }} - {{ record.checkTime }}</h3>
+            <el-descriptions :column="3"
+                             border>
+              <el-descriptions-item label="妫�娴嬫棩鏈�">{{ record.checkTime || '-' }}</el-descriptions-item>
+              <el-descriptions-item label="鐢熶骇宸ュ崟鍙�">{{ record.workOrderNo || '-' }}</el-descriptions-item>
+              <el-descriptions-item label="宸ュ簭">{{ record.process || '-' }}</el-descriptions-item>
+              <el-descriptions-item label="妫�楠屽憳">{{ record.checkName || '-' }}</el-descriptions-item>
+              <el-descriptions-item label="浜у搧鍚嶇О">{{ record.productName || '-' }}</el-descriptions-item>
+              <el-descriptions-item label="瑙勬牸鍨嬪彿">{{ record.model || '-' }}</el-descriptions-item>
+              <el-descriptions-item label="鏁伴噺">{{ record.quantity || 0 }} {{ record.unit || '-' }}</el-descriptions-item>
+              <el-descriptions-item label="妫�娴嬪崟浣�">{{ record.checkCompany || '-' }}</el-descriptions-item>
+              <el-descriptions-item label="妫�娴嬬粨鏋�">
+                <el-tag :type="record.checkResult === '鍚堟牸' ? 'success' : 'danger'">
+                  {{ record.checkResult || '-' }}
+                </el-tag>
+              </el-descriptions-item>
+            </el-descriptions>
+            <h4 class="sub-section-title">妫�楠屾寚鏍囧垪琛�</h4>
+            <el-table :data="record.inspectItems"
+                      border
+                      style="width: 100%">
+              <el-table-column label="搴忓彿"
+                               type="index"
+                               width="60"
+                               align="center" />
+              <el-table-column label="鎸囨爣"
+                               prop="itemName"
+                               align="center" />
+              <el-table-column label="鍗曚綅"
+                               prop="unit"
+                               align="center" />
+              <el-table-column label="鏍囧噯鍊�"
+                               prop="standardValue"
+                               align="center" />
+              <el-table-column label="鍐呮帶鍊�"
+                               prop="controlValue"
+                               align="center" />
+              <el-table-column label="瀹為檯鍊�"
+                               prop="actualValue"
+                               align="center" />
+            </el-table>
+          </div>
+          <!-- <div class="detail-section">
+
+          </div> -->
+          <el-divider v-if="index < qualityRecords.length - 1" />
+        </div>
+      </div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="qualityDialogVisible = false">鍏抽棴</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+  import { ref, reactive, onMounted } from "vue";
+  import { useRoute, useRouter } from "vue-router";
+  import { ElMessage } from "element-plus";
+  import InputModal from "@/views/productionManagement/productionReporting/Input.vue";
+
+  const route = useRoute();
+  const router = useRouter();
+
+  // 鎼滅储鐩稿叧
+  const searchForm = reactive({
+    npsNo: "",
+  });
+  const selectedNpsNo = ref(null);
+  const npsNoLoading = ref(false);
+  const npsNoOptions = ref([
+    {
+      id: 1,
+      npsNo: "PO20240301001",
+      productName: "绮惧瘑娑插帇缂�",
+      model: "HG-100/50-500",
+      materialCode: "MAT-2024-001",
+      quantity: 500,
+      unit: "浠�",
+      status: 1,
+      customerName: "閲嶅伐鏈烘鏈夐檺鍏徃",
+      startTime: "2024-03-01",
+      completionStatus: 65,
+    },
+    {
+      id: 2,
+      npsNo: "PO20240301002",
+      productName: "宸ヤ笟浼烘湇鐢垫満",
+      model: "SV-400W-3000",
+      materialCode: "MAT-2024-002",
+      quantity: 200,
+      unit: "鍙�",
+      status: 2,
+      customerName: "鑷姩鍖栬澶囩鎶�鍏徃",
+      startTime: "2024-03-02",
+      completionStatus: 100,
+    },
+    {
+      id: 3,
+      npsNo: "PO20240301003",
+      productName: "楂樺帇瀵嗗皝鍦�",
+      model: "SR-80-5",
+      materialCode: "MAT-2024-003",
+      quantity: 5000,
+      unit: "涓�",
+      status: 0,
+      customerName: "瀵嗗皝绯荤粺閰嶄欢鍘�",
+      startTime: "2024-03-05",
+      completionStatus: 0,
+    },
+  ]);
+
+  // 璇︽儏鏁版嵁
+  const rowData = reactive({
+    productionOrderDto: null,
+    productionRecords: [],
+  });
+
+  // 鎶ュ伐璇︽儏寮圭獥
+  const detailDialogVisible = ref(false);
+  const detailData = ref({});
+
+  // 鎶曞叆妯℃�佹
+  const isShowInput = ref(false);
+  const isShowingId = ref(0);
+  const showInput = id => {
+    isShowInput.value = true;
+    isShowingId.value = id;
+  };
+
+  // 鍙傛暟璇︽儏寮圭獥
+  const paramDetailVisible = ref(false);
+  const currentParams = ref([]);
+  const showParamDetail = params => {
+    currentParams.value = params || [];
+    paramDetailVisible.value = true;
+  };
+
+  // 璐ㄦ淇℃伅寮圭獥
+  const qualityDialogVisible = ref(false);
+  const qualityRecords = ref([]);
+
+  // 鐘舵�佸鐞�
+  const getStatusType = status => {
+    const typeMap = { 0: "info", 1: "primary", 2: "success" };
+    return typeMap[status] || "info";
+  };
+  const getStatusText = status => {
+    const statusMap = { 0: "鏈紑濮�", 1: "鐢熶骇涓�", 2: "宸插畬鎴�" };
+    return statusMap[status] || "鏈煡";
+  };
+  const customColors = percentage => {
+    if (percentage < 30) return "#f56c6c";
+    if (percentage < 70) return "#e6a23c";
+    return "#67c23a";
+  };
+
+  // 妯℃嫙鎼滅储鏂规硶
+  const handleNpsNoSearch = query => {
+    if (query) {
+      npsNoLoading.value = true;
+      setTimeout(() => {
+        npsNoLoading.value = false;
+      }, 300);
+    }
+  };
+
+  const handleSearch = id => {
+    const selected = npsNoOptions.value.find(item => item.id === id);
+    if (selected) {
+      rowData.productionOrderDto = selected;
+      rowData.productionRecords = [
+        {
+          id: 1001,
+          productNo: "MO-2024-001-01",
+          productName: selected.productName,
+          model: selected.model,
+          processName: "姣涘澂鍔犲伐",
+          requiredQuantity: selected.quantity,
+          completedQuantity: Math.floor(selected.quantity * 0.4),
+          qualifiedQuantity: Math.floor(selected.quantity * 0.4) - 2,
+          unqualifiedQuantity: 2,
+          reportingTime: "2024-03-01 10:00:00",
+          schedule: "鐧界彮",
+          postName: "寮犱笁",
+          unit: selected.unit,
+        },
+        {
+          id: 1002,
+          productNo: "MO-2024-001-02",
+          productName: selected.productName,
+          model: selected.model,
+          processName: "绮惧姞宸�",
+          requiredQuantity: Math.floor(selected.quantity * 0.4),
+          completedQuantity: Math.floor(selected.quantity * 0.25),
+          qualifiedQuantity: Math.floor(selected.quantity * 0.25),
+          unqualifiedQuantity: 0,
+          reportingTime: "2024-03-01 16:00:00",
+          schedule: "鐧界彮",
+          postName: "鏉庡洓",
+          unit: selected.unit,
+        },
+      ];
+    }
+  };
+
+  const handleBack = () => {
+    router.back();
+  };
+
+  const handleClickStep = row => {
+    detailData.value = {
+      id: row.id || Math.floor(Math.random() * 1000),
+      productNo: row.productNo,
+      npsNo: rowData.productionOrderDto.npsNo,
+      schedule: row.schedule,
+      postName: row.postName,
+      materialCode: rowData.productionOrderDto.materialCode,
+      productName: row.productName,
+      model: row.model,
+      qualifiedQuantity: row.qualifiedQuantity,
+      unqualifiedQuantity: row.unqualifiedQuantity || 0,
+      quantity: row.completedQuantity,
+      unit: row.unit,
+      reportingTime: row.reportingTime,
+      productionOperationParamList: [
+        { id: 1, paramName: "涓昏酱杞��", inputValue: "2400", unit: "rpm" },
+        { id: 2, paramName: "杩涚粰閫熷害", inputValue: "120", unit: "mm/min" },
+        { id: 3, paramName: "鍒囧墛娣卞害", inputValue: "0.5", unit: "mm" },
+        { id: 4, paramName: "鍐峰嵈娑插帇鍔�", inputValue: "0.6", unit: "Mpa" },
+      ],
+    };
+    detailDialogVisible.value = true;
+  };
+
+  const handleClickQuality = row => {
+    qualityRecords.value = [
+      {
+        id: 2001,
+        checkTime: "2024-03-01 11:30:00",
+        workOrderNo: row.productNo,
+        process: row.processName,
+        checkName: "璐ㄩ噺閮�-鐜嬪缓鍥�",
+        productName: row.productName,
+        model: row.model,
+        unit: row.unit,
+        quantity: row.completedQuantity,
+        checkCompany: "鍐呴儴瀹為獙瀹�",
+        checkResult: "鍚堟牸",
+        inspectItems: [
+          {
+            id: 1,
+            itemName: "澶栧緞灏哄",
+            unit: "mm",
+            standardValue: "100.00卤0.05",
+            controlValue: "100.00卤0.03",
+            actualValue: "100.01",
+            result: "鍚堟牸",
+          },
+          {
+            id: 2,
+            itemName: "鍐呭緞灏哄",
+            unit: "mm",
+            standardValue: "50.00+0.02/-0",
+            controlValue: "50.00+0.01/-0",
+            actualValue: "50.01",
+            result: "鍚堟牸",
+          },
+          {
+            id: 3,
+            itemName: "琛ㄩ潰绮楃硻搴�",
+            unit: "Ra",
+            standardValue: "鈮�1.6",
+            controlValue: "鈮�1.2",
+            actualValue: "0.8",
+            result: "鍚堟牸",
+          },
+        ],
+      },
+      {
+        id: 2001,
+        checkTime: "2024-03-01 11:30:00",
+        workOrderNo: row.productNo,
+        process: row.processName,
+        checkName: "璐ㄩ噺閮�-鐜嬪缓鍥�",
+        productName: row.productName,
+        model: row.model,
+        unit: row.unit,
+        quantity: row.completedQuantity,
+        checkCompany: "鍐呴儴瀹為獙瀹�",
+        checkResult: "鍚堟牸",
+        inspectItems: [
+          {
+            id: 1,
+            itemName: "澶栧緞灏哄",
+            unit: "mm",
+            standardValue: "100.00卤0.05",
+            controlValue: "100.00卤0.03",
+            actualValue: "100.01",
+            result: "鍚堟牸",
+          },
+          {
+            id: 2,
+            itemName: "鍐呭緞灏哄",
+            unit: "mm",
+            standardValue: "50.00+0.02/-0",
+            controlValue: "50.00+0.01/-0",
+            actualValue: "50.01",
+            result: "鍚堟牸",
+          },
+          {
+            id: 3,
+            itemName: "琛ㄩ潰绮楃硻搴�",
+            unit: "Ra",
+            standardValue: "鈮�1.6",
+            controlValue: "鈮�1.2",
+            actualValue: "0.8",
+            result: "鍚堟牸",
+          },
+        ],
+      },
+    ];
+    qualityDialogVisible.value = true;
+  };
+
+  onMounted(() => {
+    if (route.query.npsNo) {
+      const npsNo = route.query.npsNo;
+      const found = npsNoOptions.value.find(item => item.npsNo === npsNo);
+      if (found) {
+        selectedNpsNo.value = found.id;
+        handleSearch(found.id);
+      } else {
+        // 濡傛灉娌℃壘鍒帮紝鍒涘缓涓�涓复鏃剁殑
+        const mockItem = {
+          id: Date.now(),
+          npsNo: npsNo,
+          productName: route.query.productName || "绮惧瘑娑插帇缂�",
+          model: route.query.model || "HG-100/50-500",
+          materialCode: "MAT-2024-MOCK",
+          quantity: 100,
+          unit: "浠�",
+          status: 1,
+          customerName: "妯℃嫙瀹㈡埛",
+          startTime: "2024-03-01",
+          completionStatus: 50,
+        };
+        npsNoOptions.value.push(mockItem);
+        selectedNpsNo.value = mockItem.id;
+        handleSearch(mockItem.id);
+      }
+    }
+  });
+</script>
+
+<style scoped>
+  .app-container {
+    padding: 20px;
+    background-color: #f5f7fa;
+    min-height: 100vh;
+  }
+
+  .card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 0 10px;
+  }
+
+  .search-form {
+    width: 100%;
+  }
+
+  .search-form .el-form-item {
+    margin-right: 10px;
+  }
+
+  .detail-section {
+    margin-bottom: 24px;
+    background-color: #ffffff;
+    border-radius: 10px;
+    padding: 24px;
+    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.08);
+    transition: all 0.3s ease;
+  }
+
+  .detail-section:hover {
+    box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.12);
+  }
+
+  .section-title {
+    font-size: 16px;
+    font-weight: 600;
+    margin-bottom: 20px;
+    color: #1a1a1a;
+    border-bottom: 2px solid #409eff;
+    padding-bottom: 10px;
+    display: flex;
+    align-items: center;
+  }
+
+  .section-title::before {
+    content: "";
+    display: inline-block;
+    width: 4px;
+    height: 16px;
+    background-color: #409eff;
+    margin-right: 8px;
+    border-radius: 2px;
+  }
+
+  .sub-section-title {
+    font-size: 14px;
+    font-weight: 600;
+    margin-bottom: 16px;
+    color: #303133;
+    display: flex;
+    align-items: center;
+  }
+
+  .sub-section-title::before {
+    content: "";
+    display: inline-block;
+    width: 3px;
+    height: 12px;
+    background-color: #67c23a;
+    margin-right: 8px;
+    border-radius: 2px;
+  }
+
+  .unit {
+    font-size: 12px;
+    color: #909399;
+    margin-left: 4px;
+  }
+
+  :deep(.el-descriptions) {
+    border-radius: 8px;
+    overflow: hidden;
+    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
+  }
+
+  :deep(.el-descriptions__row:nth-child(odd)) {
+    background-color: #f9f9f9;
+  }
+
+  :deep(.el-descriptions__label) {
+    font-weight: 500;
+    color: #606266;
+    background-color: #f5f7fa;
+  }
+
+  :deep(.el-descriptions__content) {
+    color: #303133;
+    font-weight: 500;
+  }
+
+  .progress-container {
+    display: flex;
+    gap: 24px;
+  }
+
+  .progress-section {
+    margin-bottom: 24px;
+    background-color: #ffffff;
+    border-radius: 10px;
+    padding: 24px;
+    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.08);
+    flex: 1;
+    transition: all 0.3s ease;
+    width: 100%;
+  }
+
+  .progress-section:hover {
+    box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.12);
+  }
+
+  .order-item {
+    margin-bottom: 20px;
+    border-radius: 8px;
+    overflow: hidden;
+    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
+  }
+
+  :deep(.el-table) {
+    border-radius: 8px;
+    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
+  }
+
+  :deep(.el-table th) {
+    background-color: #f5f7fa !important;
+    font-weight: 600;
+    color: #606266;
+  }
+
+  :deep(.el-progress-bar__inner) {
+    border-radius: 10px;
+  }
+
+  :deep(.el-tag) {
+    border-radius: 12px;
+    padding: 2px 10px;
+  }
+
+  /* 寮圭獥鏍峰紡 */
+  .detail-container {
+    max-height: 600px;
+    overflow-y: auto;
+    padding: 0 16px;
+  }
+
+  .process-item {
+    margin-bottom: 24px;
+    padding: 20px;
+    background-color: #ffffff;
+    border-radius: 8px;
+    border: 1px solid #ebeef5;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+  }
+
+  .process-header {
+    margin-bottom: 20px;
+    padding-bottom: 12px;
+    border-bottom: 1px solid #f0f2f5;
+  }
+
+  .process-title {
+    font-size: 15px;
+    font-weight: 600;
+    margin-bottom: 12px;
+    color: #1a1a1a;
+    display: flex;
+    align-items: center;
+  }
+
+  .process-title::before {
+    content: "";
+    display: inline-block;
+    width: 4px;
+    height: 16px;
+    background-color: #409eff;
+    margin-right: 8px;
+    border-radius: 2px;
+  }
+
+  .process-info {
+    display: flex;
+    gap: 20px;
+    font-size: 13px;
+    color: #606266;
+  }
+
+  .process-label {
+    padding: 4px 12px;
+    background-color: #ecf5ff;
+    border-radius: 4px;
+    color: #409eff;
+    font-weight: 500;
+  }
+
+  .process-details {
+    margin-bottom: 20px;
+  }
+
+  .num1 {
+    color: #1107cc;
+    font-weight: 600;
+  }
+
+  .num2 {
+    color: #0fcf25;
+    font-weight: 600;
+  }
+
+  .num3 {
+    color: #d31818;
+    font-weight: 600;
+  }
+
+  .dialog-footer {
+    text-align: center;
+    padding: 20px;
+    border-top: 1px solid #ebeef5;
+  }
+
+  .dialog-footer .el-button {
+    min-width: 100px;
+    padding: 8px 20px;
+  }
+
+  /* 鑷畾涔夊璇濇鏍峰紡 */
+  :deep(.custom-dialog) {
+    border-radius: 12px;
+    overflow: hidden;
+  }
+
+  :deep(.custom-dialog .el-dialog__header) {
+    background-color: #f5f7fa;
+    padding: 20px;
+    border-bottom: 1px solid #ebeef5;
+  }
+
+  :deep(.custom-dialog .el-dialog__title) {
+    font-size: 18px;
+    font-weight: 600;
+    color: #1a1a1a;
+  }
+
+  :deep(.custom-dialog .el-dialog__body) {
+    padding: 20px;
+  }
+
+  .unit-text {
+    margin-left: 5px;
+    color: #909399;
+    font-size: 12px;
+  }
+
+  .param-detail-list {
+    padding: 10px;
+  }
+</style>

--
Gitblit v1.9.3