From d1733a6f090c0caf96c30c4b2f4f08c7d43c8d3c Mon Sep 17 00:00:00 2001
From: yyb <995253665@qq.com>
Date: 星期三, 18 三月 2026 14:35:34 +0800
Subject: [PATCH] Merge branch 'dev_银川_中盛建材' of http://114.132.189.42:9002/r/product-inventory-management into dev_银川_中盛建材

---
 src/views/productionManagement/productStructure/index.vue |  670 +++++++++++++++++++++++++++++++++++++++++++++++--------
 1 files changed, 572 insertions(+), 98 deletions(-)

diff --git a/src/views/productionManagement/productStructure/index.vue b/src/views/productionManagement/productStructure/index.vue
index e32ff8d..7032c09 100644
--- a/src/views/productionManagement/productStructure/index.vue
+++ b/src/views/productionManagement/productStructure/index.vue
@@ -1,113 +1,587 @@
 <template>
   <div class="app-container">
-    <PIMTable
-        rowKey="id"
-        :column="tableColumn"
-        :tableData="tableData"
-        :page="page"
-        :isSelection="true"
-        @selection-change="handleSelectionChange"
-        :tableLoading="tableLoading"
-        @pagination="pagination"
-    >
-      <template #detail="{row}">
-        <el-button
-            type="primary"
-            text
-            @click="showDetail(row.id)">{{ row.productName }}
+    <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
+      <!-- 鎼滅储鍖哄煙 -->
+      <div class="search-form">
+        <el-form :inline="true"
+                 :model="searchForm">
+          <el-form-item label="浜у搧绫诲瀷"
+                        style="margin-bottom: 0px !important;">
+            <el-select v-model="searchForm.dictCode"
+                       style="width: 200px;"
+                       placeholder="璇烽�夋嫨浜у搧绫诲瀷"
+                       clearable>
+              <el-option v-for="option in productTypeOptions"
+                         :key="option.dictCode"
+                         :label="option.dictLabel"
+                         :value="option.dictCode" />
+            </el-select>
+          </el-form-item>
+          <el-form-item style="margin-bottom: 0px !important;">
+            <el-button type="primary"
+                       @click="handleSearch">鎼滅储</el-button>
+            <el-button @click="handleReset">閲嶇疆</el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+      <!-- 鎸夐挳鍖哄煙 -->
+      <div>
+        <el-button type="info"
+                   plain
+                   icon="Upload"
+                   @click="handleImport"
+                   v-hasPermi="['product:bom:import']">瀵煎叆</el-button>
+        <el-button type="warning"
+                   plain
+                   icon="Download"
+                   @click="handleExport"
+                   :disabled="selectedRows.length !== 1"
+                   v-hasPermi="['product:bom:export']">瀵煎嚭</el-button>
+        <el-button type="primary"
+                   @click="handleAdd">鏂板</el-button>
+        <el-button type="danger"
+                   plain
+                   @click="handleBatchDelete"
+                   :disabled="selectedRows.length === 0">鍒犻櫎</el-button>
+      </div>
+    </div>
+    <PIMTable rowKey="id"
+              :column="tableColumn"
+              :tableData="tableData"
+              :page="page"
+              :isSelection="true"
+              @selection-change="handleSelectionChange"
+              :tableLoading="tableLoading"
+              @pagination="pagination">
+      <template #detail="{ row }">
+        <el-button type="primary"
+                   text
+                   @click="showDetail(row)">{{ row.bomNo }}
         </el-button>
       </template>
     </PIMTable>
