From a3aed6e0d25fcdc37860f68f28525686bf150316 Mon Sep 17 00:00:00 2001
From: gongchunyi <deslre0381@gmail.com>
Date: 星期六, 24 一月 2026 16:33:24 +0800
Subject: [PATCH] feat: BOM的导入导出功能

---
 src/views/productionManagement/productStructure/index.vue |  407 ++++++++++++++++++++++++++++++++++++++++++++++++++-------
 1 files changed, 355 insertions(+), 52 deletions(-)

diff --git a/src/views/productionManagement/productStructure/index.vue b/src/views/productionManagement/productStructure/index.vue
index e32ff8d..ce88565 100644
--- a/src/views/productionManagement/productStructure/index.vue
+++ b/src/views/productionManagement/productStructure/index.vue
@@ -1,113 +1,416 @@
 <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="text-align: right; margin-bottom: 10px;">
+      <el-button type="info" plain icon="Upload" @click="handleImport">瀵煎叆</el-button>
+      <el-button type="warning" plain icon="Download" @click="handleExport"
+        :disabled="selectedRows.length !== 1">瀵煎嚭</el-button>
+      <el-button type="primary" @click="handleAdd">鏂板</el-button>
+      <el-button type="danger" plain @click="handleBatchDelete" :disabled="selectedRows.length === 0">鍒犻櫎</el-button>
+    </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="productModelId">
+          <el-button type="primary" @click="showProductSelectDialog = true">
+            {{ form.productName || '閫夋嫨浜у搧' }}
+          </el-button>
+        </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>
+
+    <!-- 浜у搧閫夋嫨寮圭獥 -->
+    <ProductSelectDialog v-model="showProductSelectDialog" @confirm="handleProductSelect" single />
+
+    <!-- BOM瀵煎叆瀵硅瘽妗� -->
+    <el-dialog :title="upload.title" v-model="upload.open" width="400px" append-to-body>
+      <el-upload ref="uploadRef" :limit="1" accept=".xlsx, .xls" :headers="upload.headers" :action="upload.url"
+        :disabled="upload.isUploading" :on-progress="handleFileUploadProgress" :on-success="handleFileSuccess"
+        :auto-upload="false" drag>
+        <el-icon class="el-icon--upload"><upload-filled /></el-icon>
+        <div class="el-upload__text">灏嗘枃浠舵嫋鍒版澶勶紝鎴�<em>鐐瑰嚮涓婁紶</em></div>
+        <template #tip>
+          <div class="el-upload__tip text-center">
+            <span>浠呭厑璁稿鍏ls銆亁lsx鏍煎紡鏂囦欢銆�</span>
+          </div>
+        </template>
+      </el-upload>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="submitFileForm">纭� 瀹�</el-button>
+          <el-button @click="upload.open = false">鍙� 娑�</el-button>
+        </div>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
 <script setup>
-import {ref} from "vue";
-import {productModelList} from "@/api/basicData/productModel.js";
+import { ref, reactive, toRefs, onMounted, getCurrentInstance, defineAsyncComponent } from "vue";
+import { getToken } from "@/utils/auth";
+import { listPage, add, update, batchDelete, exportBom } from "@/api/productionManagement/productBom.js";
 import { useRouter } from 'vue-router'
+import { ElMessageBox } from 'element-plus'
+import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.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: "BOM缂栧彿",
+    prop: "bomNo",
+    dataType: 'slot',
+    slot: "detail",
+    minWidth: 140
   },
   {
     label: "浜у搧鍚嶇О",
     prop: "productName",
-    dataType: 'slot',
-    slot: "detail"
+
+    minWidth: 160
   },
   {
     label: "瑙勬牸鍨嬪彿",
-    prop: "model",
+    prop: "productModelName",
+    minWidth: 140
   },
   {
-    label: "鍗曚綅",
-    prop: "unit",
+    label: "鐗堟湰鍙�",
+    prop: "version",
+    width: 100
+  },
+  {
+    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"
+});
+
 const page = reactive({
   current: 1,
   size: 10,
   total: 0,
 });
+
 const data = reactive({
   form: {
+    id: undefined,
     productName: "",
+    productModelName: "",
+    productModelId: "",
+    remark: "",
+    version: ""
   },
   rules: {
-    productName: [{required: true, message: "璇疯緭鍏�", trigger: "blur"}],
-  },
-  modelForm: {
-    otherModel: '',
-    model: "",
-    unit: "",
-    speculativeTradingName: [],
-  },
+    productModelId: [{ required: true, message: "璇烽�夋嫨浜у搧", trigger: "change" }],
+    version: [{ required: true, message: "璇疯緭鍏ョ増鏈彿", trigger: "blur" }]
+  }
 });
-const {form, rules} = toRefs(data);
+
+const { form, rules } = toRefs(data);
+
 // 琛ㄦ牸閫夋嫨鏁版嵁
 const handleSelectionChange = (selection) => {
   selectedRows.value = selection;
 };
 
-// 鏌ヨ瑙勬牸鍨嬪彿
+// 鍒嗛〉
 const pagination = (obj) => {
   page.current = obj.page;
   page.size = obj.limit;
-  getModelList();
+  getList();
 };
 
-const showDetail = (id) => {
+// 鏌ヨ鍒楄〃
+const getList = () => {
+  tableLoading.value = true;
+  listPage({
+    current: page.current,
+    size: page.size,
+  })
+    .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,
+    productName: "",
+    productModelName: "",
+    productModelId: "",
+    remark: "",
+    version: ""
+  });
+  dialogVisible.value = true;
+};
+
+// 缂栬緫
+const handleEdit = (row) => {
+  operationType.value = 'edit';
+  Object.assign(form.value, {
+    id: row.id,
+    productName: row.productName || "",
+    productModelName: row.productModelName || "",
+    productModelId: row.productModelId || "",
+    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 handleFileUploadProgress = (event, file, fileList) => {
+  upload.isUploading = true;
+};
+
+//  鏂囦欢涓婁紶鎴愬姛澶勭悊
+const handleFileSuccess = (response, file, fileList) => {
+  upload.open = false;
+  upload.isUploading = false;
+  proxy.$refs["uploadRef"].handleRemove(file);
+  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 showDetail = (row) => {
   router.push({
     path: '/productionManagement/productStructureDetail',
     query: {
-      id: id
+      id: row.id,
+      bomNo: row.bomNo || '',
+      productName: row.productName || '',
+      productModelName: row.productModelName || ''
     }
-  })
-}
-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;
   });
 };
+
 onMounted(() => {
-  getModelList();
-})
+  getList();
+});
 </script>

--
Gitblit v1.9.3