소스 검색

feat: update: 完善sku 编辑模块 新增上传文件支持

叶静 3 주 전
부모
커밋
9bb6e39f06

+ 4 - 0
.env.development

@@ -6,6 +6,10 @@ SHEEP_BASE_URL = /
 # 开发环境接口域名
  SHEEP_DEV_BASE_URL = /mall
 
+# 开发环境上传文件接口域名
+ SHEEP_UPLOAD_BASE_URL = /operating
+
+
 # 开发模式端口号
 SHEEP_DEV_PORT = 3001
 # 预览模式端口号

+ 3 - 0
.env.production

@@ -6,6 +6,9 @@ SHEEP_BASE_URL = https://shop.fusenlink.com
 # 开发环境接口域名
 SHEEP_DEV_BASE_URL = https://shop.fusenlink.com
 
+# 开发环境上传文件接口域名
+ SHEEP_UPLOAD_BASE_URL = /operating
+
 # 开发模式端口号
 SHEEP_DEV_PORT = 3001
 # 预览模式端口号

+ 2 - 1
src/app/admin/api/index.js

@@ -207,7 +207,8 @@ export default {
     // 文件上传
     upload: (params, data) =>
       request({
-        url: 'admin/file/upload',
+        baseURL: import.meta.env.SHEEP_UPLOAD_BASE_URL,
+        url: '/file/upload',
         method: 'POST',
         params,
         data,

+ 3 - 0
src/app/shop/admin/goods/goods.service.js

@@ -46,6 +46,9 @@ const api = {
         method: 'GET',
       }),
   },
+  rule: {
+    ...CRUD('product/rule'),
+  },
   category: {
     ...CRUD('category'),
     select: (params) => SELECT('shop/admin/category_tag', params),

+ 341 - 87
src/app/shop/admin/goods/goods/edit.vue

@@ -119,20 +119,23 @@
                   >
                 </el-form-item> -->
 
-                <el-form-item label="库存预警值" prop="stockThreshold">
+                <!-- <el-form-item label="库存预警值" prop="stockThreshold">
                   <el-input
                     v-model="formData.stockThreshold"
                     placeholder="请输入库存预警值"
                     type="number"
                     min="0"
                   />
-                </el-form-item>
+                </el-form-item> -->
 
                 <el-form-item label="商品状态" prop="isShow" required>
-                  <el-radio-group v-model="formData.isShow">
+                  <el-radio-group v-model="formData.isShow" :disabled="!isEdit">
                     <el-radio :label="1">上架</el-radio>
                     <el-radio :label="0">下架</el-radio>
                   </el-radio-group>
+                  <div v-if="!isEdit" class="form-tip ml-20px mb-6px"
+                    >新增商品默认为下架状态,保存后可修改</div
+                  >
                 </el-form-item>
 
                 <el-form-item label="商品供应商" prop="itemSupplier" required>
@@ -142,19 +145,19 @@
 
               <!-- 右侧图片上传 -->
               <el-col :span="10">
-                <el-form-item label="商品主图" prop="image" required>
+                <el-form-item label="白底主图" prop="image" required>
                   <sa-upload-image
                     v-model="formData.image"
-                    :max-count="5"
+                    :max-count="1"
                     :accept="['jpg', 'jpeg', 'png']"
                     :max-size="5"
                     :direct-upload="true"
                     :size="100"
-                    placeholder="上传商品主图"
+                    placeholder="上传白底主图"
                   />
                 </el-form-item>
 
-                <el-form-item label="轮播图" prop="sliderImage">
+                <el-form-item label="轮播图" prop="sliderImage" required>
                   <sa-upload-image
                     v-model="formData.sliderImage"
                     :max-count="5"
@@ -162,7 +165,7 @@
                     :max-size="5"
                     :direct-upload="true"
                     :size="100"
-                    placeholder="上传白底图"
+                    placeholder="上传轮播图"
                   />
                 </el-form-item>
 
@@ -183,7 +186,7 @@
         </div>
 
         <!-- 第二步:商品属性 -->
-        <div v-show="currentStep === 1" class="">
+        <div v-show="currentStep === 1" class="" :key="forceUpdateKey">
           <el-form ref="attrFormRef" :model="formData" :rules="attrRules" label-width="120px">
             <!-- 多规格设置 -->
             <el-card class="spec-card">
@@ -194,8 +197,12 @@
               </template>
 
               <!-- 操作 -->
-              <div class="sku-wrap">
-                <div class="sku" v-for="(s, k) in formData.skus" :key="k">
+              <div class="sku-wrap" :key="`sku-wrap-${forceUpdateKey}`">
+                <div
+                  class="sku"
+                  v-for="(s, k) in formData.skus"
+                  :key="`sku-${k}-${forceUpdateKey}`"
+                >
                   <div class="sku-key sa-flex sa-row-between">
                     <div class="sa-flex">
                       <div class="sa-m-r-16">规格名称</div>
@@ -260,29 +267,38 @@
                     未选择规格默认为全选批量设置
                   </div>
                 </el-form-item>
-                <div class="sa-flex">
+                <div class="sa-flex sa-flex-wrap">
                   <el-input
                     v-model="allEditObj.price"
                     placeholder="请输入售价(৳)"
-                    class="sa-w-200 sa-m-r-10"
+                    class="sa-w-200 sa-m-r-10 sa-m-b-10"
                   >
                     <template #prepend>售价(৳)</template>
                   </el-input>
+                  <el-input
+                    v-model="allEditObj.otPrice"
+                    placeholder="请输入市场价(৳)"
+                    class="sa-w-200 sa-m-r-10 sa-m-b-10"
+                  >
+                    <template #prepend>市场价(৳)</template>
+                  </el-input>
                   <el-input
                     v-model="allEditObj.stock"
                     placeholder="请输入库存(件)"
-                    class="sa-w-200 sa-m-r-10"
+                    class="sa-w-200 sa-m-r-10 sa-m-b-10"
                   >
                     <template #prepend>库存(件)</template>
                   </el-input>
                   <el-input
-                    v-model="allEditObj.stock_warning"
+                    v-model="allEditObj.stockThreshold"
                     placeholder="请输入库存预警值(件)"
-                    class="sa-w-200 sa-m-r-10"
+                    class="sa-w-200 sa-m-r-10 sa-m-b-10"
                   >
                     <template #prepend>库存预警值(件)</template>
                   </el-input>
-                  <el-button type="primary" @click="batchEdit">批量设置</el-button>
+                  <el-button type="primary" @click="batchEdit" class="sa-m-b-10"
+                    >批量设置</el-button
+                  >
                 </div>
               </div>
 
@@ -296,6 +312,7 @@
                       </template>
                       <th>图片</th>
                       <th><span class="required">*</span>销售价格(৳)</th>
+                      <th><span class="required">*</span>市场价(৳)</th>
                       <th><span class="required">*</span>商品库存</th>
                       <th>库存预警值</th>
                       <th>SKU编码</th>
@@ -334,6 +351,18 @@
                           :class="{ 'is-error': !item.price || item.price <= 0 }"
                         ></el-input>
                       </td>
+                      <td>
+                        <el-input
+                          v-model="item.otPrice"
+                          placeholder="请输入市场价"
+                          size="small"
+                          type="number"
+                          :step="0.01"
+                          :min="0"
+                          :precision="2"
+                          :class="{ 'is-error': !item.otPrice || item.otPrice <= 0 }"
+                        ></el-input>
+                      </td>
                       <td class="stock">
                         <el-input
                           v-model="item.stock"
@@ -345,9 +374,9 @@
                           :class="{ 'is-error': !item.stock || item.stock < 0 }"
                         ></el-input>
                       </td>
-                      <td class="stock_warning">
+                      <td class="stockThreshold">
                         <el-input
-                          v-model="item.stock_warning"
+                          v-model="item.stockThreshold"
                           placeholder="请输入预警值"
                           size="small"
                           type="number"
@@ -357,7 +386,7 @@
                       </td>
                       <td class="sn">
                         <el-input
-                          v-model="item.sn"
+                          v-model="item.skuCode"
                           placeholder="请输入SKU编码"
                           size="small"
                         ></el-input>
@@ -451,6 +480,9 @@
   // 是否为编辑模式
   const isEdit = computed(() => props.modal?.params?.type === 'edit');
 
+  // 强制重新渲染的key
+  const forceUpdateKey = ref(0);
+
   // 表单引用
   const basicFormRef = ref();
   const attrFormRef = ref();
@@ -470,7 +502,7 @@
     otPrice: '',
     stock: '',
     stockThreshold: '',
-    isShow: 1, // 状态 0-未上架 1-上
+    isShow: 0, // 状态 0-未上架 1-上架,新增默认下
     itemSupplier: '',
     sort: 0,
     cost: '', // 成本价
@@ -480,6 +512,7 @@
     image: ['https://mall-oss.trust-will.com/O1CN01oBqd3M1ilz2OudYgb_!!2216255674454-0-cib.jpg'], // 商品主图
     sliderImage: [
       'https://mall-oss.trust-will.com/O1CN01oBqd3M1ilz2OudYgb_!!2216255674454-0-cib.jpg',
+      'https://mall-oss.trust-will.com/07dd855cb85adcf7.jpg',
     ], // 轮播图(白底图)
     flatPattern: [
       'https://mall-oss.trust-will.com/O1CN01oBqd3M1ilz2OudYgb_!!2216255674454-0-cib.jpg',
@@ -515,6 +548,7 @@
     isShow: [{ required: true, message: '请选择商品状态', trigger: 'change' }],
     itemSupplier: [{ required: true, message: '请输入商品供应商', trigger: 'blur' }],
     image: [{ required: true, message: '请上传商品主图', trigger: 'change' }],
+    sliderImage: [{ required: true, message: '请上传轮播图', trigger: 'change' }],
     flatPattern: [{ required: true, message: '请上传商品详情图', trigger: 'change' }],
   };
 
@@ -538,8 +572,9 @@
   // 批量操作相关变量
   const allEditObj = ref({
     price: 0,
+    otPrice: 0,
     stock: 0,
-    stock_warning: 0,
+    stockThreshold: 0,
   });
 
   // 添加主规格
@@ -562,10 +597,10 @@
     // 删除主规格
     formData.skus.splice(k, 1);
 
-    // 如果当前删除的主规格存在子规格,则清空 skuPrice
+    // 如果当前删除的主规格存在子规格,则清空 skuPrice,不存在子规格则不清空
     if (data.children.length > 0) {
-      formData.sku_prices = [];
-      isResetSku.value = 1;
+      formData.sku_prices = []; // 规格大变化,清空skuPrice
+      isResetSku.value = 1; // 重置规格
     }
     buildSkuPriceTable();
   };
@@ -583,18 +618,32 @@
       return false;
     }
 
+    // 修复:确保countId是全局最大的temp_id + 1
+    let maxTempId = 0;
+    formData.skus.forEach((sku) => {
+      sku.children.forEach((child) => {
+        if (child.temp_id > maxTempId) {
+          maxTempId = child.temp_id;
+        }
+      });
+    });
+    const newTempId = maxTempId + 1;
+
     formData.skus[k].children.push({
       id: 0,
-      temp_id: countId.value++,
+      temp_id: newTempId,
       name: childrenModal[k] || '',
       pid: formData.skus[k].id,
     });
     childrenModal[k] = '';
 
+    // 更新countId为新的最大值
+    countId.value = newTempId + 1;
+
     // 如果是添加的第一个子规格,清空 skuPrice
     if (formData.skus[k].children.length == 1) {
-      formData.sku_prices = [];
-      isResetSku.value = 1;
+      formData.sku_prices = []; // 规格大变化,清空skuPrice
+      isResetSku.value = 1; // 重置规格
     }
     buildSkuPriceTable();
   };