-    <StructureEdit v-if="showEdit" v-model:show-model="showEdit" :record="currentRow"/>
+    <StructureEdit v-if="showEdit"
+                   v-model:show-model="showEdit"
+                   :record="currentRow" />
+    <!-- 鏂板/缂栬緫寮圭獥 -->
+    <el-dialog v-model="dialogVisible"
+               :title="operationType === 'add' ? '鏂板BOM' : '缂栬緫BOM'"
+               width="600px"
+               @close="closeDialog">
+      <el-form ref="formRef"
+               :model="form"
+               :rules="rules"
+               label-width="120px">
+        <el-form-item label="浜у搧绫诲瀷"
+                      prop="dictCode">
+          <el-select v-model="form.dictCode"
+                     placeholder="璇烽�夋嫨浜у搧绫诲瀷"
+                     clearable>
+            <el-option v-for="option in productTypeOptions"
+                       :key="option.dictCode"
+                       :label="option.dictLabel"
+                       :value="option.dictCode" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="鐗堟湰鍙�"
+                      prop="version">
+          <el-input v-model="form.version"
+                    placeholder="璇疯緭鍏ョ増鏈彿"
+                    clearable />
+        </el-form-item>
+        <el-form-item label="澶囨敞"
+                      prop="remark">
+          <el-input v-model="form.remark"
+                    type="textarea"
+                    :rows="3"
+                    placeholder="璇疯緭鍏ュ娉�"
+                    clearable />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="closeDialog">鍙栨秷</el-button>
+        <el-button type="primary"
+                   @click="handleSubmit">纭畾</el-button>
+      </template>
+    </el-dialog>
+    <!-- BOM瀵煎叆瀵硅瘽妗� -->
+    <ImportDialog ref="uploadRef"
+                  v-model="upload.open"
+                  :title="upload.title"
+                  :action="upload.url"
+                  :headers="upload.headers"
+                  :disabled="upload.isUploading"
+                  :on-progress="handleFileUploadProgress"
+                  :on-success="handleFileSuccess"
+                  :show-download-template="true"
+                  :show-product-type-select="true"
+                  @confirm="submitFileForm"
+                  @download-template="handleDownloadTemplate"
+                  @close="handleImportClose" />
   </div>
 </template>
 
 <script setup>
-import {ref} from "vue";
-import {productModelList} from "@/api/basicData/productModel.js";
-import { useRouter } from 'vue-router'
+  import {
+    ref,
+    reactive,
+    toRefs,
+    onMounted,
+    getCurrentInstance,
+    defineAsyncComponent,
+  } from "vue";
+  import { getToken } from "@/utils/auth";
+  import {
+    listPage,
+    add,
+    update,
+    batchDelete,
+    exportBom,
+    downloadTemplate,
+  } 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 "./ImportDialog.vue";
+  import { getDicts } from "@/api/system/dict/data";
 
-const router = useRouter()
-const StructureEdit = defineAsyncComponent(() => import('@/views/productionManagement/productStructure/StructureEdit.vue'))
+  const router = useRouter();
+  const { proxy } = getCurrentInstance();
+  const StructureEdit = defineAsyncComponent(() =>
+    import("@/views/productionManagement/productStructure/StructureEdit.vue")
+  );
 
