From e40e1cf5d4aa99412ca3a87771b5d5a8ea5a105d Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期三, 29 四月 2026 15:39:53 +0800
Subject: [PATCH] 天津军泰伟业 1.员工台账添加导入功能

---
 src/views/basicData/product/index.vue |  476 +++++++++++++++++++++++++++++++++++++++++++++++++++++++---
 1 files changed, 445 insertions(+), 31 deletions(-)

diff --git a/src/views/basicData/product/index.vue b/src/views/basicData/product/index.vue
index 62550ae..857ec8b 100644
--- a/src/views/basicData/product/index.vue
+++ b/src/views/basicData/product/index.vue
@@ -64,7 +64,46 @@
         @selection-change="handleSelectionChange"
         :tableLoading="tableLoading"
         @pagination="pagination"
-      ></PIMTable>
+      >
+        <template #drawingFiles="{ row }">
+          <div v-if="row.salesLedgerFiles && row.salesLedgerFiles.length" class="drawing-thumbs-list">
+            <div
+              v-for="(file, index) in row.salesLedgerFiles.slice(0, 3)"
+              :key="index"
+              class="drawing-thumb-item"
+              @click="handlePreviewFile(file)"
+            >
+              <img
+                v-if="isImageFile(file)"
+                :src="getDrawingFileUrl(file)"
+                class="drawing-thumb-img"
+              />
+              <div v-else class="drawing-thumb-placeholder">
+                {{ getDrawingFileExtension(file).toUpperCase() }}
+              </div>
+            </div>
+            <div v-if="row.salesLedgerFiles.length > 3" class="drawing-thumb-more">
+              +{{ row.salesLedgerFiles.length - 3 }}
+            </div>
+          </div>
+          <div v-else-if="row.drawingFile" class="drawing-thumbs-list">
+            <div
+              class="drawing-thumb-item"
+              @click="handlePreviewDrawing(row.drawingFile)"
+            >
+              <img
+                v-if="isDrawingImageFile(row.drawingFile)"
+                :src="row.drawingFile"
+                class="drawing-thumb-img"
+              />
+              <div v-else class="drawing-thumb-placeholder">
+                {{ getFileExtensionFromUrl(row.drawingFile).toUpperCase() }}
+              </div>
+            </div>
+          </div>
+          <span v-else>-</span>
+        </template>
+      </PIMTable>
     </div>
 
     <FormDialog
@@ -144,10 +183,11 @@
             :data="upload.data"
             :on-success="handleDrawingUploadSuccess"
             :on-remove="handleDrawingRemove"
+            :on-preview="handleDrawingPreview"
             :before-upload="handleDrawingBeforeUpload"
-            :limit="1"
+            :limit="5"
             accept=".pdf,.jpg,.jpeg,.png,.dwg"
-            list-type="picture-card"
+            list-type="text"
           >
             <el-icon class="avatar-uploader-icon"><Plus /></el-icon>
             <template #tip>
@@ -156,6 +196,39 @@
               </div>
             </template>
           </el-upload>
+          <div v-if="drawingFileList.length" class="drawing-preview-list">
+            <div
+              v-for="file in drawingFileList"
+              :key="file.uid || file.id || file.name"
+              class="drawing-preview-card"
+              @click="handleDrawingPreview(file)"
+            >
+              <img
+                v-if="isImageFile(file)"
+                :src="getDrawingFileUrl(file)"
+                :alt="file.name"
+                class="drawing-preview-image"
+              />
+              <div
+                v-else
+                class="drawing-preview-placeholder"
+                :class="`is-${getDrawingFileExtension(file)}`"
+              >
+                {{ getDrawingFileExtension(file).toUpperCase() }}
+              </div>
+              <div class="drawing-preview-name">{{ file.name }}</div>
+            </div>
+          </div>
+        </el-form-item>
+        <el-form-item label="澶囨敞" prop="remark">
+          <el-input
+            v-model="modelForm.remark"
+            placeholder="璇疯緭鍏ュ娉�"
+            clearable
+            type="textarea"
+            :rows="3"
+            show-word-limit
+          />
         </el-form-item>
       </el-form>
     </FormDialog>