@@ -623,8 +672,8 @@
 
     // 当前规格项,所有子规格都被删除,清空 sku_prices
     if (formData.skus[k].children.length <= 0) {
-      formData.sku_prices = [];
-      isResetSku.value = 1;
+      formData.sku_prices = []; // 规格大变化,清空skuPrice
+      isResetSku.value = 1; // 重置规格
     }
     buildSkuPriceTable();
   };
@@ -644,6 +693,7 @@
         arr.push(childrenIdArray);
       }
     });
+
     recursionSku(arr, 0, []);
   };
 
@@ -652,16 +702,23 @@
     if (k == arr.length && k != 0) {
       let tempDetail = [];
       let tempDetailIds = [];
-      temp.forEach((item, index) => {
+
+      // 修复:每个temp_id只应该匹配一次,找到就跳出
+      temp.forEach((item) => {
+        let found = false;
         for (let sku of formData.skus) {
+          if (found) break;
           for (let child of sku.children) {
             if (item == child.temp_id) {
               tempDetail.push(child.name);
               tempDetailIds.push(child.temp_id);
+              found = true;
+              break;
             }
           }
         }
       });
+
       let flag = false; // 默认添加新的
       for (let i = 0; i < formData.sku_prices.length; i++) {
         if (formData.sku_prices[i].goods_sku_temp_ids.join(',') == tempDetailIds.join(',')) {
@@ -680,9 +737,10 @@
           image: '',
           imageList: [],
           stock: 0,
-          stock_warning: 0,
+          stockThreshold: 0,
           price: 0,
-          sn: '',
+          otPrice: 0,
+          skuCode: '',
           goods_sku_text: tempDetail,
           goods_sku_temp_ids: tempDetailIds,
         });
@@ -707,18 +765,20 @@
       if (
         batchIds.length ? batchIds.every((citem) => item.goods_sku_temp_ids.includes(citem)) : true
       ) {
-        const { price, stock, stock_warning } = allEditObj.value;
+        const { price, otPrice, stock, stockThreshold } = allEditObj.value;
         if (price) item.price = price;
+        if (otPrice) item.otPrice = otPrice;
         if (stock) item.stock = stock;
-        if (stock_warning) item.stock_warning = stock_warning;
+        if (stockThreshold) item.stockThreshold = stockThreshold;
       }
     });
 
     // 清空输入框
     allEditObj.value = {
       price: 0,
+      otPrice: 0,
       stock: 0,
-      stock_warning: 0,
+      stockThreshold: 0,
     };
 
     // 清空选择的规格
@@ -748,6 +808,10 @@
         ElMessage.error(`第${i + 1}个规格的销售价格不能为空且必须大于0`);
         return false;
       }