-const tableColumn = ref([
-  {
-    label: "浜у搧缂栫爜",
-    prop: "productCode",
-    slot: "detail"
-  },
-  {
-    label: "浜у搧鍚嶇О",
-    prop: "productName",
-    dataType: 'slot',
-    slot: "detail"
-  },
-  {
-    label: "瑙勬牸鍨嬪彿",
-    prop: "model",
-  },
-  {
-    label: "鍗曚綅",
-    prop: "unit",
-  }
-]);
-const tableData = ref([]);
-const tableLoading = ref(false);
-const showEdit = ref(false);
-const selectedRows = ref([]);
-const currentRow = ref({});
-const page = reactive({
-  current: 1,
-  size: 10,
-  total: 0,
-});
-const data = reactive({
-  form: {
-    productName: "",
-  },
-  rules: {
-    productName: [{required: true, message: "璇疯緭鍏�", trigger: "blur"}],
-  },
-  modelForm: {
-    otherModel: '',
-    model: "",
-    unit: "",
-    speculativeTradingName: [],
-  },
-});
-const {form, rules} = toRefs(data);
-// 琛ㄦ牸閫夋嫨鏁版嵁
-const handleSelectionChange = (selection) => {
-  selectedRows.value = selection;
-};
+  const tableColumn = ref([
+    {
+      label: "BOM缂栧彿",
+      prop: "bomNo",
+      dataType: "slot",
+      slot: "detail",
+      minWidth: 140,
+    },
+    {
+      label: "浜у搧绫诲瀷",
+      prop: "dictLabel",
+      dataType: "tag",
+      formatType: params => {
+        if (params == "3.5鐮屽潡") {
+          return "warning";
+        } else if (params == "5.0鐮屽潡") {
+          return "primary";
+        } else if (params == "鏉挎潗") {
+          return "success";
+        } else {
+          return "info";
+        }
+      },
+    },
+    // {
+    //   label: "浜у搧缂栫爜",
+    //   prop: "productCode",
 
-// 鏌ヨ瑙勬牸鍨嬪彿
-const pagination = (obj) => {
-  page.current = obj.page;
-  page.size = obj.limit;
-  getModelList();
-};
+    //   minWidth: 160,
+    // },
+    // {
+    //   label: "浜у搧鍚嶇О",
+    //   prop: "productName",
 
-const showDetail = (id) => {
-  router.push({
-    path: '/productionManagement/productStructureDetail',
-    query: {
-      id: id
-    }
-  })
-}
-const getModelList = () => {
-  tableLoading.value = true;
-  productModelList({
-    current: page.current,
-    size: page.size,
-  }).then((res) => {
-    tableData.value = res.records;
-    page.total = res.total;
-    tableLoading.value = false;
+    //   minWidth: 160,
+    // },
+    // {
+    //   label: "瑙勬牸鍨嬪彿",
+    //   prop: "productModelName",
+    //   minWidth: 140,
+    // },
+    {
+      label: "鐗堟湰鍙�",
+      prop: "version",
+    },
+    {
+      label: "澶囨敞",
+      prop: "remark",
+      minWidth: 160,
+    },
+    {
+      dataType: "action",
+      label: "鎿嶄綔",
+      align: "center",
+      fixed: "right",
+      width: 150,
+      operation: [
+        {
+          name: "缂栬緫",
+          type: "text",
+          clickFun: row => {
+            handleEdit(row);
+          },
+        },
+        {
+          name: "鍒犻櫎",
+          type: "danger",
+          link: true,
+          clickFun: row => {
+            handleDelete(row);
+          },
+        },
+      ],
+    },
+  ]);
+
+  const tableData = ref([]);
+  const tableLoading = ref(false);
+  const showEdit = ref(false);
+  const selectedRows = ref([]);
+  const currentRow = ref({});
+  const dialogVisible = ref(false);
+  const operationType = ref("add"); // add | edit
+  const formRef = ref(null);
+  const showProductSelectDialog = ref(false);
+
+  //  BOM瀵煎叆鍙傛暟
+  const upload = reactive({
+    // 鏄惁鏄剧ず寮瑰嚭灞傦紙BOM瀵煎叆锛�
+    open: false,
+    // 寮瑰嚭灞傛爣棰橈紙BOM瀵煎叆锛�
+    title: "",
+    // 鏄惁绂佺敤涓婁紶
+    isUploading: false,
+    // 璁剧疆涓婁紶鐨勮姹傚ご閮�
+    headers: { Authorization: "Bearer " + getToken() },
+    // 涓婁紶鐨勫湴鍧�
+    url: import.meta.env.VITE_APP_BASE_API + "/productBom/uploadBom",
   });
-};
-onMounted(() => {
-  getModelList();
-})
+
+  const page = reactive({
+    current: 1,
+    size: 10,
+    total: 0,
+  });
+
+  const data = reactive({
+    form: {
+      id: undefined,
+      dictCode: "",
+      remark: "",
+      version: "",
+    },
+    rules: {
+      dictCode: [
+        { required: true, message: "璇烽�夋嫨浜у搧绫诲瀷", trigger: "change" },
+      ],
+      version: [{ required: true, message: "璇疯緭鍏ョ増鏈彿", trigger: "blur" }],
+    },
+    searchForm: {
+      dictCode: "",
+    },
+    productTypeOptions: [],
+  });
+
+  const { form, rules, searchForm, productTypeOptions } = toRefs(data);
+
+  // 琛ㄦ牸閫夋嫨鏁版嵁
+  const handleSelectionChange = selection => {
+    selectedRows.value = selection;
+  };
+
+  // 鍒嗛〉
+  const pagination = obj => {
+    page.current = obj.page;
+    page.size = obj.limit;
+    getList();
+  };
+
+  // 鑾峰彇浜у搧绫诲瀷瀛楀吀
+  const getProductTypeOptions = () => {
+    getDicts("product_type")
+      .then(res => {
+        if (res.code === 200) {
+          productTypeOptions.value = res.data;
+        }
+      })
+      .catch(err => {
+        console.error("鑾峰彇浜у搧绫诲瀷瀛楀吀澶辫触锛�", err);
+      });
+  };
+
+  // 鎼滅储
+  const handleSearch = () => {
+    getList();
+  };
+
+  // 閲嶇疆
+  const handleReset = () => {
+    searchForm.value.dictCode = "";
+    getList();
+  };
+
+  // 鏌ヨ鍒楄〃
+  const getList = () => {
+    tableLoading.value = true;
+    listPage({
+      current: page.current,
+      size: page.size,
+      dictCode: searchForm.value.dictCode,
+    })
+      .then(res => {
+        const records = res?.data?.records || [];
+        tableData.value = records;
+        page.total = res?.data?.total || 0;
+      })
+      .catch(err => {
+        console.error("鑾峰彇鍒楄〃澶辫触锛�", err);
+      })
+      .finally(() => {
+        tableLoading.value = false;
+      });
+  };
+
+  // 鏂板
+  const handleAdd = () => {
+    operationType.value = "add";
+    Object.assign(form.value, {
+      id: undefined,
+      dictCode: "",
+      remark: "",
+      version: "",
+    });
+    dialogVisible.value = true;
+  };
+
+  // 缂栬緫
+  const handleEdit = row => {
+    operationType.value = "edit";
+    Object.assign(form.value, {
+      id: row.id,
+      dictCode: row.dictCode || "",
+      remark: row.remark || "",
+      version: row.version || "",
+    });
+    dialogVisible.value = true;
+  };
+
+  // 鍒犻櫎锛堝崟鏉★級
+  const handleDelete = row => {
+    ElMessageBox.confirm("纭鍒犻櫎璇OM锛�", "鎻愮ず", {
+      confirmButtonText: "纭",
+      cancelButtonText: "鍙栨秷",
+      type: "warning",
+    })
+      .then(() => {
+        batchDelete([row.id])
+          .then(() => {
+            proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+            getList();
+          })
+          .catch(() => {
+            proxy.$modal.msgError("鍒犻櫎澶辫触");
+          });
+      })
+      .catch(() => {});
+  };
+
+  // 鎵归噺鍒犻櫎
+  const handleBatchDelete = () => {
+    if (!selectedRows.value.length) {
+      proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+      return;
+    }
+    const ids = selectedRows.value.map(item => item.id);
+    ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎鎻愮ず", {
+      confirmButtonText: "纭",
+      cancelButtonText: "鍙栨秷",
+      type: "warning",
+    })
+      .then(() => {
+        batchDelete(ids)
+          .then(() => {
+            proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+            getList();
+          })
+          .catch(() => {
+            proxy.$modal.msgError("鍒犻櫎澶辫触");
+          });
+      })
+      .catch(() => {});
+  };
+
+  // 浜у搧閫夋嫨
+  const handleProductSelect = products => {
+    if (products && products.length > 0) {
+      const product = products[0];
+      form.value.productModelId = product.id;
+      form.value.productName = product.productName;
+      form.value.productModelName = product.model;
+    }
+    showProductSelectDialog.value = false;
+  };
+
+  // 鎻愪氦琛ㄥ崟
+  const handleSubmit = () => {
+    formRef.value.validate(valid => {
+      if (valid) {
+        const payload = { ...form.value };
+        if (operationType.value === "add") {
+          add(payload)
+            .then(() => {
+              proxy.$modal.msgSuccess("鏂板鎴愬姛");
+              closeDialog();
+              getList();
+            })
+            .catch(() => {
+              proxy.$modal.msgError("鏂板澶辫触");
+            });
+        } else {
+          update(payload)
+            .then(() => {
+              proxy.$modal.msgSuccess("淇敼鎴愬姛");
+              closeDialog();
+              getList();
+            })
+            .catch(() => {
+              proxy.$modal.msgError("淇敼澶辫触");
+            });
+        }
+      }
+    });
+  };
+
+  // 鍏抽棴寮圭獥
+  const closeDialog = () => {
+    dialogVisible.value = false;
+    formRef.value?.resetFields();
+  };
+
+  //  瀵煎叆鎸夐挳鎿嶄綔
+  const handleImport = () => {
+    upload.title = "BOM瀵煎叆";
+    upload.open = true;
+  };
+
+  // 鍏抽棴瀵煎叆瀵硅瘽妗嗘椂娓呴櫎鏂囦欢
+  const handleImportClose = () => {
+    proxy.$refs["uploadRef"].clearFiles();
+  };
+
+  //  鏂囦欢涓婁紶涓鐞�
+  const handleFileUploadProgress = (event, file, fileList) => {
+    upload.isUploading = true;
+  };
+
+  //  鏂囦欢涓婁紶鎴愬姛澶勭悊
+  const handleFileSuccess = (response, file, fileList) => {
+    upload.open = false;
+    upload.isUploading = false;
+    proxy.$refs["uploadRef"].clearFiles();
+    if (response.code === 200) {
+      proxy.$modal.msgSuccess(response.msg || "瀵煎叆鎴愬姛");
+      getList();
+    } else {
+      proxy.$alert(
+        "<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" +
+          response.msg +
+          "</div>",
+        "瀵煎叆缁撴灉",
+        { dangerouslyUseHTMLString: true }
+      );
+    }
+  };
+
+  // 鎻愪氦涓婁紶鏂囦欢
+  const submitFileForm = () => {
+    proxy.$refs["uploadRef"].submit();
+  };
+
+  //  瀵煎嚭鎸夐挳鎿嶄綔
+  const handleExport = () => {
+    if (selectedRows.value.length !== 1) {
+      proxy.$modal.msgWarning("璇烽�夋嫨涓�鏉℃暟鎹繘琛屽鍑�");
+      return;
+    }
+
+    const bomId = selectedRows.value[0].id;
+    const fileName = `BOM_${selectedRows.value[0].bomNo || bomId}.xlsx`;
+
+    exportBom(bomId)
+      .then(res => {
+        // 杩斿洖鐨勬暟鎹槸鍚︿负绌�
+        if (!res) {
+          proxy.$modal.msgError("瀵煎嚭澶辫触锛岃繑鍥炴暟鎹负绌�");
+          return;
+        }
+
+        const blob = new Blob([res], {
+          type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+        });
+        const downloadElement = document.createElement("a");
+        const href = window.URL.createObjectURL(blob);
+
+        downloadElement.style.display = "none";
+        downloadElement.href = href;
+        downloadElement.download = fileName;
+
+        document.body.appendChild(downloadElement);
+        downloadElement.click();
+
+        document.body.removeChild(downloadElement);
+        window.URL.revokeObjectURL(href);
+
+        proxy.$modal.msgSuccess("瀵煎嚭鎴愬姛");
+      })
+      .catch(err => {
+        console.error("瀵煎嚭寮傚父锛�", err);
+        proxy.$modal.msgError("绯荤粺寮傚父锛屽鍑哄け璐�");
+      });
+  };
+
+  //  涓嬭浇妯℃澘
+  const handleDownloadTemplate = async () => {
+    const res = await downloadTemplate();
+    // 杩斿洖鐨勬暟鎹槸鍚︿负绌�
+    if (!res) {
+      proxy.$modal.msgError("涓嬭浇澶辫触锛岃繑鍥炴暟鎹负绌�");
+      return;
+    }
+
+    const blob = new Blob([res], {
+      type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+    });
+    const downloadElement = document.createElement("a");
+    const href = window.URL.createObjectURL(blob);
+
+    downloadElement.href = href;
+    downloadElement.download = "BOM妯℃澘.xlsx";
+
+    document.body.appendChild(downloadElement);
+    downloadElement.click();
+
+    document.body.removeChild(downloadElement);
+    window.URL.revokeObjectURL(href);
+
+    proxy.$modal.msgSuccess("涓嬭浇鎴愬姛");
+  };
+
+  // 鏌ョ湅璇︽儏
+  const showDetail = row => {
+    router.push({
+      path: "/productionManagement/productStructureDetail",
+      query: {
+        id: row.id,
+        bomNo: row.bomNo || "",
+        productName: row.productName || "",
+        productModelName: row.productModelName || "",
+      },
+    });
+  };
+
+  onMounted(() => {
+    getProductTypeOptions();
+    getList();
+  });
 </script>
+<style scoped lang="scss">
+  .search-form {
+    :deep(.search-form) {
+      margin-bottom: 0px !important;
+    }
+  }
+</style>

--
Gitblit v1.9.3