@@ -172,6 +245,7 @@
         :limit="1"
         accept=".xlsx,.xls"
         :action="importUpload.url"
+        :http-request="importUpload.httpRequest"
         :headers="importUpload.headers"
         :before-upload="importUpload.beforeUpload"
         :on-success="importUpload.onSuccess"
@@ -193,15 +267,18 @@
         </template>
       </el-upload>
     </FormDialog>
+    <filePreview ref="filePreviewRef" />
   </div>
 </template>
 
 <script setup>
 import { ref, reactive, onMounted } from "vue";
+import axios from "axios";
 import { ElMessageBox } from "element-plus";
 import { Plus } from "@element-plus/icons-vue";
 import { getToken } from "@/utils/auth.js";
 import FormDialog from "@/components/Dialog/FormDialog.vue";
+import filePreview from "@/components/filePreview/index.vue";
 import {
   addOrEditProductModel,
   delProduct,
@@ -209,10 +286,12 @@
   downloadTemplate,
 } from "@/api/basicData/product.js";
 import { listPage as getProcessRouteList } from "@/api/productionManagement/processRoute.js";
+import { delLedgerFile } from "@/api/salesManagement/salesLedger.js";
 import ImportExcel from "./ImportExcel/index.vue";
 
 const { proxy } = getCurrentInstance();
 const importUploadRef = ref(null);
+const filePreviewRef = ref(null);
 
 const modelDia = ref(false);
 const importDia = ref(false);
@@ -259,6 +338,16 @@
     minWidth: 100,
   },
   {
+    label: "宸ヨ壓璺嚎",
+    prop: "routeName",
+    minWidth: 100,
+  },
+  {
+    label: "瀛愰」鏁伴噺",
+    prop: "subItemCount",
+    minWidth: 100,
+  },
+  {
     label: "浜у搧灞炴��",
     prop: "productType",
     width: 100,
@@ -269,6 +358,29 @@
       return typeMap[String(v)] || "info";
     },
   },
