From f2485e01843a569549bef74aa65ac44df3cfaa3b Mon Sep 17 00:00:00 2001
From: huminmin <mac@MacBook-Pro.local>
Date: 星期二, 03 二月 2026 17:58:21 +0800
Subject: [PATCH] 库存管理-合格库存 增加条形码和二维码

---
 src/views/basicData/product/index.vue |  380 ++++++++++++++++++++++++++++++++++++++++++++++++++---
 1 files changed, 354 insertions(+), 26 deletions(-)

diff --git a/src/views/basicData/product/index.vue b/src/views/basicData/product/index.vue
index 3f0921a..4b36793 100644
--- a/src/views/basicData/product/index.vue
+++ b/src/views/basicData/product/index.vue
@@ -30,11 +30,8 @@
           :props="{ children: 'children', label: 'label' }"
           highlight-current
           node-key="id"
-          style="
-            height: calc(100vh - 190px);
-            overflow-y: scroll;
-            scrollbar-width: none;
-          "
+          class="product-tree-scroll"
+          style="height: calc(100vh - 190px); overflow-y: auto"
         >
           <template #default="{ node, data }">
             <div class="custom-tree-node">
@@ -43,7 +40,7 @@
                   <component :is="data.children && data.children.length > 0
                   ? node.expanded ? 'FolderOpened' : 'Folder' : 'Tickets'" />
                 </el-icon>
-                {{ data.label }}
+                <span class="tree-node-label">{{ data.label }}</span>
               </span>
               <div>
                 <el-button
@@ -95,7 +92,18 @@
         @selection-change="handleSelectionChange"
         :tableLoading="tableLoading"
         @pagination="pagination"
-      ></PIMTable>
+      >
+        <template #productImage="{ row }">
+          <img
+            v-if="row.url"
+            class="upload-img"
+            :src="javaApiUrl + row.url"
+            @click="previewImage(row.url)"
+            style="cursor: pointer"
+          />
+          <span v-else style="color: #909399">鏆傛棤鍥剧墖</span>
+        </template>
+      </PIMTable>
     </div>
     <el-dialog v-model="productDia" title="浜у搧" width="400px" @keydown.enter.prevent>
       <el-form
@@ -111,6 +119,8 @@
               <el-input
                 v-model="form.productName"
                 placeholder="璇疯緭鍏ヤ骇鍝佸悕绉�"
+                maxlength="20"
+                show-word-limit
                 clearable
                 @keydown.enter.prevent
               />
@@ -128,7 +138,7 @@
     <el-dialog
       v-model="modelDia"
       title="瑙勬牸鍨嬪彿"
-      width="400px"
+      width="600px"
       @close="closeModelDia"
       @keydown.enter.prevent
     >
@@ -139,8 +149,8 @@
         :rules="modelRules"
         ref="modelFormRef"
       >
-        <el-row>
-          <el-col :span="24">
+        <el-row :gutter="20">
+          <el-col :span="12">
             <el-form-item label="瑙勬牸鍨嬪彿锛�" prop="model">
               <el-input
                 v-model="modelForm.model"
@@ -150,9 +160,7 @@
               />
             </el-form-item>
           </el-col>
-        </el-row>
-        <el-row>
-          <el-col :span="24">
+          <el-col :span="12">
             <el-form-item label="鍗曚綅锛�" prop="unit">
               <el-input
                 v-model="modelForm.unit"
@@ -160,6 +168,79 @@
                 clearable
                 @keydown.enter.prevent
               />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="楂樺害锛�" prop="height">