+      if (!item.otPrice || item.otPrice <= 0) {
+        ElMessage.error(`第${i + 1}个规格的市场价不能为空且必须大于0`);
+        return false;
+      }
       if (item.stock === null || item.stock === undefined || item.stock < 0) {
         ElMessage.error(`第${i + 1}个规格的商品库存不能为空且不能小于0`);
         return false;
@@ -761,17 +825,171 @@
   const submitError = ref('');
   const submitSuccess = ref(false);
 
-  // 图片数组转换为逗号分隔字符串的函数
-  const convertImagesToString = (imageArray) => {
-    return Array.isArray(imageArray) ? imageArray.join(',') : '';
-  };
+  // 数据转换工具方法
+  const dataConverter = {
+    // 图片数组转换为逗号分隔字符串
+    imagesToString: (imageArray) => {
+      return Array.isArray(imageArray) ? imageArray.join(',') : '';
+    },
+
+    // 图片字符串转换为数组
+    stringToImages: (imageString) => {
+      if (!imageString) return [];
+      return imageString.split(',').filter((img) => img.trim());
+    },
 
-  // 图片字符串转换为数组的函数
-  const convertStringToImages = (imageString) => {
-    if (!imageString) return [];
-    return imageString.split(',').filter((img) => img.trim());
+    // 将后端返回的 attr 和 attrValue 数据转换为前端需要的格式
+    convertBackendToFrontend: (attr, attrValue) => {
+      const skus = [];
+      const sku_prices = [];
+      let tempId = 1;
+
+      // 处理 attr 数据,构建 skus 结构
+      if (attr && attr.length > 0) {
+        attr.forEach((attrItem) => {
+          const children = attrItem.attrValues.split(',').map((value) => ({
+            id: 0, // 子规格值没有单独的ID,保持为0
+            temp_id: tempId++,
+            name: value.trim(),
+            pid: attrItem.id, // 使用父规格的ID
+          }));
+
+          skus.push({
+            id: attrItem.id || 0, // 保留原有的规格ID
+            temp_id: tempId++,
+            name: attrItem.attrName,
+            batchId: '',
+            pid: 0,
+            children: children,
+          });
+        });
+      }
+
+      // 处理 attrValue 数据,构建 sku_prices 结构
+      if (attrValue && attrValue.length > 0) {
+        // 按照attr的顺序对attrValue进行排序
+        const sortedAttrValue = [...attrValue].sort((a, b) => {
+          try {
+            const aAttrObj = JSON.parse(a.attrValue || '{}');
+            const bAttrObj = JSON.parse(b.attrValue || '{}');
+
+            // 按照attr的顺序比较每个属性值
+            for (let i = 0; i < attr.length; i++) {
+              const attrName = attr[i].attrName;
+              const aValue = aAttrObj[attrName];
+              const bValue = bAttrObj[attrName];
+
+              if (aValue && bValue) {
+                // 找到该属性值在attrValues中的索引
+                const attrValues = attr[i].attrValues.split(',').map((v) => v.trim());
+                const aIndex = attrValues.indexOf(aValue);
+                const bIndex = attrValues.indexOf(bValue);
+
+                if (aIndex !== bIndex) {
+                  return aIndex - bIndex;
+                }
+              }
+            }
+            return 0;
+          } catch (e) {
+            return 0;
+          }
+        });
+
+        sortedAttrValue.forEach((item) => {
+          let attrValueObj = {};
+          try {
+            attrValueObj = JSON.parse(item.attrValue || '{}');
+          } catch (e) {
+            console.warn('解析 attrValue 失败:', item.attrValue, e);
+          }
+
+          // 构建 goods_sku_text 和 goods_sku_temp_ids
+          const goods_sku_text = [];
+          const goods_sku_temp_ids = [];
+
+          // 根据 skus 的顺序构建 goods_sku_text
+          skus.forEach((sku) => {
+            const value = attrValueObj[sku.name];
+            if (value) {
+              goods_sku_text.push(value);
+              // 找到对应的 temp_id
+              const child = sku.children.find((c) => c.name === value);
+              if (child) {
+                goods_sku_temp_ids.push(child.temp_id);
+              }
+            }
+          });
+
+          sku_prices.push({
+            id: parseInt(item.id) || 0, // 保留原有的SKU价格ID
+            temp_id: sku_prices.length + 1,
+            goods_sku_ids: '',
+            goods_id: parseInt(item.productId) || 0,
+            weigh: 0,
+            image: item.image || '',
+            imageList: item.image ? [item.image] : [],
+            stock: parseInt(item.stock) || 0,
+            stockThreshold: parseInt(item.stockThreshold) || 0,
+            price: parseFloat(item.price) || 0,
+            otPrice: parseFloat(item.otPrice) || 0,
+            skuCode: item.skuCode || item.barCode || item.suk || '',
+            goods_sku_text: goods_sku_text,
+            goods_sku_temp_ids: goods_sku_temp_ids,
+          });
+        });
+      }
+
+      return { skus, sku_prices };
+    },
+
+    // 将前端数据转换为后端需要的格式
+    convertFrontendToBackend: (skus, sku_prices) => {
+      // 生成 attr 数据
+      const attr = skus
+        .filter((sku) => sku.name && sku.children.length > 0)
+        .map((sku) => ({
+          id: sku.id || 0, // 保留规格ID,新增时为0
+          attrName: sku.name,
+          attrValues: sku.children.map((child) => child.name).join(','),
+        }));
+
+      // 生成 attrValue 数据
+      const attrValue = sku_prices.map((item) => {
+        const attrObj = {};
+        const attrValueObj = {};
+
+        // 根据 goods_sku_text 和对应的规格名称构建属性对象
+        item.goods_sku_text.forEach((value, index) => {
+          const specName = skus[index]?.name;
+          if (specName) {
+            attrObj[specName] = value;
+            attrValueObj[specName] = value;
+          }
+        });
+
+        return {
+          id: item.id || 0, // 保留SKU价格ID,新增时为0
+          image: item.imageList && item.imageList.length > 0 ? item.imageList[0] : '',
+          price: item.price || '0',
+          otPrice: item.otPrice || '0',
+          stock: item.stock || 0,
+          skuCode: item.skuCode || '',
+          stockThreshold: item.stockThreshold || 0,
+          attrValue: JSON.stringify(attrValueObj),
+          ...attrObj,
+          productId: item.goods_id || 0,
+        };
+      });
+
+      return { attr, attrValue };
+    },
   };
 
+  // 兼容旧方法名
+  const convertImagesToString = dataConverter.imagesToString;
+  const convertStringToImages = dataConverter.stringToImages;
+
   // 下一步
   const nextStep = async () => {
     if (currentStep.value === 0) {
@@ -872,6 +1090,10 @@
             ElMessage.error(`第${i + 1}个规格的销售价格不能为空且必须大于0`);
             return;
           }
+          if (!item.otPrice || item.otPrice <= 0) {
+            ElMessage.error(`第${i + 1}个规格的市场价不能为空且必须大于0`);
+            return;
+          }
           if (item.stock === null || item.stock === undefined || item.stock < 0) {
             ElMessage.error(`第${i + 1}个规格的商品库存不能为空且不能小于0`);
             return;
@@ -898,44 +1120,18 @@
     }
   };
 
-  // 生成后端需要的 attrValue 数据格式
+  // 生成后端需要的数据格式(使用统一转换工具)
   const generateAttrValueData = () => {
-    return formData.sku_prices.map((item) => {
-      // 构建规格属性对象,如 {"颜色": "红色", "尺寸": "S"}
-      const attrObj = {};
-      const attrValueObj = {};
-
-      // 根据 goods_sku_text 和对应的规格名称构建属性对象
-      item.goods_sku_text.forEach((value, index) => {
-        const specName = formData.skus[index]?.name;
-        if (specName) {
-          attrObj[specName] = value;
-          attrValueObj[specName] = value;
-        }
-      });
-
-      return {
-        image: item.imageList && item.imageList.length > 0 ? item.imageList[0] : '',
-        price: item.price || '0',
-        stock: item.stock || 0,
-        barCode: item.sn || '',
-        stock_warning: item.stock_warning || 0,
-        attrValue: JSON.stringify(attrValueObj),
-        ...attrObj, // 展开规格属性,如 "颜色": "红色", "尺寸": "S"
-        id: 0,
-        productId: 0,
-      };
-    });
+    const { attrValue } = dataConverter.convertFrontendToBackend(
+      formData.skus,
+      formData.sku_prices,
+    );
+    return attrValue;
   };
 
-  // 生成后端需要的 attr 数据格式
   const generateAttrData = () => {
-    return formData.skus
-      .filter((sku) => sku.name && sku.children.length > 0)
-      .map((sku) => ({
-        attrName: sku.name,
-        attrValues: sku.children.map((child) => child.name).join(','),
-      }));
+    const { attr } = dataConverter.convertFrontendToBackend(formData.skus, formData.sku_prices);
+    return attr;
   };
 
   // 统一提交接口 - 保存商品信息和属性
@@ -996,20 +1192,78 @@
   // 加载商品数据
   const loadGoodsData = async (id) => {
     try {
-      // 这里调用获取商品详情接口
-      console.log('加载商品数据:', id);
       const response = await api.goods.detail(id);
+
       if (response.code == '200') {
-        // 处理图片字段:将逗号分隔的字符串转换为数组
         const data = { ...response.data };
 
         // 转换图片字段:将逗号分隔的字符串转换为数组
-        data.image = convertStringToImages(data.image);
-        data.sliderImage = convertStringToImages(data.sliderImage);
-        data.flatPattern = convertStringToImages(data.flatPattern);
+        data.image = dataConverter.stringToImages(data.image);
+        data.sliderImage = dataConverter.stringToImages(data.sliderImage);
+        data.flatPattern = dataConverter.stringToImages(data.flatPattern);
+
+        // 处理商品属性数据:将后端的 attr 和 attrValue 转换为前端格式
+        if (data.attr && data.attrValue && data.attr.length > 0 && data.attrValue.length > 0) {
+          const { skus, sku_prices } = dataConverter.convertBackendToFrontend(
+            data.attr,
+            data.attrValue,
+          );
+
+          data.skus = skus.length > 0 ? skus : formData.skus;
+          data.sku_prices = sku_prices;
+          data.specType = skus.length > 0 ? 1 : 0;
+        } else {
+          // 如果没有属性数据,保持默认的单规格结构
+          data.skus = formData.skus;
+          data.sku_prices = [];
+          data.specType = 1; // 默认多规格模式
+        }
 
-        // 将处理后的数据填充到表单中
-        Object.assign(formData, data);
+        // 将处理后的数据填充到表单中 - 使用直接赋值确保响应式更新
+        // 基本信息字段
+        formData.id = data.id;
+        formData.cateId = data.cateId;
+        formData.storeName = data.storeName;
+        formData.keyword = data.keyword;
+        formData.itemBrand = data.itemBrand;
+        formData.storeInfo = data.storeInfo;
+        formData.itemNumber = data.itemNumber;
+        formData.price = data.price;
+        formData.otPrice = data.otPrice;
+        formData.stock = data.stock;
+        formData.stockThreshold = data.stockThreshold;
+        formData.isShow = data.isShow;
+        formData.itemSupplier = data.itemSupplier;
+        formData.sort = data.sort;
+        formData.cost = data.cost;
+        formData.vipPrice = data.vipPrice;
+
+        // 图片字段
+        formData.image = data.image;
+        formData.sliderImage = data.sliderImage;
+        formData.flatPattern = data.flatPattern;
+
+        // 规格字段 - 使用全新对象替换
+        formData.specType = data.specType;
+
+        // 重要:先清空数组,然后添加新元素
+        formData.skus.length = 0;
+        data.skus.forEach((item) => formData.skus.push(item));
+
+        formData.sku_prices.length = 0;
+        data.sku_prices.forEach((item) => formData.sku_prices.push(item));
+
+        // 强制触发响应式更新
+        await new Promise((resolve) => {
+          setTimeout(() => {
+            // 强制重新构建SKU表格
+            buildSkuPriceTable();
+
+            // 强制重新渲染组件
+            forceUpdateKey.value++;
+            resolve();
+          }, 100);
+        });
       }
     } catch (error) {
       console.error('加载商品数据失败:', error);
@@ -1217,7 +1471,7 @@
         &.stock {
           min-width: 138px;
         }
-        &.stock_warning {
+        &.stockThreshold {
           min-width: 168px;
           .sku-stock-switch {
             margin-right: 10px;

+ 0 - 7
src/app/shop/admin/goods/goods/index.vue

@@ -39,7 +39,6 @@
                 @click="getData()"
               ></el-button>
               <el-button icon="Plus" type="primary" @click="addRow">新建</el-button>
-              <el-button icon="Plus" type="success" @click="addRowWithTab">新建(Tab版)</el-button>
             </div>
           </div>
         </el-header>
@@ -129,12 +128,6 @@
                     <el-button class="is-link" type="primary" @click="editRow(scope.row)"
                       >编辑</el-button
                     >
-                    <el-button
-                      class="is-link sa-m-l-12"
-                      type="success"
-                      @click="editRowWithTab(scope.row)"
-                      >Tab编辑</el-button
-                    >
                     <el-popconfirm
                       width="fit-content"
                       confirm-button-text="确认"

+ 0 - 1983
src/app/shop/admin/goods/goods/o-edit.vue

@@ -1,1983 +0,0 @@
-<template>
-  <el-container class="goods-edit">
-    <el-header>
-      <el-tabs class="sa-m-t-10" v-model="stepActive" @tab-click="isValidate">
-        <el-tab-pane :name="0">
-          <template #label>
-            <div class="sa-flex" :class="validateData['0'] ? 'is-error' : ''">
-              基本信息
-              <el-icon v-if="validateData['0']" class="sa-m-l-5">
-                <warning-filled />
-              </el-icon>
-            </div>
-          </template>
-        </el-tab-pane>
-        <el-tab-pane :name="1">
-          <template #label>
-            <div class="sa-flex" :class="validateData['1'] ? 'is-error' : ''">
-              价格/库存
-              <el-icon v-if="validateData['1']" class="sa-m-l-5">
-                <warning-filled />
-              </el-icon>
-            </div>
-          </template>
-        </el-tab-pane>
-        <el-tab-pane :name="2">
-          <template #label>
-            <div class="sa-flex" :class="validateData['2'] ? 'is-error' : ''">
-              发货设置
-              <el-icon v-if="validateData['2']" class="sa-m-l-5">
-                <warning-filled />
-              </el-icon>
-            </div>
-          </template>
-        </el-tab-pane>
-        <el-tab-pane :name="3">
-          <template #label>
-            <div class="sa-flex" :class="validateData['3'] ? 'is-error' : ''">
-              商品参数
-              <el-icon v-if="validateData['3']" class="sa-m-l-5">
-                <warning-filled />
-              </el-icon>
-            </div>
-          </template>
-        </el-tab-pane>
-        <el-tab-pane :name="4">
-          <template #label>
-            <div class="sa-flex" :class="validateData['4'] ? 'is-error' : ''">
-              商品详情
-              <el-icon v-if="validateData['4']" class="sa-m-l-5">
-                <warning-filled />
-              </el-icon>
-            </div>
-          </template>
-        </el-tab-pane>
-      </el-tabs>
-    </el-header>
-    <el-main class="sa-p-t-10">
-      <el-form :model="form.model" :rules="form.rules" ref="formRef0" label-width="100px">
-        <div v-show="stepActive == 0">
-          <div class="goodstype sa-flex sa-m-b-20" @click="goodsModality">
-            <img
-              v-if="!(modal.params.type == 'edit' && form.model.type == 'virtual')"
-              class="goods-type"
-              :class="form.model.type == 'normal' ? 'is-active' : ''"
-              src="/static/images/shop/goods/entity.png"
-              @click="onChangeGoodsType('normal')"
-            />
-            <img
-              v-if="!(modal.params.type == 'edit' && form.model.type == 'normal')"
-              class="goods-type"
-              :class="form.model.type == 'virtual' ? 'is-active' : ''"
-              src="/static/images/shop/goods/virtual.png"
-              @click="onChangeGoodsType('virtual')"
-            />
-          </div>
-          <el-form-item label="商品标题" prop="title">
-            <el-input v-model="form.model.title" placeholder="请输入标题"></el-input>
-          </el-form-item>
-          <el-form-item label="品牌名称" prop="brand_name">
-            <el-input v-model="form.model.brand_name" placeholder="请输入品牌名称"></el-input>
-          </el-form-item>
-          <el-form-item label="副标题">
-            <el-input v-model="form.model.subtitle" placeholder="请输入副标题"></el-input>
-          </el-form-item>
-          <el-form-item label="商品主图" prop="image">
-            <sa-uploader
-              v-model="form.model.image"
-              fileType="image"
-              @success="onSuccess"
-            ></sa-uploader>
-            <div class="warning-title"> 作用于商城列表、分享图片;建议尺寸:750*750 px </div>
-          </el-form-item>
-          <el-form-item label="轮播图" prop="images">
-            <sa-uploader
-              v-model="form.model.images"
-              :multiple="true"
-              :fileType="['image', 'video']"
-            ></sa-uploader>
-            <div class="warning-title sa-m-l-8">
-              作用于商品详情顶部轮播显示,<br />轮播图可以拖拽调整顺序
-            </div>
-          </el-form-item>
-          <el-form-item label="商品分类">
-            <div class="sa-w-360">
-              <el-popover
-                popper-class="category-tooltip sa-popper"
-                effect="light"
-                placement="top-start"
-                trigger="click"
-              >
-                <el-tabs v-model="tempCategory.tabActive">
-                  <el-tab-pane
-                    v-for="tab in category.select"
-                    :key="tab"
-                    :label="tab.name"
-                    :name="tab.id + ''"
-                  >
-                    <el-cascader-panel
-                      v-model="tempCategory.idsArr[tab.id]"
-                      :ref="(el) => setCategoryRef(el, tab)"
-                      :options="tab.children"
-                      :props="{
-                        multiple: true,
-                        checkStrictly: true,
-                        value: 'id',
-                        label: 'name',
-                        children: 'children',
-                        emitPath: false,
-                      }"
-                      @change="onChangeCategoryIds"
-                    ></el-cascader-panel>
-                  </el-tab-pane>
-                </el-tabs>
-                <template #reference>
-                  <div class="category-tag-wrap">
-                    <el-tag
-                      v-for="(value, key) in tempCategory.label"
-                      :key="key"
-                      type="info"
-                      closable
-                      @close.stop="onDeleteCategoryIds(key)"
-                      >{{ value }}</el-tag
-                    >
-                    <div
-                      class="category-tag-wrap-suffix"
-                      :class="JSON.stringify(tempCategory.label) == '{}' ? '' : 'is-active'"
-                    >
-                      <el-icon class="arrow-down">
-                        <arrow-down />
-                      </el-icon>
-                      <el-icon class="circle-close" @click.stop="onClearCategoryIds">
-                        <circle-close />
-                      </el-icon>
-                    </div>
-                  </div>
-                </template>
-              </el-popover>
-            </div>
-            <el-button
-              v-auth="'shop.admin.category.add'"
-              class="sa-m-l-12"
-              type="primary"
-              link
-              @click="onAddCategory"
-              >添加商品分类</el-button
-            >
-          </el-form-item>
-          <el-form-item label="商品排序">
-            <el-input
-              v-model="form.model.weigh"
-              placeholder="请输入商品排序"
-              class="sa-w-160"
-              type="number"
-              :min="0"
-              step-strictly
-            ></el-input>
-          </el-form-item>
-          <el-form-item label="限购类型">
-            <el-radio-group v-model="form.model.limit_type">
-              <el-radio label="none">不限购</el-radio>
-              <el-radio label="daily">每日</el-radio>
-              <el-radio label="all">累计</el-radio>
-            </el-radio-group>
-          </el-form-item>
-          <el-form-item label="限购数量" v-if="form.model.limit_type != 'none'">
-            <el-input
-              v-model="form.model.limit_num"
-              placeholder="请输入限购数量"
-              class="sa-w-160"
-              :min="0"
-              type="number"
-            ></el-input>
-          </el-form-item>
-          <el-form-item label="来源">
-            <el-radio-group v-model="form.model.source">
-              <el-radio label="self">自建</el-radio>
-              <el-radio label="linkedmall">LinkedMall</el-radio>
-              <el-radio label="zkh">震坤行</el-radio>
-            </el-radio-group>
-          </el-form-item>
-          <el-form-item label="商品编码">
-            <el-input
-              v-model="form.model.out_item_code"
-              placeholder="请输入商品编码"
-              class="sa-w-360"
-            ></el-input>
-          </el-form-item>
-          <el-form-item label="SPU ID">
-            <el-input
-              v-model="form.model.out_code"
-              placeholder="请输入SPU ID"
-              class="sa-w-360"
-            ></el-input>
-          </el-form-item>
-          <el-form-item label="商品状态">
-            <el-radio-group v-model="form.model.status">
-              <el-radio label="up">上架</el-radio>
-              <el-radio label="hidden">隐藏</el-radio>
-              <el-radio label="down">下架</el-radio>
-            </el-radio-group>
-          </el-form-item>
-        </div>
-      </el-form>
-      <el-form :model="form.model" :rules="form.rules" ref="formRef1" label-width="100px">
-        <div v-show="stepActive == 1">
-          <el-form-item label="商品规格">
-            <el-radio-group
-              v-model="form.model.is_sku"
-              :disabled="props.modal.params.type == 'edit'"
-            >
-              <el-radio :label="0">单规格</el-radio>
-              <el-radio :label="1">多规格</el-radio>
-            </el-radio-group>
-            <div class="warning-title">
-              如商品参与了拼团、秒杀、积分等活动,切换规格,可能导致活动规格不可用
-            </div>
-          </el-form-item>
-          <template v-if="form.model.is_sku == 1">
-            <!-- 操作 -->
-            <div class="sku-wrap">
-              <div class="sku" v-for="(s, k) in form.model.skus" :key="k">
-                <div class="sku-key sa-flex sa-row-between">
-                  <div class="sa-flex">
-                    <div class="sa-m-r-16">规格名称</div>
-                    <el-input
-                      v-model="s.name"
-                      placeholder="请输入规格名称"
-                      class="sku-key-input"
-                    ></el-input>
-                  </div>
-                  <el-icon @click="deleteMainSku(k)" class="sku-key-icon">
-                    <CircleCloseFilled />
-                  </el-icon>
-                </div>
-                <div class="sku-value sa-flex sa-flex-wrap">
-                  <div class="sku-value-title sa-m-r-16 sa-m-b-16 sa-flex"> 规格值 </div>
-                  <div v-for="(sc, c) in s.children" :key="c" class="sku-value-box sa-m-b-16">
-                    <el-input
-                      v-model="sc.name"
-                      placeholder="请输入规格值"
-                      class="sku-value-input"
-                    ></el-input>
-                    <el-icon @click="deleteChildrenSku(k, c)" class="sku-value-icon">
-                      <CircleCloseFilled />
-                    </el-icon>
-                  </div>
-                  <div
-                    @click="addChildrenSku(k)"
-                    class="sku-value-add sa-m-r-24 sa-m-b-16 sa-flex cursor-pointer"
-                  >
-                    添加规格值
-                  </div>
-                </div>
-              </div>
-              <div class="sku-tools sa-flex">
-                <el-button type="primary" class="add" @click="addMainSku">+ 添加规格</el-button>
-              </div>
-            </div>
-            <!-- 表格 -->
-            <div class="sku-table-wrap sa-m-b-20">
-              <table class="sku-table" rules="all">
-                <thead>
-                  <tr>
-                    <template v-for="(item, i) in form.model.skus" :key="i">
-                      <th v-if="item.children.length">{{ item.name }}</th>
-                    </template>
-                    <th>图片</th>
-                    <th>
-                      <div class="sa-flex">
-                        <div class="th-title">价格(元)</div>
-                        <el-popover
-                          placement="top"
-                          width="160"
-                          v-model:visible="allEditPopover.price"
-                          trigger="click"
-                        >
-                          <template #reference>
-                            <el-icon class="batch-icon">
-                              <Edit />
-                            </el-icon>
-                          </template>
-                          <el-input
-                            v-model="allEditDatas"
-                            placeholder="请输入价格"
-                            size="small"
-                            class="alledit-input"
-                            type="number"
-                            :step="0.01"
-                            :min="0"
-                            :precision="2"
-                          ></el-input>
-                          <div class="sa-flex sa-row-right">
-                            <el-button
-                              class="is-link"
-                              size="small"
-                              type="primary"
-                              @click="allEditData('price', 'cancel')"
-                              >取消</el-button
-                            >
-                            <el-button
-                              type="primary"
-                              size="small"
-                              @click="allEditData('price', 'define')"
-                              >确定</el-button
-                            >
-                          </div>
-                        </el-popover>
-                      </div>
-                    </th>
-                    <th>
-                      <div class="sa-flex">
-                        <div class="th-title">划线价格</div>
-                        <el-popover
-                          placement="top"
-                          width="160"
-                          v-model:visible="allEditPopover.original_price"
-                          trigger="click"
-                        >
-                          <template #reference>
-                            <el-icon class="batch-icon">
-                              <Edit />
-                            </el-icon>
-                          </template>
-                          <el-input
-                            v-model="allEditDatas"
-                            placeholder="请输入划线价格"
-                            size="small"
-                            class="alledit-input"
-                            type="number"
-                            :step="0.01"
-                            :min="0"
-                            :precision="2"
-                          ></el-input>
-                          <div class="sa-flex sa-row-right">
-                            <el-button
-                              size="small"
-                              type="primary"
-                              class="is-link"
-                              @click="allEditData('original_price', 'cancel')"
-                              >取消</el-button
-                            >
-                            <el-button
-                              type="primary"
-                              size="small"
-                              @click="allEditData('original_price', 'define')"
-                              >确定</el-button
-                            >
-                          </div>
-                        </el-popover>
-                      </div>
-                    </th>
-                    <th>
-                      <div class="sa-flex">
-                        <div class="th-title">成本价</div>
-                        <el-popover
-                          placement="top"
-                          width="160"
-                          v-model:visible="allEditPopover.cost_price"
-                          trigger="click"
-                        >
-                          <template #reference>
-                            <el-icon class="batch-icon">
-                              <Edit />
-                            </el-icon>
-                          </template>
-                          <el-input
-                            v-model="allEditDatas"
-                            placeholder="请输入成本价"
-                            size="small"
-                            class="alledit-input"
-                            type="number"
-                            :step="0.01"
-                            :min="0"
-                            :precision="2"
-                          ></el-input>
-                          <div class="sa-flex sa-row-right">
-                            <el-button
-                              class="is-link"
-                              size="small"
-                              type="primary"
-                              @click="allEditData('cost_price', 'cancel')"
-                              >取消</el-button
-                            >
-                            <el-button
-                              type="primary"
-                              size="small"
-                              @click="allEditData('cost_price', 'define')"
-                              >确定</el-button
-                            >
-                          </div>
-                        </el-popover>
-                      </div>
-                    </th>
-                    <th>LM库存</th>
-                    <th>库存(件)</th>
-                    <th>库存预警(件)</th>
-                    <th>
-                      <div class="sa-flex">
-                        <div class="th-title">重量(kg)</div>
-                        <el-popover
-                          placement="top"
-                          width="160"
-                          v-model:visible="allEditPopover.weight"
-                          trigger="click"
-                        >
-                          <template #reference>
-                            <el-icon class="batch-icon">
-                              <Edit />
-                            </el-icon>
-                          </template>
-                          <el-input
-                            v-model="allEditDatas"
-                            placeholder="请输入重量"
-                            size="small"
-                            class="alledit-input"
-                            type="number"
-                            :step="0.01"
-                            :min="0"
-                            :precision="2"
-                          ></el-input>
-                          <div class="sa-flex sa-row-right">
-                            <el-button
-                              class="is-link"
-                              size="small"
-                              type="primary"
-                              @click="allEditData('weight', 'cancel')"
-                              >取消</el-button
-                            >
-                            <el-button
-                              type="primary"
-                              size="small"
-                              @click="allEditData('weight', 'define')"
-                              >确定</el-button
-                            >
-                          </div>
-                        </el-popover>
-                      </div>
-                    </th>
-                    <th>
-                      <div class="sa-flex">
-                        <div class="th-title">规格编码</div>
-                        <el-popover
-                          placement="top"
-                          width="160"
-                          v-model:visible="allEditPopover.sn"
-                          trigger="click"
-                        >
-                          <template #reference>
-                            <el-icon class="batch-icon">
-                              <Edit />
-                            </el-icon>
-                          </template>
-                          <el-input
-                            v-model="allEditDatas"
-                            placeholder="请输入规格编码"
-                            size="small"
-                            class="alledit-input"
-                            type="number"
-                          ></el-input>
-                          <div class="sa-flex sa-row-right">
-                            <el-button
-                              class="is-link"
-                              size="small"
-                              type="primary"
-                              @click="allEditData('sn', 'cancel')"
-                              >取消</el-button
-                            >
-                            <el-button
-                              type="primary"
-                              size="small"
-                              @click="allEditData('sn', 'define')"
-                              >确定</el-button
-                            >
-                          </div>
-                        </el-popover>
-                      </div>
-                    </th>
-                    <th>商品状态</th>
-                  </tr>
-                </thead>
-                <tbody>
-                  <tr v-for="(item, i) in form.model.sku_prices" :key="i">
-                    <template v-for="(v, j) in item.goods_sku_text" :key="j">
-                      <td>
-                        <span class="th-center">{{ v }}</span>
-                      </td>
-                    </template>
-                    <td class="image">
-                      <sa-uploader v-model="item.image" fileType="image" size="28"></sa-uploader>
-                    </td>
-                    <td>
-                      <el-input
-                        v-model="item.price"
-                        placeholder="请输入价格"
-                        size="small"
-                        type="number"
-                        :step="0.01"
-                        :min="0"
-                        :precision="2"
-                      ></el-input>
-                    </td>
-                    <td>
-                      <el-input
-                        v-model="item.original_price"
-                        placeholder="请输入划线价格"
-                        size="small"
-                        type="number"
-                        :step="0.01"
-                        :min="0"
-                        :precision="2"
-                      ></el-input>
-                    </td>
-                    <td>
-                      <el-input
-                        v-model="item.cost_price"
-                        placeholder="请输入成本价"
-                        size="small"
-                        type="number"
-                        :step="0.01"
-                        :min="0"
-                        :precision="2"
-                      ></el-input>
-                    </td>
-                    <td class="stock">
-                      <span>
-                        {{ item.fuzzy_quantity || '暂无' }}
-                      </span>
-                    </td>
-                    <td class="stock">
-                      <el-input
-                        v-model="item.stock"
-                        placeholder="请输入库存"
-                        size="small"
-                        v-if="props.modal.params.type == 'add'"
-                        type="number"
-                        :step="1"
-                        :min="0"
-                      ></el-input>
-                      <span v-if="props.modal.params.type == 'edit'">
-                        {{ item.stock }}
-                      </span>
-                    </td>
-                    <td class="stock_warning">
-                      <div class="sa-flex">
-                        <el-switch
-                          v-model="item.stock_warning_switch"
-                          @change="changeStockWarningSwitch(i)"
-                          class="sku-stock-switch"
-                        />
-                        <span v-if="!item.stock_warning_switch">使用默认库存预警</span>
-                        <el-input
-                          v-model="item.stock_warning"
-                          placeholder="请输入"
-                          size="small"
-                          v-if="item.stock_warning_switch"
-                          type="number"
-                          :step="1"
-                          :min="0"
-                        ></el-input>
-                      </div>
-                    </td>
-                    <td>
-                      <el-input
-                        v-model="item.weight"
-                        placeholder="请输入"
-                        size="small"
-                        type="number"
-                        :step="0.01"
-                        :min="0"
-                        :precision="2"
-                      ></el-input>
-                    </td>
-                    <td class="sn">
-                      <el-input v-model="item.sn" placeholder="请输入" size="small"></el-input>
-                    </td>
-                    <td>
-                      <el-select v-model="item.status" placeholder="请选择" size="small">
-                        <el-option label="上架" value="up"></el-option>
-                        <el-option label="下架" value="down"></el-option>
-                      </el-select>
-                    </td>
-                  </tr>
-                </tbody>
-              </table>
-            </div>
-          </template>
-          <div v-if="form.model.is_sku == 0">
-            <el-form-item label="售卖价格" prop="price">
-              <el-input
-                v-model="form.model.price"
-                placeholder="请输入售卖价格"
-                class="sa-w-160"
-                type="number"
-                :min="0"
-              >
-                <template #append>元</template>
-              </el-input>
-            </el-form-item>
-            <el-form-item label="划线价格">
-              <el-input
-                v-model="form.model.original_price"
-                placeholder="请输入划线价格"
-                class="sa-w-160"
-                type="number"
-                :step="0.01"
-                :min="0"
-                :precision="2"
-              >
-                <template #append>元</template>
-              </el-input>
-            </el-form-item>
-            <el-form-item label="成本价格">
-              <el-input
-                v-model="form.model.cost_price"
-                placeholder="请输入成本价格"
-                class="sa-w-160"
-                type="number"
-                :step="0.01"
-                :min="0"
-                :precision="2"
-              >
-                <template #append>元</template>
-              </el-input>
-            </el-form-item>
-            <el-form-item label="库存类型">
-              <el-radio-group v-model="form.model.stock_show_type">
-                <el-radio label="exact">
-                  <div class="sa-flex">
-                    <div>精确显示</div>
-                    <el-popover placement="right" title :width="244" trigger="hover">
-                      <template #reference>
-                        <div class="icon-warning">
-                          <img src="/static/images/shop/category/warning.png" />
-                        </div>
-                      </template>
-                      <div class="sale-hover-img">
-                        <img src="/static/images/shop/goods/stock2.png" />
-                      </div>
-                    </el-popover>
-                  </div>
-                </el-radio>
-                <el-radio label="sketchy">
-                  <div class="sa-flex">
-                    <div>粗略显示</div>
-                    <el-popover placement="right" title :width="244" trigger="hover">
-                      <template #reference>
-                        <div class="icon-warning">
-                          <img src="/static/images/shop/category/warning.png" />
-                        </div>
-                      </template>
-                      <div class="sale-hover-img">
-                        <img src="/static/images/shop/goods/stock1.png" />
-                      </div>
-                    </el-popover>
-                  </div>
-                </el-radio>
-              </el-radio-group>
-            </el-form-item>
-            <el-form-item label="商品库存">
-              <el-input
-                v-model="form.model.stock"
-                placeholder="请输入商品库存"
-                class="sa-w-160"
-                type="number"
-                :step="1"
-                :min="0"
-                :disabled="props.modal.params.type == 'edit'"
-              >
-                <template #append>件</template>
-              </el-input>
-            </el-form-item>
-            <el-form-item label="库存预警">
-              <el-switch v-model="stockWarning" />
-              <div class="warning-title"> 库存预警在未开启的状态下,使用默认库存预警 </div>
-            </el-form-item>
-            <el-form-item v-if="stockWarning" label="预警数量">
-              <el-input
-                v-model="form.model.stock_warning"
-                placeholder="请输入库存预警数量"
-                class="sa-w-160"
-              >
-                <template #append>件</template>
-              </el-input>
-            </el-form-item>
-          </div>
-          <el-form-item label="销量类型">
-            <el-radio-group v-model="form.model.sales_show_type">
-              <el-radio label="exact">
-                <div class="sa-flex">
-                  <div>精确显示</div>
-                  <el-popover placement="right" :width="244" trigger="hover">
-                    <img class="exact" src="/static/images/shop/goods/sales2.png" />
-                    <template #reference>
-                      <div class="icon-warning">
-                        <img src="/static/images/shop/category/warning.png" />
-                      </div>
-                    </template>
-                  </el-popover>
-                </div>
-              </el-radio>
-              <el-radio label="sketchy">
-                <div class="sa-flex">
-                  <div>粗略显示</div>
-                  <el-popover placement="right" :width="244" trigger="hover">
-                    <img class="sketchy" src="/static/images/shop/goods/sales1.png" />
-                    <template #reference>
-                      <div class="icon-warning">
-                        <img src="/static/images/shop/category/warning.png" />
-                      </div>
-                    </template>
-                  </el-popover>
-                </div>
-              </el-radio>
-            </el-radio-group>
-          </el-form-item>
-          <el-form-item label="虚拟销量">
-            <div class="sa-form-wrap">
-              <el-input
-                class="sa-w-360"
-                v-model="form.model.show_sales"
-                placeholder="请输入虚拟销量"
-                type="number"
-                :min="0"
-              >
-              </el-input>
-              <div class="warning"> 可以提高商品的销量排行榜,鼓励用户下单 </div>
-            </div>
-          </el-form-item>
-          <div v-if="form.model.is_sku == 0">
-            <el-form-item label="商品重量">
-              <el-input
-                v-model="form.model.weight"
-                placeholder="请输入商品重量"
-                class="sa-w-160"
-                type="number"
-                :step="0.01"
-                :min="0"
-                :precision="2"
-              >
-                <template #append>kg</template>
-              </el-input>
-            </el-form-item>
-            <!-- <el-form-item label="商品编号">
-              <el-input
-                v-model="form.model.sn"
-                placeholder="请输入商品编号"
-                class="sa-w-160"
-              ></el-input>
-            </el-form-item> -->
-          </div>
-        </div>
-      </el-form>
-      <el-form :model="form.model" :rules="form.rules" ref="formRef2" label-width="100px">
-        <div v-show="stepActive == 2">
-          <template v-if="form.model.type == 'normal'">
-            <el-form-item label="配送方式">
-              <el-radio-group v-model="form.model.dispatch_type">
-                <el-radio label="express">物流快递</el-radio>
-              </el-radio-group>
-            </el-form-item>
-            <el-form-item v-if="form.model.dispatch_type" label="物流快递" prop="dispatch_id">
-              <el-select
-                class="sa-w-360"
-                v-model="form.model.dispatch_id"
-                placeholder="请选择物流快递"
-              >
-                <el-option
-                  v-for="item in dispatch.select"
-                  :key="item.id"
-                  :label="item.name"
-                  :value="item.id"
-                ></el-option>
-              </el-select>
-              <el-button
-                v-auth="'shop.admin.dispatch.dispatch.add'"
-                class="sa-m-l-12"
-                type="primary"
-                link
-                @click="onAddDispatch('express')"
-              >
-                添加物流快递
-              </el-button>
-            </el-form-item>
-            <el-form-item label="货到付款">
-              <el-switch
-                v-model="form.model.is_offline"
-                :active-value="1"
-                :inactive-value="0"
-              ></el-switch>
-            </el-form-item>
-          </template>
-          <template v-if="form.model.type == 'virtual'">
-            <el-form-item label="配送方式">
-              <el-radio-group v-model="form.model.dispatch_type" @change="onChangeDispatchType">
-                <el-radio label="autosend">自动发货</el-radio>
-                <el-radio label="custom"
-                  >手动发货
-                  <el-popover popper-class="sa-popper" trigger="hover">
-                    在订单管理,手动对订单进行发货,发货时填写自定义发货内容
-                    <template #reference>
-                      <el-icon class="warning">
-                        <warning />
-                      </el-icon>
-                    </template>
-                  </el-popover>
-                </el-radio>
-              </el-radio-group>
-            </el-form-item>
-            <el-form-item
-              v-if="form.model.dispatch_type == 'autosend'"
-              label="自动发货"
-              prop="dispatch_id"
-            >
-              <el-select
-                class="sa-w-360"
-                v-model="form.model.dispatch_id"
-                placeholder="请选择自动发货"
-              >
-                <el-option
-                  v-for="item in dispatch.select"
-                  :key="item.id"
-                  :label="item.name"
-                  :value="item.id"
-                ></el-option>
-              </el-select>
-              <el-button
-                v-auth="'shop.admin.dispatch.dispatch.add'"
-                class="sa-m-l-12"
-                type="primary"
-                link
-                @click="onAddDispatch('autosend')"
-              >
-                添加自动发货
-              </el-button>
-            </el-form-item>
-          </template>
-        </div>
-      </el-form>
-      <el-form :model="form.model" :rules="form.rules" ref="formRef3" label-width="100px">
-        <div v-show="stepActive == 3">
-          <el-form-item label="服务保障" prop="service_ids" class="server">
-            <el-select v-model="form.model.service_ids" placeholder="请选择服务保障" multiple>
-              <el-option
-                v-for="item in service.select"
-                :key="item.id"
-                :label="item.name"
-                :value="item.id"
-              ></el-option>
-            </el-select>
-            <el-button
-              v-auth="'shop.admin.goods.service.add'"
-              class="sa-m-l-12"
-              type="primary"
-              link
-              @click="createService"
-            >
-              添加标签
-            </el-button>
-          </el-form-item>
-          <el-form-item label="参数详情">
-            <div class="sa-template-wrap">
-              <div class="title sa-flex">
-                <div class="key">参数名称</div>
-                <div class="value">内容</div>
-                <div class="oper">操作</div>
-              </div>
-              <sa-draggable
-                v-model="form.model.params"
-                :animation="300"
-                handle=".sortable-drag"
-                item-key="element"
-              >
-                <template #item="{ element, index }">
-                  <div class="item">
-                    <el-form-item
-                      class="key"
-                      :prop="'params.' + index + '.title'"
-                      :rules="templateRules.title"
-                    >
-                      <el-input placeholder="请输入名称" v-model="element.title"></el-input>
-                    </el-form-item>
-                    <el-form-item
-                      class="value"
-                      :prop="'params.' + index + '.content'"
-                      :rules="templateRules.content"
-                    >
-                      <el-input placeholder="请输入标识" v-model="element.content"></el-input>
-                    </el-form-item>
-                    <el-form-item class="oper">
-                      <el-button @click="deleteTemplate(index)" type="danger" class="is-link" plain
-                        >删除</el-button
-                      >
-                      <sa-svg class="sa-m-l-8 sortable-drag" name="sa-round"></sa-svg>
-                    </el-form-item>
-                  </div>
-                </template>
-              </sa-draggable>
-              <el-button @click="addTemplate()" class="sa-m-l-16" type="primary" plain icon="Plus"
-                >添加</el-button
-              >
-            </div>
-          </el-form-item>
-        </div>
-      </el-form>
-      <el-form :model="form.model" :rules="form.rules" ref="formRef4" label-width="100px">
-        <div v-show="stepActive == 4">
-          <el-form-item label="图文详情">
-            <div>
-              <sa-editor v-model:content="form.model.content"></sa-editor>
-            </div>
-          </el-form-item>
-        </div>
-      </el-form>
-    </el-main>
-    <el-footer class="sa-footer--submit">
-      <el-button
-        v-if="props.modal.params.type == 'add' || props.modal.params.type == 'copy'"
-        v-auth="'shop.admin.goods.goods.add'"
-        v-throttle
-        type="primary"
-        @click="confirm"
-        >保存</el-button
-      >
-      <el-button
-        v-if="props.modal.params.type == 'edit'"
-        v-auth="'shop.admin.goods.goods.edit'"
-        v-throttle
-        type="primary"
-        @click="confirm"
-        >更新</el-button
-      >
-    </el-footer>
-  </el-container>
-</template>
-<script setup>
-  import { onMounted, reactive, ref, unref, watch, nextTick, getCurrentInstance } from 'vue';
-  import { api } from '../goods.service';
-  import { api as categoryApi } from '@/app/shop/admin/category/category.service';
-  import { api as dispatchApi } from '@/app/shop/admin/dispatch/dispatch.service';
-  import { isArray, isEmpty, cloneDeep } from 'lodash';
-  import SaEditor from '@/sheep/components/sa-editor/sa-editor.vue';
-  import SaDraggable from 'vuedraggable';
-  import { useModal } from '@/sheep/hooks';
-  import CategoryEdit from '../../category/edit.vue';
-  import DispatchEdit from '../../dispatch/edit.vue';
-  import ServiceEdit from '../service/edit.vue';
-  import { checkAuth } from '@/sheep/directives/auth';
-
-  const { proxy } = getCurrentInstance();
-
-  const emit = defineEmits(['modalCallBack']);
-  const props = defineProps({
-    modal: {
-      type: Object,
-    },
-  });
-
-  const tagWrapper = ref(null);
-  const tagInput = ref(null);
-  const updateStyle = () => {
-    const inputInner = tagInput.value?.input;
-    const tagWrapperEl = tagWrapper.value;
-    if (tagWrapperEl) {
-      const inputInitialHeight = 32;
-      const { offsetHeight } = tagWrapperEl;
-      const height = !isEmpty(tempCategory.label)
-        ? `${Math.max(offsetHeight + 6, inputInitialHeight)}px`
-        : `${inputInitialHeight}px`;
-      inputInner.style.height = height;
-      updatePopperPosition();
-    }
-  };
-  const tooltipRef = ref();
-  const updatePopperPosition = () => {
-    nextTick(() => {
-      tooltipRef.value?.updatePopper();
-    });
-  };
-
-  // 添加 编辑 form
-  let formRef = ref(null);
-  const stockWarning = ref(false);
-  const form = reactive({
-    model: {
-      type: 'normal', // 商品类型
-      title: '',
-      brand_name: '',
-      subtitle: '',
-      category_ids: '',
-      category_ids_arr: [],
-      image: '',
-      images: [],
-      params: [],
-      content: '',
-      original_price: 0,
-      price: '',
-      cost_price: '',
-      is_sku: 0,
-      limit_type: 'none',
-      limit_num: 0,
-      likes: '',
-      views: 0,
-      sales: '',
-      stock: '',
-      stock_warning: '',
-      stock_warning_switch: false,
-      sales_show_type: 'exact',
-      stock_show_type: 'exact',
-      show_sales: 0,
-      service_ids: [],
-      dispatch_type: 'express',
-      dispatch_id: '',
-      is_offline: 0,
-      status: 'up',
-      weigh: 0,
-      weight: '',
-      source: 'self', // 来源,默认为自建
-      out_code: '', // 外部编码
-      out_item_code: '', //
-      skus: [
-        {
-          id: 0,
-          name: '',
-          goods_id: 0,
-          parent_id: 0,
-          weigh: 0,
-          children: [
-            {
-              id: 0,
-              name: '',
-              goods_id: 0,
-              parent_id: 0,
-              weigh: 0,
-            },
-          ],
-        },
-      ],
-      sku_prices: [],
-    },
-    rules: {
-      image: [{ required: true, message: '请选择商品主图', trigger: 'blur' }],
-      images: [{ required: true, message: '请选择轮播图', trigger: 'blur' }],
-      title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
-      category_ids: [{ required: true, message: '请输入商品分类', trigger: 'blur' }],
-      price: [{ required: true, message: '请输入售卖价格', trigger: 'blur' }],
-      dispatch_id: [{ required: true, message: '请选择发货模板', trigger: 'blur' }],
-    },
-  });
-  const loading = ref(false);
-  //步骤条
-  const stepActive = ref(0);
-  const goback = () => {
-    stepActive.value--;
-    nextTick(() => updateStyle());
-  };
-  const next = () => {
-    unref(formRef).validate((valid) => {
-      if (valid) {
-        stepActive.value++;
-      } else {
-        return false;
-      }
-    });
-  };
-  const validateData = ref({
-    0: 0,
-    1: 0,
-    2: 0,
-    3: 0,
-    4: 0,
-  });
-  function isValidate() {
-    nextTick(async () => {
-      for (var key in validateData.value) {
-        await proxy.$refs[`formRef${key}`].validate((valid) => {
-          if (valid) {
-            validateData.value[key] = 0;
-          } else {
-            validateData.value[key] = 1;
-          }
-        });
-      }
-    });
-  }
-
-  //商品形式
-  const goodsModality = () => {
-    let modality = true;
-  };
-  // 获取详情
-  async function getDetail(id) {
-    loading.value = true;
-    const { error, data } = await api.goods.detail(id);
-    if (error === 0) {
-      form.model = data;
-      form.model.params = isArray(form.model.params) ? form.model.params : [];
-      form.model.sku_prices = isArray(form.model.sku_prices) ? form.model.sku_prices : [];
-      form.model.price = Number(data.price);
-      if (form.model.dispatch_id) {
-        dispatchCheck.value = true;
-      }
-
-      // 商品分类
-      initCategoryIds();
-
-      if (form.model.is_sku == 0) {
-        if (form.model.stock_warning > 0) {
-          stockWarning.value = true;
-        }
-      }
-
-      if (form.model.is_sku == 1) {
-        getInit();
-      }
-
-      if (!form.model.skus) {
-        form.model.skus = [];
-        getInit();
-      }
-    }
-  }
-
-  let categoryRef = {};
-  const setCategoryRef = (el, tab) => {
-    if (el) {
-      categoryRef[tab.id + '-' + tab.name] = el;
-    }
-  };
-  const tempCategory = reactive({
-    tabActive: '',
-    idsArr: {},
-    label: {},
-  });
-  function initCategoryIds() {
-    tempCategory.idsArr = {};
-    form.model.category_ids_arr.forEach((item) => {
-      if (tempCategory.idsArr[item[0]]) {
-        tempCategory.idsArr[item[0]].push(item.pop());
-      } else {
-        tempCategory.idsArr[item[0]] = [];
-        tempCategory.idsArr[item[0]].push(item.pop());
-      }
-    });
-    onChangeCategoryIds();
-  }
-  function onChangeCategoryIds() {
-    nextTick(() => {
-      tempCategory.label = {};
-      for (var key in categoryRef) {
-        let keyArr = key.split('-');
-        if (categoryRef[key].checkedNodes.length > 0) {
-          categoryRef[key].checkedNodes.forEach((row) => {
-            tempCategory.label[row.value] = keyArr[1] + '/' + row.pathLabels.join('/');
-          });
-        }
-      }
-    });
-  }
-  function onDeleteCategoryIds(id) {
-    delete tempCategory.label[id];
-    let idx = -1;
-    for (var key in tempCategory.idsArr) {
-      tempCategory.idsArr[key].forEach((item, index) => {
-        if (item == id) {
-          idx = index;
-        }
-      });
-      if (idx != -1) {
-        tempCategory.idsArr[key].splice(idx, 1);
-        idx = -1;
-      }
-    }
-  }
-  function onClearCategoryIds() {
-    tempCategory.idsArr = {};
-    tempCategory.label = {};
-  }
-  const category = reactive({
-    select: [],
-  });
-  async function getCategorySelect() {
-    ({ data: category.select } = await categoryApi.select());
-    tempCategory.tabActive = category.select.length ? category.select[0].id + '' : '';
-  }
-  //新建分类
-  function onAddCategory() {
-    useModal(
-      CategoryEdit,
-      { title: '新建', type: 'add' },
-      {
-        confirm: () => {
-          getCategorySelect();
-        },
-      },
-    );
-  }
-
-  const isEditInit = ref(false);
-  function getInit() {
-    let tempIdArr = {};
-    for (let i in form.model.skus) {
-      // 为每个 规格增加当前页面自增计数器,比较唯一用
-      form.model.skus[i]['temp_id'] = countId.value++;
-      for (let j in form.model.skus[i]['children']) {
-        // 为每个 规格项增加当前页面自增计数器,比较唯一用
-        form.model.skus[i]['children'][j]['temp_id'] = countId.value++;
-        // 记录规格项真实 id 对应的 临时 id
-        tempIdArr[form.model.skus[i]['children'][j]['id']] =
-          form.model.skus[i]['children'][j]['temp_id'];
-      }
-    }
-    for (var i = 0; i < form.model.sku_prices.length; i++) {
-      let tempSkuPrice = form.model.sku_prices[i];
-      tempSkuPrice['temp_id'] = i + 1;
-      // 将真实 id 数组,循环,找到对应的临时 id 组合成数组
-      tempSkuPrice['goods_sku_temp_ids'] = [];
-      let goods_sku_id_arr = tempSkuPrice['goods_sku_ids'].split(',');
-      for (let ids of goods_sku_id_arr) {
-        tempSkuPrice['goods_sku_temp_ids'].push(tempIdArr[ids]);
-      }
-      form.model.sku_prices[i] = tempSkuPrice;
-    }
-
-    if (props.modal.params.type == 'copy') {
-      for (let i in form.model.skus) {
-        // 为每个 规格增加当前页面自增计数器,比较唯一用
-        form.model.skus[i].id = 0;
-        for (let j in form.model.skus[i]['children']) {
-          form.model.skus[i]['children'][j].id = 0;
-        }
-      }
-    }
-
-    if (form.model.sku_prices.length > 0) {
-      form.model.sku_prices.forEach((si) => {
-        si.stock_warning_switch = false;
-        if (si.stock_warning || si.stock_warning == 0) {
-          si.stock_warning_switch = true;
-        }
-      });
-    }
-    loading.value = false;
-    setTimeout(() => {
-      isEditInit.value = true;
-    }, 200);
-  }
-  //添加主规格
-  const skuModal = ref('');
-  const countId = ref(1);
-  function addMainSku() {
-    form.model.skus.push({
-      id: 0,
-      temp_id: countId.value++,
-      name: skuModal.value,
-      pid: 0,
-      children: [],
-    });
-    skuModal.value = '';
-    buildSkuPriceTable();
-  }
-  function deleteMainSku(k) {
-    let data = form.model.skus[k];
-
-    // 删除主规格
-    form.model.skus.splice(k, 1);
-
-    // 如果当前删除的主规格存在子规格,则清空 skuPrice, 不存在子规格则不清空
-    if (data.children.length > 0) {
-      form.model.sku_prices = []; // 规格大变化,清空skuPrice
-      isResetSku.value = 1; // 重置规格
-    }
-    buildSkuPriceTable();
-  }
-  //添加子规格
-  const isResetSku = ref(0);
-  const childrenModal = [];
-  function addChildrenSku(k) {
-    let isExist = false;
-    form.model.skus[k].children.forEach((e) => {
-      if (e.name == childrenModal[k] && e.name != '') {
-        isExist = true;
-      }
-    });
-    if (isExist) {
-      alert('子规格已存在');
-      return false;
-    }
-
-    form.model.skus[k].children.push({
-      id: 0,
-      temp_id: countId.value++,
-      name: childrenModal[k],
-      pid: form.model.skus[k].id,
-    });
-    childrenModal[k] = '';
-
-    // 如果是添加的第一个子规格,清空 skuPrice
-    if (form.model.skus[k].children.length == 1) {
-      form.model.sku_prices = []; // 规格大变化,清空skuPrice
-      isResetSku.value = 1; // 重置规格
-    }
-    buildSkuPriceTable();
-  }
-  function deleteChildrenSku(k, i) {
-    let data = form.model.skus[k].children[i];
-    form.model.skus[k].children.splice(i, 1);
-
-    // 查询 skuPrice 中包含被删除的的子规格的项,然后移除
-    let deleteArr = [];
-    form.model.sku_prices.forEach((item, index) => {
-      item.goods_sku_text.forEach((e, i) => {
-        if (e == data.name) {
-          deleteArr.push(index);
-        }
-      });
-    });
-    deleteArr.sort(function (a, b) {
-      return b - a;
-    });
-    // 移除有相关子规格的项
-    deleteArr.forEach((i, e) => {
-      form.model.sku_prices.splice(i, 1);
-    });
-
-    // 当前规格项,所有子规格都被删除,清空 skuPrice
-    if (form.model.skus[k].children.length <= 0) {
-      form.model.sku_prices = []; // 规格大变化,清空skuPrice
-      isResetSku.value = 1; // 重置规格
-    }
-    buildSkuPriceTable();
-  }
-  watch(
-    () => form.model.skus,
-    () => {
-      if (isEditInit.value && form.model.is_sku) {
-        buildSkuPriceTable();
-      }
-    },
-    { deep: true },
-  );
-  //组成新的规格
-  function buildSkuPriceTable() {
-    let arr = [];
-    //遍历sku子规格生成新数组,然后执行递归笛卡尔积
-    form.model.skus.forEach((s1, k1) => {
-      let children = s1.children;
-      let childrenIdArray = [];
-      if (children.length > 0) {
-        children.forEach((s2, k2) => {
-          childrenIdArray.push(s2.temp_id);
-        });
-        // 如果 children 子规格数量为 0,则不渲染当前规格, (相当于没有这个主规格)
-        arr.push(childrenIdArray);
-      }
-    });
-    recursionSku(arr, 0, []);
-  }
-  //递归找笛卡尔规格集合
-  function recursionSku(arr, k, temp) {
-    if (k == arr.length && k != 0) {
-      let tempDetail = [];
-      let tempDetailIds = [];
-      temp.forEach((item, index) => {
-        for (let sku of form.model.skus) {
-          for (let child of sku.children) {
-            if (item == child.temp_id) {
-              tempDetail.push(child.name);
-              tempDetailIds.push(child.temp_id);
-            }
-          }
-        }
-      });
-      let flag = false; // 默认添加新的
-      for (let i = 0; i < form.model.sku_prices.length; i++) {
-        if (form.model.sku_prices[i].goods_sku_temp_ids.join(',') == tempDetailIds.join(',')) {
-          flag = i;
-          break;
-        }
-      }
-
-      if (flag === false) {
-        form.model.sku_prices.push({
-          id: 0,
-          temp_id: form.model.sku_prices.length + 1,
-          goods_sku_ids: '',
-          goods_id: 0,
-          weigh: 0,
-          image: '',
-          stock: 0,
-          stock_warning: null,
-          stock_warning_switch: false,
-          price: 0,
-          sn: '',
-          weight: 0,
-          status: 'up',
-          goods_sku_text: tempDetail,
-          goods_sku_temp_ids: tempDetailIds,
-        });
-      } else {
-        form.model.sku_prices[flag].goods_sku_text = tempDetail;
-        form.model.sku_prices[flag].goods_sku_temp_ids = tempDetailIds;
-      }
-      return;
-    }
-    if (arr.length) {
-      for (let i = 0; i < arr[k].length; i++) {
-        temp[k] = arr[k][i];
-        recursionSku(arr, k + 1, temp);
-      }
-    }
-  }
-
-  //获取库存预警
-  function changeStockWarningSwitch(e) {
-    form.model.sku_prices[e].stock_warning = form.model.sku_prices[e].stock_warning_switch
-      ? 0
-      : null;
-  }
-
-  //获取配送方式
-  const dispatchCheck = ref(true);
-  const dispatch = reactive({
-    select: [],
-  });
-  async function getDispatchSelect() {
-    ({ data: dispatch.select } = await dispatchApi.select({
-      type: form.model.dispatch_type,
-    }));
-  }
-  function onAddDispatch(dispatch_type) {
-    useModal(
-      DispatchEdit,
-      { title: '新建', type: 'add', dispatchType: dispatch_type },
-      {
-        confirm: () => {
-          getDispatchSelect();
-        },
-      },
-    );
-  }
-  function onChangeDispatchType(val) {
-    form.model.dispatch_id = val == 'custom' ? 0 : '';
-    getDispatchSelect();
-  }
-
-  // 获取服务保障
-  const service = reactive({
-    select: [],
-  });
-  async function getServiceSelect() {
-    ({ data: service.select } = await api.service.select());
-  }
-  function createService() {
-    useModal(
-      ServiceEdit,
-      { title: '新建', type: 'add' },
-      {
-        confirm: () => {
-          getServiceSelect();
-        },
-      },
-    );
-  }
-
-  const allEditDatas = ref('');
-  const allstock_warning_switch = ref(false);
-  const stock_warning_switch = ref(false);
-  const allEditPopover = reactive({
-    price: false,
-    original_price: false,
-    cost_price: false,
-    stock: false,
-    stock_warning: false,
-    weight: false,
-    sn: false,
-  });
-  const templateRules = {
-    title: [{ required: true, message: '请输入名称', trigger: 'blur' }],
-    content: [{ required: true, message: '请输入内容', trigger: 'blur' }],
-  };
-  function addTemplate() {
-    form.model.params.push({
-      title: '',
-      content: '',
-    });
-  }
-  function deleteTemplate(index) {
-    form.model.params.splice(index, 1);
-  }
-
-  //批量操作
-  //保存
-  function allEditData(type, opt) {
-    switch (opt) {
-      case 'define':
-        form.model.sku_prices.forEach((i) => {
-          if (type == 'stock_warning') {
-            if (allstock_warning_switch.value) {
-              i.stock_warning_switch.value = true;
-              if (allEditDatas.value) {
-                i[type] = allEditDatas.value;
-              } else {
-                i[type] = 0;
-              }
-            } else {
-              i.stock_warning_switch.value = false;
-              if (i.stock_warning_switch.value) {
-                i[type] = allEditDatas.value;
-              } else {
-                i[type] = null;
-              }
-            }
-          } else {
-            i[type] = allEditDatas.value;
-          }
-        });
-        allEditDatas.value = '';
-        allEditPopover[type] = false;
-        allstock_warning_switch.value = false;
-        break;
-      case 'cancel':
-        allEditDatas.value = '';
-        allEditPopover[type] = false;
-        allstock_warning_switch.value = false;
-        break;
-    }
-  }
-
-  function onChangeGoodsType(type) {
-    form.model.type = type;
-    form.model.dispatch_type = type == 'normal' ? 'express' : 'autosend';
-    form.model.dispatch_id = '';
-    getDispatchSelect();
-  }
-
-  function onSuccess(data) {
-    form.model.image_wh = {
-      w: data.image_width,
-      h: data.image_height,
-    };
-  }
-
-  // 表单关闭时提交
-  function confirm() {
-    isValidate();
-    setTimeout(async () => {
-      if (validateData.value[0] == 0 && validateData.value[1] == 0 && validateData.value[2] == 0) {
-        let submitForm = cloneDeep(form.model);
-
-        // 处理category_ids
-        let idsArr = [];
-        for (var key in tempCategory.idsArr) {
-          idsArr.push(...tempCategory.idsArr[key]);
-        }
-        submitForm.category_ids = idsArr.join(',');
-
-        if (submitForm.is_sku == 1) {
-          delete submitForm.stock;
-          delete submitForm.cost_price;
-          delete submitForm.original_price;
-          delete submitForm.price;
-          delete submitForm.stock_warning;
-          delete submitForm.sn;
-          delete submitForm.weight;
-        }
-
-        if (props.modal.params.type == 'copy') {
-          delete submitForm.id;
-        }
-
-        // 虚拟商品is_offline=0
-        if (submitForm.type == 'virtual') {
-          submitForm.is_offline = 0;
-        }
-
-        const { error } =
-          props.modal.params.type == 'add' || props.modal.params.type == 'copy'
-            ? await api.goods.add(submitForm)
-            : await api.goods.edit(props.modal.params.id, submitForm);
-        if (error == 0) {
-          emit('modalCallBack', {
-            event: 'confirm',
-          });
-        }
-      }
-    }, 500);
-  }
-  async function init() {
-    await getServiceSelect();
-    await getCategorySelect();
-    if (props.modal.params.type == 'edit' || props.modal.params.type == 'copy') {
-      await getDetail(props.modal.params.id);
-    } else {
-      getInit();
-    }
-    getDispatchSelect();
-  }
-  onMounted(() => {
-    init();
-    updateStyle();
-  });
-</script>
-<style lang="scss" scoped>
-  .goods-edit {
-    .el-header {
-      --el-header-height: fit-content;
-    }
-    .goods-type {
-      width: 140px;
-      height: 56px;
-      border: 1px solid rgb(230, 230, 230);
-      border-radius: 4px;
-      margin-left: 16px;
-      cursor: pointer;
-      &.is-active {
-        border: 1px solid var(--el-color-primary);
-      }
-    }
-    .is-error {
-      color: #ff4d4f;
-    }
-  }
-  .goods-edit {
-    .oper {
-      :deep() {
-        .el-form-item__content {
-          height: 32px;
-        }
-      }
-    }
-  }
-  .header {
-    width: 100%;
-    height: 40px;
-    color: #434343;
-    padding-left: 16px;
-    background: var(--sa-table-striped);
-    margin: 24px 0 16px 0;
-    font-weight: 500;
-    font-size: 14px;
-  }
-  //商品形式
-  .goodstype {
-    width: 140px;
-    height: 56px;
-    border-radius: 4px;
-    position: relative;
-  }
-  .badge {
-    color: var(--el-color-primary);
-    width: 16px;
-    height: 16px;
-    line-height: 16px;
-    text-align: center;
-    border-radius: 50%;
-    font-weight: 600;
-    font-size: 14px;
-    position: absolute;
-    top: -8px;
-    right: -8px;
-  }
-  //提示文本
-  .warning-title {
-    color: #faad14;
-    font-size: 12px;
-    line-height: 16px;
-    margin-left: 16px;
-  }
-  .add-category {
-    color: var(--el-color-primary);
-    font-size: 14px;
-    line-height: 16px;
-  }
-
-  //警告图标
-  .icon-warning {
-    width: 14px;
-    height: 14px;
-    display: flex;
-    margin-left: 8px;
-  }
-  //精确图片
-  .sales-hover-img {
-    width: 220px;
-    height: 98px;
-    display: flex;
-  }
-  .stock-hover-img {
-    width: 220px;
-    height: 74px;
-    display: flex;
-  }
-  //批量操作
-  .batch {
-    height: 40px;
-    padding: 0 16px;
-    border: 1px solid var(--sa-border);
-    border-top: none;
-    font-size: 12px;
-    color: var(--sa-subtitle);
-    .batch-title {
-      margin-left: 12px;
-      color: var(--el-color-primary);
-      cursor: pointer;
-    }
-    .batch-cancle {
-      margin-left: 8px;
-      color: var(--sa-subfont);
-    }
-  }
-
-  :deep() {
-    //步骤条
-    .el-step__head {
-      display: none;
-    }
-    .el-step.is-simple:not(:last-of-type) .el-step__title {
-      max-width: 100%;
-    }
-    .el-step__title.is-process {
-      font-weight: 500;
-      color: var(--el-color-primary);
-    }
-    .el-step__title.is-wait {
-      font-weight: 500;
-      color: var(--sa-font);
-    }
-    .el-step.is-simple .el-step__arrow::before {
-      transform: rotate(-45deg) translateY(-3px);
-      height: 9px;
-    }
-    .el-step.is-simple .el-step__arrow::after {
-      transform: rotate(45deg) translateY(3px);
-      height: 9px;
-    }
-    .el-steps--simple {
-      background: var(--sa-table-header-bg);
-    }
-    .server {
-      .el-form-item__content {
-        width: 100%;
-      }
-      .el-button {
-        border-radius: 0 4px 4px 0;
-        background: var(--sa-table-header-bg);
-      }
-
-      .el-select__tags {
-        padding-left: 12px;
-        .el-tag {
-          background: var(--sa-table-header-bg);
-        }
-      }
-    }
-  }
-  img {
-    width: 100%;
-    height: 100%;
-  }
-  .success {
-    color: red;
-  }
-
-  .sku-wrap {
-    width: 100%;
-    border: 1px solid #d9d9d9;
-    padding: 8px;
-    box-sizing: border-box;
-    .sku {
-      width: 100%;
-      min-height: 100px;
-      .sku-key {
-        width: 100%;
-        height: 40px;
-        color: var(--sa-subtitle);
-        padding: 0 16px;
-        background: var(--sa-table-header-bg);
-        font-size: 14px;
-        .sku-key-input {
-          width: 120px;
-        }
-        .sku-key-icon {
-          color: var(--el-color-primary);
-        }
-      }
-      .sku-value {
-        padding: 12px 0 0 30px;
-        font-size: 14px;
-        color: var(--sa-subtitle);
-        .sku-value-title {
-          height: 32px;
-        }
-        .sku-value-box {
-          position: relative;
-          margin-right: 24px;
-          .sku-value-input {
-            width: 104px;
-          }
-          .sku-value-icon {
-            position: absolute;
-            right: -8px;
-            top: -8px;
-            width: 16px;
-            height: 16px;
-            color: var(--el-color-primary);
-          }
-        }
-        .sku-value-add {
-          width: 104px;
-          height: 32px;
-          font-size: 14px;
-          color: var(--el-color-primary);
-        }
-      }
-    }
-    .sku-tools {
-      width: 100%;
-      height: 40px;
-      color: #434343;
-      padding-left: 16px;
-      background: var(--sa-table-header-bg);
-      font-size: 12px;
-      .add {
-      }
-    }
-  }
-  .alledit-input {
-    margin-bottom: 10px;
-  }
-
-  .sku-table-wrap {
-    width: 100%;
-    overflow: auto;
-    margin-top: 16px;
-    .sku-table {
-      width: 100%;
-      border: 1px solid var(--sa-border);
-      tbody {
-        font-size: 12px;
-      }
-      th {
-        font-size: 12px;
-        color: var(--subtitle);
-        height: 32px;
-        line-height: 1;
-        padding-left: 12px;
-        box-sizing: border-box;
-        text-align: left;
-        .sku-table-header-title {
-          margin-right: 10px;
-        }
-        .th-title {
-          font-size: 12px;
-          color: var(--subtitle);
-          font-weight: bold;
-        }
-      }
-      td {
-        min-width: 88px;
-        padding: 0 10px;
-        height: 40px;
-        box-sizing: border-box;
-        &.image {
-          min-width: 48px;
-        }
-        &.stock {
-          min-width: 138px;
-        }
-        &.stock_warning {
-          min-width: 168px;
-          .sku-stock-switch {
-            margin-right: 10px;
-          }
-        }
-        &.sn {
-          min-width: 116px;
-        }
-      }
-    }
-  }
-  .batch-icon {
-    width: 12px;
-    height: 12px;
-    margin-left: 10px;
-    color: var(--el-color-primary);
-  }
-</style>
-<style lang="scss">
-  .popper-category {
-    // width: 350px;
-    padding: 10px !important;
-    .el-cascader-panel {
-      overflow: auto;
-    }
-    .el-tooltip__trigger {
-      position: relative;
-      max-width: 360px;
-      width: 100%;
-    }
-  }
-  .category-content {
-    position: relative;
-    max-width: 360px;
-    width: 100%;
-    .category-tag {
-      position: absolute;
-      z-index: 10;
-      left: 0;
-      right: 76px;
-      top: 50%;
-      transform: translateY(-50%);
-      display: flex;
-      flex-wrap: wrap;
-      line-height: normal;
-      text-align: left;
-      box-sizing: border-box;
-      .el-tag {
-        margin: 2px 0 2px 6px;
-      }
-    }
-  }
-
-  .category-tag-wrap {
-    flex: 1;
-    min-height: 32px;
-    padding-right: 12px;
-    border-radius: 4px;
-    border: 1px solid var(--sa-border);
-    cursor: pointer;
-    position: relative;
-  }
-
-  .category-tag-wrap .el-tag {
-    display: inline-flex;
-    align-items: center;
-    max-width: 100%;
-    margin: 2px 0 2px 6px;
-    text-overflow: ellipsis;
-  }
-
-  .category-tag-wrap-suffix {
-    width: 12px;
-    height: 100%;
-    position: absolute;
-    right: 6px;
-    top: 0;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-  }
-
-  .category-tag-wrap-suffix .circle-close {
-    display: none;
-  }
-
-  .category-tag-wrap-suffix.is-active:hover .circle-close {
-    display: block;
-  }
-
-  .category-tag-wrap-suffix.is-active:hover .arrow-down {
-    display: none;
-  }
-</style>

+ 2 - 6
src/app/shop/admin/goods/goods/tab-edit.vue

@@ -642,13 +642,9 @@
         attr: generateAttrData(),
       };
 
-      console.log('提交属性数据:', submitData);
-      console.log('attrValue 数据格式:', submitData.attrValue);
-      console.log('attr 数据格式:', submitData.attr);
-      return;
-
       // 这里调用属性保存接口(待实现)
-      // const response = await api.goods.saveAttributes(submitData);
+      const { code, data } = await api.rule.add(submitData);
+      console.log(code, data);
 
       // 临时模拟成功
       attributesSaved.value = true;

+ 2 - 2
src/app/shop/admin/marketing/group/index.vue

@@ -186,13 +186,13 @@
   // 获取成团人数
   const getGroupNumber = (content) => {
     const parsed = parseContent(content);
-    return parsed.groupNumber || '--';
+    return parsed?.groupNumber || '--';
   };
 
   // 获取倒计时时间
   const getCountdownTime = (content) => {
     const parsed = parseContent(content);
-    return parsed.countdownTime || '--';
+    return parsed?.countdownTime || '--';
   };
 
   // 搜索字段配置

+ 120 - 11
src/sheep/components/sa-uploader/sa-upload-image.global.vue

@@ -53,13 +53,26 @@
       <!-- 上传按钮 -->
       <div
         v-if="!maxCount || imageList.length < maxCount"
-        class="upload-trigger"
-        :class="{ 'compact-trigger': compact }"
+        class="upload-wrapper"
         :style="{ width: size + 'px', height: size + 'px' }"
-        @click="handleUpload"
       >
-        <el-icon class="upload-icon" :size="compact ? 16 : 24"><Plus /></el-icon>
-        <div v-if="!compact" class="upload-text">{{ placeholder || '上传图片' }}</div>
+        <div
+          class="upload-trigger"
+          :class="{ 'compact-trigger': compact, 'is-uploading': uploading }"
+          :style="{ width: size + 'px', height: size + 'px' }"
+          @click="handleUploadClick"
+        >
+          <el-icon class="upload-icon" :size="compact ? 16 : 24">
+            <Plus />
+          </el-icon>
+          <div v-if="!compact" class="upload-text">{{ placeholder || '上传图片' }}</div>
+        </div>
+
+        <!-- 自定义loading遮罩 -->
+        <div v-if="uploading" class="upload-loading">
+          <div class="loading-spinner"></div>
+          <div v-if="!compact" class="loading-text">上传中...</div>
+        </div>
       </div>
 
       <!-- 隐藏的文件输入框 -->
@@ -176,6 +189,9 @@
   // 图片列表
   const imageList = ref([...props.modelValue]);
 
+  // 上传loading状态
+  const uploading = ref(false);
+
   // 计算接受的文件类型字符串
   const acceptString = computed(() => {
     return props.accept.map((type) => `.${type}`).join(',');
@@ -211,6 +227,13 @@
     }
   };
 
+  // 处理点击事件(带上传状态检查)
+  const handleUploadClick = () => {
+    if (!uploading.value) {
+      handleUpload();
+    }
+  };
+
   // 处理文件选择
   const handleFileSelect = async (event) => {
     const files = Array.from(event.target.files);
@@ -233,6 +256,9 @@
 
     if (validFiles.length === 0) return;
 
+    // 开始上传,显示loading
+    uploading.value = true;
+
     // 上传文件
     try {
       const uploadPromises = validFiles.map((file) => uploadFile(file));
@@ -245,9 +271,21 @@
         }
       });
 
-      ElMessage.success(`成功上传${results.filter((r) => r.success).length}张图片`);
+      const successCount = results.filter((r) => r.success).length;
+      if (successCount > 0) {
+        ElMessage.success(`成功上传${successCount}张图片`);
+      }
+
+      // 如果有失败的,显示失败信息
+      const failedCount = results.length - successCount;
+      if (failedCount > 0) {
+        ElMessage.error(`${failedCount}张图片上传失败`);
+      }
     } catch (error) {
       ElMessage.error('上传失败:' + error.message);
+    } finally {
+      // 无论成功还是失败,都关闭loading
+      uploading.value = false;
     }
 
     // 清空文件输入框
@@ -276,11 +314,13 @@
   // 上传文件
   const uploadFile = async (file) => {
     try {
-      const response = await adminApi.file.upload({}, file);
+      var formData = new FormData();
+      formData.append('file', file);
+      const response = await adminApi.file.upload({}, formData);
       if (response.code == '200') {
         return {
           success: true,
-          url: response.data.url,
+          url: response.data,
         };
       } else {
         throw new Error(response.msg || '上传失败');
@@ -399,6 +439,11 @@
       }
     }
 
+    .upload-wrapper {
+      position: relative;
+      display: inline-block;
+    }
+
     .upload-trigger {
       display: flex;
       flex-direction: column;
@@ -407,23 +452,87 @@
       border: 2px dashed #dcdfe6;
       border-radius: 6px;
       cursor: pointer;
-      transition: border-color 0.3s;
+      transition: all 0.3s;
       background: #fafafa;
 
-      &:hover {
-        // 使用系统主色调
+      &:hover:not(.is-uploading) {
+        border-color: var(--el-color-primary);
+      }
+
+      &.is-uploading {
+        cursor: not-allowed;
         border-color: var(--el-color-primary);
+        border-style: solid;
+        background: var(--el-color-primary-light-9);
+        animation: uploadingBorder 2s ease-in-out infinite;
       }
 
       .upload-icon {
         font-size: 24px;
         color: #8c939d;
         margin-bottom: 4px;
+        transition: all 0.3s;
       }
 
       .upload-text {
         font-size: 12px;
         color: #8c939d;
+        transition: all 0.3s;
+      }
+    }
+
+    .upload-loading {
+      position: absolute;
+      top: 0;
+      left: 0;
+      right: 0;
+      bottom: 0;
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      justify-content: center;
+      background: rgba(255, 255, 255, 0.9);
+      border-radius: 6px;
+      backdrop-filter: blur(2px);
+
+      .loading-spinner {
+        width: 20px;
+        height: 20px;
+        border: 2px solid #e4e7ed;
+        border-top: 2px solid var(--el-color-primary);
+        border-radius: 50%;
+        animation: spin 1s linear infinite;
+        margin-bottom: 8px;
+      }
+
+      .loading-text {
+        font-size: 12px;
+        color: var(--el-color-primary);
+        font-weight: 500;
+      }
+    }
+
+    @keyframes spin {
+      0% {
+        transform: rotate(0deg);
+      }
+      100% {
+        transform: rotate(360deg);
+      }
+    }
+
+    @keyframes uploadingBorder {
+      0% {
+        border-color: var(--el-color-primary);
+        box-shadow: 0 0 0 0 var(--el-color-primary-light-7);
+      }
+      50% {
+        border-color: var(--el-color-primary-light-3);
+        box-shadow: 0 0 0 4px var(--el-color-primary-light-8);
+      }
+      100% {
+        border-color: var(--el-color-primary);
+        box-shadow: 0 0 0 0 var(--el-color-primary-light-7);
       }
     }
 

+ 5 - 2
vite.config.js

@@ -47,9 +47,12 @@ export default (command, mode) => {
       },
       proxy: {
         '/mall': {
-          target: 'http://192.168.0.104:8101/',
+          target: 'http://192.168.0.101:8101/',
+          changeOrigin: true,
+        },
+        '/operating': {
+          target: 'http://192.168.0.101:8301/',
           changeOrigin: true,
-          // rewrite: (path) => path.replace(new RegExp(`^${API_BASE_URL}`), '/klk'),
         },
       },
     },