+  {
+    label: "澶囨敞",
+    prop: "remark",
+    minWidth: 150,
+    showOverflowTooltip: true,
+  },
+  {
+    label: "鍥剧焊",
+    prop: "salesLedgerFiles",
+    minWidth: 200,
+    dataType: "slot",
+    slot: "drawingFiles",
+  },
+	{
+		label: "鍒涘缓鏃堕棿",
+		prop: "createTime",
+		width: 120,
+	},
+	{
+		label: "淇敼鏃堕棿",
+		prop: "updateTime",
+		width: 120,
+	},
   {
     dataType: "action",
     label: "鎿嶄綔",
@@ -288,6 +400,7 @@
 
 const data = reactive({
   modelForm: {
+    productId: null,
     productName: "",
     model: "",
     unit: "",
@@ -296,6 +409,7 @@
     drawingFile: "",
     tempFileIds: [],
     salesLedgerFiles: [],
+    remark: "",
   },
   modelRules: {
     productName: [
@@ -309,12 +423,92 @@
 });
 const { modelForm, modelRules } = toRefs(data);
 
+const createDefaultModelForm = () => ({
+  productId: null,
+  productName: "",
+  model: "",
+  unit: "",
+  productType: null,
+  routeId: null,
+  drawingFile: "",
+  remark: "",
+  tempFileIds: [],
+  salesLedgerFiles: [],
+});
+
+const downloadImportErrorFile = (blob, filename = "import-error.xlsx") => {
+  const downloadElement = document.createElement("a");
+  const href = window.URL.createObjectURL(blob);
+  downloadElement.href = href;
+  downloadElement.download = filename;
+  document.body.appendChild(downloadElement);
+  downloadElement.click();
+  document.body.removeChild(downloadElement);
+  window.URL.revokeObjectURL(href);
+};
+
+const tryParseJsonBlob = async (blob) => {
+  try {
+    const text = await blob.text();
+    if (!text || !text.trim()) {
+      return null;
+    }
+    return JSON.parse(text);
+  } catch (_) {
+    return null;
+  }
+};
+
 const importUpload = reactive({
   title: "浜у搧瀵煎叆",
   open: false,
   url: import.meta.env.VITE_APP_BASE_API + "/basic/product/import",
   headers: { Authorization: "Bearer " + getToken() },
   isUploading: false,
+  httpRequest: async (options) => {
+    const { file, onProgress, onSuccess, onError } = options;
+    importUpload.isUploading = true;
+    const formData = new FormData();
+    formData.append("file", file);
+    try {
+      const response = await axios({
+        url: importUpload.url,
+        method: "post",
+        headers: {
+          ...importUpload.headers,
+          "Content-Type": "multipart/form-data",
+        },
+        data: formData,
+        responseType: "blob",
+        onUploadProgress: (progressEvent) => {
+          const total = progressEvent.total || 1;
+          const percent = Math.round((progressEvent.loaded * 100) / total);
+          onProgress?.({ percent }, file);
+        },
+      });
+      importUpload.isUploading = false;
+      const blob = response.data;
+      // Contract: success => empty response body; failure => binary error file.
+      if (!blob || blob.size === 0) {
+        onSuccess?.({ code: 200, msg: "import success" }, file);
+        return;
+      }
+      const json = await tryParseJsonBlob(blob);
+      if (json) {
+        if (String(json.code) === "200" || json.success === true) {
+          onSuccess?.(json, file);
+        } else {
+          onError?.(new Error(json.msg || json.message || "import failed"), file);
+        }
+        return;
+      }
+      downloadImportErrorFile(blob);
+      onError?.(new Error("import failed, error file downloaded"), file);
+    } catch (error) {
+      importUpload.isUploading = false;
+      onError?.(error, file);
+    }
+  },
   beforeUpload: (file) => {
     const isExcel = file.name.endsWith('.xlsx') || file.name.endsWith('.xls');
     const isLt10M = file.size / 1024 / 1024 < 10;
@@ -337,7 +531,7 @@
   onSuccess: (response, file, fileList) => {
     console.log('涓婁紶鎴愬姛', response, file, fileList);
     importUpload.isUploading = false;
-    if (response.code === 200) {
+    if (String(response?.code) === "200" || response?.success === true) {
       proxy.$modal.msgSuccess("瀵煎叆鎴愬姛");
       importDia.value = false;
       if (importUploadRef.value) {
@@ -371,21 +565,20 @@
 const openModelDia = (type, data) => {
   modelOperationType.value = type;
   modelDia.value = true;
-  modelForm.value.productName = "";
-  modelForm.value.model = "";
-  modelForm.value.id = "";
-  modelForm.value.unit = "";
-  modelForm.value.productType = null;
-  modelForm.value.routeId = null;
-  modelForm.value.drawingFile = "";
-  modelForm.value.tempFileIds = [];
-  modelForm.value.salesLedgerFiles = [];
+  Object.assign(modelForm.value, createDefaultModelForm());
   drawingFileList.value = [];
   if (type === "edit") {
-    modelForm.value = { ...data };
+    Object.assign(modelForm.value, data);
     modelForm.value.tempFileIds = data.tempFileIds || [];
     modelForm.value.salesLedgerFiles = data.salesLedgerFiles || [];
-    if (data.drawingFile) {
+    // 澶勭悊鍥剧焊鏂囦欢鍙嶆樉
+    if (data.salesLedgerFiles && data.salesLedgerFiles.length > 0) {
+      drawingFileList.value = data.salesLedgerFiles.map(file => ({
+        id: file.id,  // 甯︿笂id鐢ㄤ簬鍒犻櫎鏃惰皟鐢ㄦ帴鍙�
+        name: file.name,
+        url: file.url
+      }));
+    } else if (data.drawingFile) {
       drawingFileList.value = [{
         name: data.drawingFile.split('/').pop(),
         url: data.drawingFile
@@ -397,7 +590,13 @@
 const submitModelForm = () => {
   modelFormRef.value.validate((valid) => {
     if (valid) {
-      addOrEditProductModel(modelForm.value).then((res) => {
+      // 鏋勫缓鎻愪氦鏁版嵁锛岀‘淇� routeId 涓虹┖鏃朵紶 null锛屽悓鏃舵竻绌� routeName
+      const submitData = {
+        ...modelForm.value,
+        routeId: modelForm.value.routeId || 0,
+        routeName: modelForm.value.routeId ? modelForm.value.routeName : null
+      };
+      addOrEditProductModel(submitData).then((res) => {
         proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
         closeModelDia();
         getModelList();
@@ -407,7 +606,9 @@
 };
 
 const closeModelDia = () => {
-  modelFormRef.value.resetFields();
+  modelFormRef.value?.resetFields();
+  Object.assign(modelForm.value, createDefaultModelForm());
+  drawingFileList.value = [];
   modelDia.value = false;
 };
 
@@ -515,22 +716,96 @@
   console.log('涓婁紶鎴愬姛鍝嶅簲', response);
   console.log('response.data', response.data);
   if (response.code === 200) {
-    modelForm.value.tempFileIds = [response.data?.tempId];
-    modelForm.value.salesLedgerFiles = [{
+    file.url = response.data?.tempPath || file.url;
+    file.name = response.data?.originalName || file.name;
+    file.tempId = response.data?.tempId;
+    // 鏀寔澶氭枃浠讹紝杩藉姞鍒版暟缁�
+    modelForm.value.tempFileIds.push(response.data?.tempId);
+    modelForm.value.salesLedgerFiles.push({
       tempId: response.data?.tempId,
       originalName: response.data?.originalName || file.name,
       tempPath: response.data?.tempPath,
       type: response.data?.type || 13
-    }];
+    });
     proxy.$modal.msgSuccess("涓婁紶鎴愬姛");
   } else {
     proxy.$modal.msgError(response.msg || "涓婁紶澶辫触");
   }
 };
 
+const getDrawingFileUrl = (file) => {
+  return file.url || file.response?.data?.tempPath || file.tempPath || "";
+};
+
+const getDrawingFileName = (file) => {
+  return file.name || file.originalName || getDrawingFileUrl(file).split("/").pop() || "";
+};
+
+const getDrawingFileExtension = (file) => {
+  const name = getDrawingFileName(file).split("?")[0];
+  const nameParts = name.split(".");
+  return nameParts.length > 1 ? nameParts.pop().toLowerCase() : "file";
+};
+
+const isImageFile = (file) => {
+  return ["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(getDrawingFileExtension(file));
+};
+
+const isDrawingImageFile = (url) => {
+  if (!url) return false;
+  const ext = url.split("?")[0].split(".").pop()?.toLowerCase();
+  return ["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(ext);
+};
+
+const getFileExtensionFromUrl = (url) => {
+  if (!url) return "file";
+  const cleanUrl = url.split("?")[0];
+  const parts = cleanUrl.split(".");
+  return parts.length > 1 ? parts.pop().toLowerCase() : "file";
+};
+
+const handleDrawingPreview = (file) => {
+  const fileUrl = getDrawingFileUrl(file);
+  if (!fileUrl) {
+    return;
+  }
+  filePreviewRef.value?.open(fileUrl);
+};
+
+const handlePreviewFile = (file) => {
+  const fileUrl = file.url || file.tempPath || "";
+  if (!fileUrl) {
+    return;
+  }
+  filePreviewRef.value?.open(fileUrl);
+};
+
+const handlePreviewDrawing = (drawingFile) => {
+  if (!drawingFile) {
+    return;
+  }
+  filePreviewRef.value?.open(drawingFile);
+};
+
 const handleDrawingRemove = (file) => {
-  modelForm.value.tempFileIds = [];
-  modelForm.value.salesLedgerFiles = [];
+  // 濡傛灉鏄紪杈戞ā寮忎笅宸插瓨鍦ㄧ殑鏂囦欢锛堝甫鏈塱d锛夛紝璋冪敤鍒犻櫎鎺ュ彛
+  if (file.id) {
+    delLedgerFile({ id: file.id }).then(res => {
+      if (res.code === 200) {
+        proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+      }
+    }).catch(err => {
+      console.error("鍒犻櫎鏂囦欢澶辫触锛�", err);
+    });
+  }
+  // 浠庢暟缁勪腑绉婚櫎瀵瑰簲鐨勬枃浠�
+  const index = modelForm.value.salesLedgerFiles.findIndex(item => 
+    item.tempId === file.response?.data?.tempId || item.tempId === file.tempId
+  );
+  if (index > -1) {
+    modelForm.value.tempFileIds.splice(index, 1);
+    modelForm.value.salesLedgerFiles.splice(index, 1);
+  }
 };
 
 onMounted(() => {
@@ -579,20 +854,159 @@
 .avatar-uploader-icon {
   font-size: 28px;
   color: #8c939d;
-  width: 148px;
-  height: 148px;
+  width: 88px;
+  height: 88px;
   text-align: center;
-  line-height: 148px;
+  line-height: 88px;
 }
 
-:deep(.el-upload--picture-card) {
-  width: 148px;
-  height: 148px;
+:deep(.el-upload--text) {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  width: 88px;
+  height: 88px;
+  border: 1px dashed #dcdfe6;
+  border-radius: 8px;
 }
 
-:deep(.el-upload-list__item) {
-  width: 148px;
-  height: 148px;
+:deep(.el-upload-list--text) {
+  margin-top: 8px;
+}
+
+.drawing-preview-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 12px;
+  margin-top: 12px;
+}
+
+.drawing-preview-card {
+  width: 120px;
+  cursor: pointer;
+}
+
+.drawing-preview-image,
+.drawing-preview-placeholder {
+  width: 120px;
+  height: 120px;
+  border: 1px solid #dcdfe6;
+  border-radius: 8px;
+  background: #f5f7fa;
+}
+
+.drawing-preview-image {
+  object-fit: cover;
+}
+
+.drawing-files-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 4px;
+  align-items: center;
+}
+
+.drawing-file-tag {
+  cursor: pointer;
+  max-width: 120px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.drawing-file-tag:hover {
+  color: #409eff;
+}
+
+.drawing-file-link {
+  color: #409eff;
+  cursor: pointer;
+  text-decoration: underline;
+}
+
+.drawing-file-link:hover {
+  color: #66b1ff;
+}
+
+.drawing-thumbs-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 6px;
+  align-items: center;
+}
+
+.drawing-thumb-item {
+  width: 50px;
+  height: 50px;
+  border-radius: 4px;
+  overflow: hidden;
+  cursor: pointer;
+  border: 1px solid #dcdfe6;
+  transition: all 0.2s;
+}
+
+.drawing-thumb-item:hover {
+  border-color: #409eff;
+  box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3);
+  transform: scale(1.05);
+}
+
+.drawing-thumb-img {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+}
+
+.drawing-thumb-placeholder {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 12px;
+  font-weight: 600;
+  color: #606266;
+  background: #f5f7fa;
+}
+
+.drawing-thumb-more {
+  width: 50px;
+  height: 50px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 12px;
+  color: #909399;
+  background: #f5f7fa;
+  border-radius: 4px;
+  border: 1px dashed #dcdfe6;
+}
+
+.drawing-preview-placeholder {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 24px;
+  font-weight: 600;
+  color: #606266;
+}
+
+.drawing-preview-placeholder.is-pdf {
+  color: #f56c6c;
+  background: #fef0f0;
+}
+
+.drawing-preview-placeholder.is-dwg {
+  color: #409eff;
+  background: #ecf5ff;
+}
+
+.drawing-preview-name {
+  margin-top: 6px;
+  font-size: 12px;
+  line-height: 1.4;
+  color: #606266;
+  word-break: break-all;
 }
 
 :deep(.el-upload__tip) {

--
Gitblit v1.9.3