+              <el-input
+                v-model="modelForm.height"
+                placeholder="璇疯緭鍏ラ珮搴�"
+                clearable
+                @keydown.enter.prevent
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="姣忎欢鏁伴噺/鏀細" prop="boxNum">
+              <el-input-number
+                :step="1"
+                :min="0"
+                style="width: 100%"
+                v-model="modelForm.boxNum"
+                @change="calculateTotalPrice"
+                placeholder="璇疯緭鍏ユ瘡浠舵暟閲�"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="鍗曚环(鍏�)/浠讹細" prop="taxInclusiveUnitPrice">
+              <el-input-number
+                :step="0.01"
+                :min="0"
+                style="width: 100%"
+                v-model="modelForm.taxInclusiveUnitPrice"
+                @change="calculateTotalPrice"
+                placeholder="璇疯緭鍏ュ崟浠�"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="鍗曚环(缇庡厓)/浠讹細" prop="dollarPrice">
+              <el-input-number
+                :step="0.01"
+                :min="0"
+                style="width: 100%"
+                v-model="modelForm.dollarPrice"
+                placeholder="璇疯緭鍏ョ編鍏冨崟浠�"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="24">
+            <el-form-item label="浜у搧鍥剧墖锛�" prop="url">
+              <el-upload
+                :action="uploadUrl"
+                :before-upload="handleBeforeUpload"
+                :on-success="(res, file) => handleUploadSuccess(res, file)"
+                :on-error="handleUploadError"
+                name="file"
+                :show-file-list="false"
+                :headers="headers"
+                accept="image/*"
+                :data="{ type: 13 }"
+              >
+                <img
+                  v-if="modelForm.url"
+                  class="upload-img-dialog"
+                  :src="javaApiUrl + modelForm.url"
+                />
+                <el-icon v-else class="avatar-uploader-icon-dialog"><Plus /></el-icon>
+              </el-upload>
             </el-form-item>
           </el-col>
         </el-row>
@@ -175,8 +256,10 @@
 </template>
 
 <script setup>
-import { ref } from "vue";
+import { ref, reactive, toRefs, getCurrentInstance, nextTick } from "vue";
 import { ElMessageBox } from "element-plus";
+import { Plus } from "@element-plus/icons-vue";
+import { getToken } from "@/utils/auth";
 import {
   addOrEditProduct,
   addOrEditProductModel,
@@ -186,6 +269,7 @@
   productTreeList,
 } from "@/api/basicData/product.js";
 import ImportExcel from "./ImportExcel/index.vue";
+import PIMTable from "@/components/PIMTable/PIMTable.vue";
 
 const { proxy } = getCurrentInstance();
 const tree = ref(null);
@@ -203,12 +287,40 @@
 const expandedKeys = ref([]);
 const tableColumn = ref([
   {
+    label: "浜у搧鍥剧墖",
+    prop: "url",
+    dataType: "slot",
+    slot: "productImage",
+    align: "center",
+    width: 100,
+  },
+  {
     label: "瑙勬牸鍨嬪彿",
     prop: "model",
   },
   {
     label: "鍗曚綅",
     prop: "unit",
+  },
+  {
+    label: "楂樺害",
+    prop: "height",
+    width: 120,
+  },
+  {
+    label: "姣忎欢鏁伴噺/鏀�",
+    prop: "boxNum",
+    width: 120,
+  },
+  {
+    label: "鍗曚环(鍏�)/浠�",
+    prop: "taxInclusiveUnitPrice",
+    width: 120,
+  },
+  {
+    label: "鍗曚环(缇庡厓)/浠�",
+    prop: "dollarPrice",
+    width: 130,
   },
   {
     dataType: "action",
@@ -229,6 +341,11 @@
 const tableLoading = ref(false);
 const isShowButton = ref(false);
 const selectedRows = ref([]);
+
+// 涓婁紶閰嶇疆
+const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + "/file/upload"); // 涓婁紶鐨勫浘鐗囨湇鍔″櫒鍦板潃
+const headers = ref({ Authorization: "Bearer " + getToken() });
+const javaApiUrl = proxy.javaApi || import.meta.env.VITE_APP_BASE_API;
 const page = reactive({
   current: 1,
   size: 10,
@@ -239,15 +356,28 @@
     productName: "",
   },
   rules: {
-    productName: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+    productName: [
+      { required: true, message: "璇疯緭鍏�", trigger: "blur" },
+      { max: 20, message: "浜у搧鍚嶇О涓嶈兘瓒呰繃20涓瓧绗�", trigger: "blur" },
+    ],
   },
   modelForm: {
     model: "",
     unit: "",
+    url: "",
+    height: "",
+    boxNum: null,
+    taxInclusiveUnitPrice: null,
+    dollarPrice: null,
   },
   modelRules: {
-    model: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
-    unit: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+    model: [{ required: true, message: "璇疯緭鍏ヨ鏍煎瀷鍙�", trigger: "blur" }],
+    unit: [{ required: true, message: "璇疯緭鍏ュ崟浣�", trigger: "blur" }],
+    url: [{ required: true, message: "璇蜂笂浼犱骇鍝佸浘鐗�", trigger: "change" }],
+    height: [{ required: true, message: "璇疯緭鍏ラ珮搴�", trigger: "blur" }],
+    boxNum: [{ required: true, message: "璇疯緭鍏ユ瘡浠舵暟閲�/鏀�", trigger: "change" }],
+    taxInclusiveUnitPrice: [{ required: true, message: "璇疯緭鍏ュ崟浠�(鍏�)/浠�", trigger: "change" }],
+    dollarPrice: [{ required: true, message: "璇疯緭鍏ュ崟浠�(缇庡厓)/浠�", trigger: "change" }],
   },
 });
 const { form, rules, modelForm, modelRules } = toRefs(data);
@@ -283,11 +413,30 @@
 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 };
+  // 閲嶇疆琛ㄥ崟
+  modelForm.value = {
+    model: "",
+    unit: "",
+    url: "",
+    height: "",
+    boxNum: null,
+    taxInclusiveUnitPrice: null,
+    dollarPrice: null,
+    id: "",
+  };
+  if (type === "edit" && data) {
+    // 濡傛灉 url 鏄� Windows 璺緞锛岄渶瑕佽浆鎹�
+    let url = data.url || "";
+    if (url && url.indexOf("\\") > -1) {
+      url = processFileUrl(url);
+    }
+    modelForm.value = {
+      ...data,
+      url: url,
+      boxNum: data.boxNum || null,
+      taxInclusiveUnitPrice: data.taxInclusiveUnitPrice || null,
+      dollarPrice: data.dollarPrice || null,
+    };
   }
 };
 // 鎻愪氦浜у搧鍚嶇О淇敼
@@ -389,10 +538,107 @@
     size: page.size,
   }).then((res) => {
     console.log("res", res);
-    tableData.value = res.records;
+    // 澶勭悊杩斿洖鐨勬暟鎹紝杞崲 Windows 璺緞涓哄彲璁块棶鐨� URL
+    tableData.value = (res.records || []).map((item) => {
+      if (item.url && item.url.indexOf("\\") > -1) {
+        item.url = processFileUrl(item.url);
+      }
+      return item;
+    });
     page.total = res.total;
     tableLoading.value = false;
   });
+};
+
+// 涓婁紶鍓嶆牎楠�
+const handleBeforeUpload = (file) => {
+  const isImage = file.type.startsWith("image/");
+  const isLt5M = file.size / 1024 / 1024 < 5;
+
+  if (!isImage) {
+    proxy.$modal.msgError("涓婁紶鏂囦欢鍙兘鏄浘鐗囨牸寮�!");
+    return false;
+  }
+  if (!isLt5M) {
+    proxy.$modal.msgError("涓婁紶鍥剧墖澶у皬涓嶈兘瓒呰繃 5MB!");
+    return false;
+  }
+  return true;
+};
+
+// 澶勭悊鏂囦欢璺緞锛氬皢 Windows 璺緞杞崲涓哄彲璁块棶鐨� URL
+const processFileUrl = (filePath) => {
+  if (!filePath) return "";
+  
+  // 濡傛灉璺緞鏄� Windows 璺緞鏍煎紡锛堝寘鍚弽鏂滄潬锛夛紝闇�瑕佽浆鎹�
+  if (filePath && filePath.indexOf("\\") > -1) {
+    // 鏌ユ壘 temp 鎴� uploads 鍏抽敭瀛楃殑浣嶇疆
+    const tempIndex = filePath.toLowerCase().indexOf("temp");
+    const uploadsIndex = filePath.toLowerCase().indexOf("uploads");
+    
+    if (tempIndex > -1) {
+      // 浠� temp 寮�濮嬫彁鍙栫浉瀵硅矾寰勶紝骞跺皢鍙嶆枩鏉犳浛鎹负姝f枩鏉�
+      const relativePath = filePath.substring(tempIndex).replace(/\\/g, "/");
+      filePath = "/" + relativePath;
+    } else if (uploadsIndex > -1) {
+      // 浠� uploads 寮�濮嬫彁鍙栫浉瀵硅矾寰�
+      const relativePath = filePath.substring(uploadsIndex).replace(/\\/g, "/");
+      filePath = "/" + relativePath;
+    } else {
+      // 濡傛灉娌℃湁鎵惧埌鍏抽敭瀛楋紝鎻愬彇鏂囦欢鍚�
+      const parts = filePath.split("\\");
+      const fileName = parts[parts.length - 1];
+      filePath = "/temp/uploads/" + fileName;
+    }
+  }
+  
+  // 纭繚璺緞浠� / 寮�澶�
+  if (filePath && !filePath.startsWith("/")) {
+    filePath = "/" + filePath;
+  }
+  
+  return filePath;
+};
+
+// 涓婁紶鎴愬姛
+const handleUploadSuccess = (res, file) => {
+  if (res.code === 200) {
+    // 浠� res.data 涓幏鍙� tempPath锛屽苟杞崲涓哄彲璁块棶鐨� URL
+    const tempPath = res.data?.tempPath || res.tempPath;
+    if (tempPath) {
+      const relativePath = processFileUrl(tempPath);
+      modelForm.value.url = relativePath;
+      proxy.$modal.msgSuccess("涓婁紶鎴愬姛");
+      // 瑙﹀彂琛ㄥ崟楠岃瘉
+      nextTick(() => {
+        proxy.$refs.modelFormRef?.validateField("url");
+      });
+    } else {
+      proxy.$modal.msgError("涓婁紶鎴愬姛浣嗘湭杩斿洖鏂囦欢璺緞");
+    }
+  } else {
+    proxy.$modal.msgError(res.msg || "涓婁紶澶辫触");
+  }
+};
+
+// 涓婁紶澶辫触
+const handleUploadError = () => {
+  proxy.$modal.msgError("涓婁紶澶辫触锛岃閲嶈瘯");
+};
+
+// 璁$畻鎬讳环
+const calculateTotalPrice = () => {
+  // 濡傛灉闇�瑕佽绠楁�讳环锛屽彲浠ュ湪杩欓噷娣诲姞閫昏緫
+  // if (modelForm.value.boxNum && modelForm.value.taxInclusiveUnitPrice) {
+  //   modelForm.value.totalPrice = modelForm.value.boxNum * modelForm.value.taxInclusiveUnitPrice;
+  // }
+};
+
+// 棰勮鍥剧墖
+const previewImage = (url) => {
+  if (url) {
+    window.open(javaApiUrl + url, "_blank");
+  }
 };
 // 鍒犻櫎瑙勬牸鍨嬪彿
 const handleDelete = () => {
@@ -467,18 +713,21 @@
   display: flex;
 }
 .left {
-  width: 380px;
+  width: 450px;
+  min-width: 450px;
   padding: 16px;
   background: #ffffff;
 }
 .right {
-  width: calc(100% - 380px);
+  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;
@@ -486,13 +735,92 @@
   padding-right: 8px;
 }
 .tree-node-content {
+  flex: 1;
+  min-width: 0;
   display: flex;
-  align-items: center; /* 鍨傜洿灞呬腑 */
+  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;
+}
+
+.upload-img {
+  width: 50px;
+  height: 50px;
+  object-fit: cover;
+  cursor: pointer;
+  border-radius: 4px;
+}
+
+.avatar-uploader-icon {
+  font-size: 28px;
+  color: #8c939d;
+  width: 50px;
+  height: 50px;
+  line-height: 50px;
+  text-align: center;
+  border: 1px dashed #d9d9d9;
+  border-radius: 4px;
+  cursor: pointer;
+}
+
+.avatar-uploader-icon:hover {
+  border-color: #409eff;
+}
+
+.upload-img-dialog {
+  width: 100px;
+  height: 100px;
+  object-fit: cover;
+  cursor: pointer;
+  border-radius: 4px;
+  border: 1px solid #dcdfe6;
+}
+
+.avatar-uploader-icon-dialog {
+  font-size: 28px;
+  color: #8c939d;
+  width: 100px;
+  height: 100px;
+  line-height: 100px;
+  text-align: center;
+  border: 1px dashed #d9d9d9;
+  border-radius: 4px;
+  cursor: pointer;
+  display: block;
+}
+
+.avatar-uploader-icon-dialog:hover {
+  border-color: #409eff;
+}
 </style>

--
Gitblit v1.9.3