Эх сурвалжийг харах

feat: 完善部分商品列表接口对接

叶静 1 сар өмнө
parent
commit
8281e68a19

+ 1 - 1
src/app/shop/admin/goods/category/index.vue

@@ -186,7 +186,7 @@
   }
   // 删除api 单独批量可以直接调用
   async function deleteApi(id) {
-    await api.category.delete(id);
+    await api.category.delete({ id });
     getData();
   }
   async function batchHandle(type) {

+ 9 - 12
src/app/shop/admin/goods/goods.service.js

@@ -31,21 +31,18 @@ const route = {
 
 const api = {
   goods: {
-    ...CRUD('shop/admin/goods/goods'),
-    select: (params, type = 'page') =>
+    ...CRUD('product'),
+    // 批量上下架
+    batchShowStatus: (data) =>
       request({
-        url: `shop/admin/goods/goods/select?type=${type}`,
-        method: 'GET',
-        params,
-      }),
-    getType: () =>
-      request({
-        url: '/shop/admin/goods/goods/getType',
-        method: 'GET',
+        url: '/product/batchShowStatus',
+        method: 'POST',
+        params: data,
       }),
-    getGoodsDetail: (id) =>
+    // 获取商品状态数量
+    getStatusNum: () =>
       request({
-        url: `/shop/admin/goods/goods/detail/${id}`,
+        url: '/product/status/num',
         method: 'GET',
       }),
   },

+ 143 - 84
src/app/shop/admin/goods/goods/edit.vue

@@ -18,9 +18,9 @@
             <el-row :gutter="40">
               <!-- 左侧表单 -->
               <el-col :span="14">
-                <el-form-item label="商品分类" prop="category_id" required>
+                <el-form-item label="商品分类" prop="cateId" required>
                   <el-select
-                    v-model="formData.category_id"
+                    v-model="formData.cateId"
                     placeholder="请选择商品分类"
                     clearable
                     style="width: 100%"
@@ -34,31 +34,31 @@
                   </el-select>
                 </el-form-item>
 
-                <el-form-item label="商品名称" prop="title" required>
+                <el-form-item label="商品名称" prop="storeName" required>
                   <el-input
-                    v-model="formData.title"
+                    v-model="formData.storeName"
                     placeholder="请输入商品名称(限100字符)"
                     maxlength="100"
                     show-word-limit
                   />
                 </el-form-item>
 
-                <el-form-item label="副标题" prop="subtitle">
+                <el-form-item label="副标题" prop="keyword">
                   <el-input
-                    v-model="formData.subtitle"
+                    v-model="formData.keyword"
                     placeholder="请输入副标题(限50字符)"
                     maxlength="50"
                     show-word-limit
                   />
                 </el-form-item>
 
-                <el-form-item label="商品品牌" prop="brand" required>
-                  <el-input v-model="formData.brand" placeholder="请输入商品品牌" />
+                <el-form-item label="商品品牌" prop="itemBrand" required>
+                  <el-input v-model="formData.itemBrand" placeholder="请输入商品品牌" />
                 </el-form-item>
 
-                <el-form-item label="商品介绍" prop="description">
+                <el-form-item label="商品介绍" prop="storeInfo">
                   <el-input
-                    v-model="formData.description"
+                    v-model="formData.storeInfo"
                     type="textarea"
                     :rows="4"
                     placeholder="请输入商品介绍(限500字符)"
@@ -67,18 +67,19 @@
                   />
                 </el-form-item>
 
-                <el-form-item label="运费模板" prop="freight_template" required>
-                  <el-select
-                    v-model="formData.freight_template"
+                <el-form-item label="运费模板" prop="tempId">
+                  <div class="mt-1px">包邮</div>
+                  <!-- <el-select
+                    v-model="formData.tempId"
                     placeholder="请选择运费模板"
                     style="width: 100%"
                   >
-                    <el-option label="包邮" value="free" />
-                  </el-select>
+                    <el-option label="包邮" :value="1" />
+                  </el-select> -->
                 </el-form-item>
 
-                <el-form-item label="商品货号" prop="goods_no" required>
-                  <el-input v-model="formData.goods_no" placeholder="请输入商品货号" />
+                <el-form-item label="商品货号" prop="itemNumber" required>
+                  <el-input v-model="formData.itemNumber" placeholder="请输入商品货号" />
                   <div class="form-tip">如果您不输入商品货号,系统将自动生成一个唯一的货号</div>
                 </el-form-item>
 
@@ -94,9 +95,9 @@
                   </el-input>
                 </el-form-item>
 
-                <el-form-item label="市场价" prop="market_price">
+                <el-form-item label="市场价" prop="otPrice">
                   <el-input
-                    v-model="formData.market_price"
+                    v-model="formData.otPrice"
                     placeholder="请输入市场价"
                     type="number"
                     min="0"
@@ -118,32 +119,32 @@
                   >
                 </el-form-item>
 
-                <el-form-item label="库存预警值" prop="stock_warning">
+                <el-form-item label="库存预警值" prop="stockThreshold">
                   <el-input
-                    v-model="formData.stock_warning"
+                    v-model="formData.stockThreshold"
                     placeholder="请输入库存预警值"
                     type="number"
                     min="0"
                   />
                 </el-form-item>
 
-                <el-form-item label="商品状态" prop="status" required>
-                  <el-radio-group v-model="formData.status">
-                    <el-radio label="up">上架</el-radio>
-                    <el-radio label="down">下架</el-radio>
+                <el-form-item label="商品状态" prop="isShow" required>
+                  <el-radio-group v-model="formData.isShow">
+                    <el-radio :label="1">上架</el-radio>
+                    <el-radio :label="0">下架</el-radio>
                   </el-radio-group>
                 </el-form-item>
 
-                <el-form-item label="商品供应商" prop="supplier" required>
-                  <el-input v-model="formData.supplier" placeholder="请输入商品供应商" />
+                <el-form-item label="商品供应商" prop="itemSupplier" required>
+                  <el-input v-model="formData.itemSupplier" placeholder="请输入商品供应商" />
                 </el-form-item>
               </el-col>
 
               <!-- 右侧图片上传 -->
               <el-col :span="10">
-                <el-form-item label="商品主图" prop="images" required>
+                <el-form-item label="商品主图" prop="image" required>
                   <sa-upload-image
-                    v-model="formData.images"
+                    v-model="formData.image"
                     :max-count="5"
                     :accept="['jpg', 'jpeg', 'png']"
                     :max-size="5"
@@ -153,10 +154,10 @@
                   />
                 </el-form-item>
 
-                <el-form-item label="白底图" prop="white_bg_image">
+                <el-form-item label="轮播图" prop="sliderImage">
                   <sa-upload-image
-                    v-model="formData.white_bg_image"
-                    :max-count="1"
+                    v-model="formData.sliderImage"
+                    :max-count="5"
                     :accept="['jpg', 'jpeg', 'png']"
                     :max-size="5"
                     :direct-upload="true"
@@ -165,9 +166,9 @@
                   />
                 </el-form-item>
 
-                <el-form-item label="详情图" prop="detail_images" required>
+                <el-form-item label="详情图" prop="flatPattern" required>
                   <sa-upload-image
-                    v-model="formData.detail_images"
+                    v-model="formData.flatPattern"
                     :max-count="10"
                     :accept="['jpg', 'jpeg', 'png']"
                     :max-size="5"
@@ -444,7 +445,7 @@
   const emit = defineEmits(['success', 'modalCallBack']);
 
   // 当前步骤
-  const currentStep = ref(1);
+  const currentStep = ref(0);
 
   // 是否为编辑模式
   const isEdit = computed(() => props.modal?.params?.type === 'edit');
@@ -456,27 +457,35 @@
   // 表单数据
   const formData = reactive({
     // 基本信息
-    category_id: '',
-    title: '',
-    subtitle: '',
-    brand: '',
-    description: '',
-    freight_template: 'free',
-    goods_no: '',
+    id: '', // 商品ID,新增后获得
+    cateId: '',
+    storeName: '',
+    keyword: '',
+    itemBrand: '',
+    storeInfo: '',
+    // tempId: 1, // 运费模板ID,默认1
+    itemNumber: '',
     price: '',
-    market_price: '',
+    otPrice: '',
     stock: '',
-    stock_warning: '',
-    status: 'up',
-    supplier: '',
-
-    // 图片
-    images: [],
-    white_bg_image: [],
-    detail_images: [],
+    stockThreshold: '',
+    isShow: 1, // 状态 0-未上架 1-上架
+    itemSupplier: '',
+    sort: 0,
+    cost: '', // 成本价
+    vipPrice: '', // 会员价格
+
+    // 图片 - 数组格式,提交时转换为逗号分隔字符串
+    image: ['https://mall-oss.trust-will.com/O1CN01oBqd3M1ilz2OudYgb_!!2216255674454-0-cib.jpg'], // 商品主图
+    sliderImage: [
+      'https://mall-oss.trust-will.com/O1CN01oBqd3M1ilz2OudYgb_!!2216255674454-0-cib.jpg',
+    ], // 轮播图(白底图)
+    flatPattern: [
+      'https://mall-oss.trust-will.com/O1CN01oBqd3M1ilz2OudYgb_!!2216255674454-0-cib.jpg',
+    ], // 展示图(详情图)
 
     // 规格
-    has_spec: true, // 默认为多规格
+    specType: 1, // 规格 0单 1多
     skus: [
       {
         id: 0,
@@ -495,17 +504,17 @@
 
   // 表单验证规则
   const basicRules = {
-    category_id: [{ required: true, message: '请选择商品分类', trigger: 'change' }],
-    title: [{ required: true, message: '请输入商品名称', trigger: 'blur' }],
-    brand: [{ required: true, message: '请输入商品品牌', trigger: 'blur' }],
-    freight_template: [{ required: true, message: '请选择运费模板', trigger: 'change' }],
-    goods_no: [{ required: true, message: '请输入商品货号', trigger: 'blur' }],
+    cateId: [{ required: true, message: '请选择商品分类', trigger: 'change' }],
+    storeName: [{ required: true, message: '请输入商品名称', trigger: 'blur' }],
+    itemBrand: [{ required: true, message: '请输入商品品牌', trigger: 'blur' }],
+    // tempId: [{ required: true, message: '请选择运费模板', trigger: 'change' }],
+    itemNumber: [{ required: true, message: '请输入商品货号', trigger: 'blur' }],
     price: [{ required: true, message: '请输入商品售价', trigger: 'blur' }],
     stock: [{ required: true, message: '请输入商品库存', trigger: 'blur' }],
-    status: [{ required: true, message: '请选择商品状态', trigger: 'change' }],
-    supplier: [{ required: true, message: '请输入商品供应商', trigger: 'blur' }],
-    images: [{ required: true, message: '请上传商品主图', trigger: 'change' }],
-    detail_images: [{ required: true, message: '请上传商品详情图', trigger: 'change' }],
+    isShow: [{ required: true, message: '请选择商品状态', trigger: 'change' }],
+    itemSupplier: [{ required: true, message: '请输入商品供应商', trigger: 'blur' }],
+    image: [{ required: true, message: '请上传商品主图', trigger: 'change' }],
+    flatPattern: [{ required: true, message: '请上传商品详情图', trigger: 'change' }],
   };
 
   const attrRules = {
@@ -751,37 +760,77 @@
   const submitError = ref('');
   const submitSuccess = ref(false);
 
-  const submitForm = async () => {
+  // 图片数组转换为逗号分隔字符串的函数
+  const convertImagesToString = (imageArray) => {
+    return Array.isArray(imageArray) ? imageArray.join(',') : '';
+  };
+
+  // 图片字符串转换为数组的函数
+  const convertStringToImages = (imageString) => {
+    if (!imageString) return [];
+    return imageString.split(',').filter((img) => img.trim());
+  };
+
+  // 提交基本信息(第一步)
+  const submitBasicInfo = async () => {
     try {
       submitLoading.value = true;
       submitError.value = '';
 
-      // 准备提交数据
+      // 准备提交数据,图片转换为逗号分隔字符串
       const submitData = {
         ...formData,
+        image: convertImagesToString(formData.image),
+        sliderImage: convertImagesToString(formData.sliderImage),
+        flatPattern: convertImagesToString(formData.flatPattern),
+      };
+
+      // 调用新增商品接口
+      const response = await api.goods.add(submitData);
+
+      if (response.code == '200') {
+        ElMessage.success('商品基本信息保存成功');
+        // 保存商品ID用于后续SKU设置
+        formData.id = response.data.id;
+        currentStep.value++; // 进入下一步
+      } else {
+        throw new Error(response.message || '保存失败');
+      }
+    } catch (error) {
+      console.error('提交基本信息失败:', error);
+      submitError.value = error.message || '提交失败,请重试';
+      ElMessage.error(submitError.value);
+    } finally {
+      submitLoading.value = false;
+    }
+  };
+
+  const submitForm = async () => {
+    try {
+      submitLoading.value = true;
+      submitError.value = '';
+
+      // 准备提交SKU数据
+      const submitData = {
+        goodsId: formData.id,
+        specType: formData.specType,
         sku_prices: formData.sku_prices.map((item) => ({
           ...item,
           image: item.imageList && item.imageList.length > 0 ? item.imageList[0] : '',
         })),
       };
-      // 测试 需删除逻辑
-      console.log(submitData);
-      submitSuccess.value = true;
-      currentStep.value = 2; // 跳转到成功页面
-      return;
 
-      // 调用API提交
-      const response = await api.post('/admin/goods/save', submitData);
+      console.log('提交SKU数据:', submitData);
 
-      if (response.code === 0) {
-        submitSuccess.value = true;
-        currentStep.value = 2; // 跳转到成功页面
-        ElMessage.success('商品保存成功');
-      } else {
-        throw new Error(response.msg || '保存失败');
-      }
+      // 这里调用SKU设置接口
+      // const response = await api.goods.setSku(submitData);
+
+      // 临时测试逻辑
+      submitSuccess.value = true;
+      currentStep.value = 2; // 跳转到成功页面
+      ElMessage.success('商品SKU设置成功');
     } catch (error) {
-      console.error('提交失败:', error);
+      console.error('提交SKU失败:', error);
       submitError.value = error.message || '提交失败,请重试';
       ElMessage.error(submitError.value);
     } finally {
@@ -798,7 +847,9 @@
         ElMessage.error('请完善基本信息');
         return;
       }
-      currentStep.value++;
+
+      // 第一步完成后直接调用新增商品接口
+      await submitBasicInfo();
     } else if (currentStep.value === 1) {
       // 验证SKU并提交
       if (!validateSku()) {
@@ -900,10 +951,10 @@
   // 获取分类数据
   const getCategoryData = async () => {
     try {
-      const response = await api.goods.getType();
+      const response = await api.category.list({ size: 100 });
       if (response.code == '200') {
         // 只使用一级分类
-        categoryOptions.value = response.data.categories;
+        categoryOptions.value = response.data.list;
       }
     } catch (error) {
       console.error('获取分类失败:', error);
@@ -924,10 +975,18 @@
     try {
       // 这里调用获取商品详情接口
       console.log('加载商品数据:', id);
-      const response = await api.goods.getGoodsDetail(id);
+      const response = await api.goods.detail(id);
       if (response.code == '200') {
-        // 将数据填充到表单中
-        Object.assign(formData, response.data);
+        // 处理图片字段:将逗号分隔的字符串转换为数组
+        const data = { ...response.data };
+
+        // 转换图片字段:将逗号分隔的字符串转换为数组
+        data.image = convertStringToImages(data.image);
+        data.sliderImage = convertStringToImages(data.sliderImage);
+        data.flatPattern = convertStringToImages(data.flatPattern);
+
+        // 将处理后的数据填充到表单中
+        Object.assign(formData, data);
       }
     } catch (error) {
       console.error('加载商品数据失败:', error);

+ 227 - 73
src/app/shop/admin/goods/goods/index.vue

@@ -8,25 +8,25 @@
             <sa-search-simple
               :searchFields="searchFields"
               :defaultValues="defaultSearchValues"
-              @search="(val) => getData(1, val)"
-              @reset="getData(1)"
+              @search="handleSearch"
+              @reset="handleReset"
             >
               <template #custom="{ data }">
                 <el-form-item label="价格区间">
                   <div class="range-input-group">
-                    <el-input v-model="data.priceMin" placeholder="最低价格" clearable />
+                    <el-input v-model="data.minPrice" placeholder="最低价格" clearable />
                     <span class="range-separator">至</span>
-                    <el-input v-model="data.priceMax" placeholder="最高价格" clearable />
+                    <el-input v-model="data.maxPrice" placeholder="最高价格" clearable />
                   </div>
                 </el-form-item>
               </template>
             </sa-search-simple>
           </div>
           <el-tabs class="sa-tabs" v-model="currentStatus" @tab-change="handleTabChange">
-            <el-tab-pane label="全部(100)" name="all"></el-tab-pane>
-            <el-tab-pane label="已上架(200)" name="up"></el-tab-pane>
-            <el-tab-pane label="已下架(300)" name="down"></el-tab-pane>
-            <el-tab-pane label="已删除(400)" name="deleted"></el-tab-pane>
+            <el-tab-pane :label="`全部(${statusCounts.all})`" name="all"></el-tab-pane>
+            <el-tab-pane :label="`已上架(${statusCounts.alreadyListed})`" name="up"></el-tab-pane>
+            <el-tab-pane :label="`已下架(${statusCounts.removed})`" name="down"></el-tab-pane>
+            <el-tab-pane :label="`已删除(${statusCounts.recycle})`" name="isRecycle"></el-tab-pane>
           </el-tabs>
           <div class="sa-title sa-flex sa-row-between">
             <div class="label sa-flex">
@@ -39,6 +39,7 @@
                 @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>
@@ -67,14 +68,14 @@
               ></el-table-column>
               <el-table-column label="商品信息" min-width="300">
                 <template #default="scope">
-                  <div class="sa-flex sa-row-center">
+                  <div class="sa-flex">
                     <el-image
                       :src="scope.row.image"
                       style="width: 60px; height: 60px; margin-right: 12px"
                       fit="cover"
                     />
                     <div>
-                      <div class="goods-title">{{ scope.row.title }}</div>
+                      <div class="goods-title">{{ scope.row.storeName }}</div>
                     </div>
                   </div>
                 </template>
@@ -87,19 +88,19 @@
                 align="center"
               >
                 <template #default="scope">
-                  <div>原价: {{ scope.row.original_price || '৳1,999' }}</div>
-                  <div>在线价: {{ scope.row.current_price || '৳1,000' }}</div>
+                  <div>原价: ৳{{ scope.row.otPrice }}</div>
+                  <div>在线价: {{ scope.row.price }}</div>
                 </template>
               </el-table-column>
               <el-table-column label="状态" min-width="100" align="center">
                 <template #default="scope">
-                  <el-tag :type="statusList.color[scope.row.status]">
-                    {{ scope.row.status_text }}
+                  <el-tag :type="statusList.color[scope.row.isShow]">
+                    {{ scope.row.isShow === 1 ? '已上架' : '已下架' }}
                   </el-tag>
                 </template>
               </el-table-column>
               <el-table-column
-                prop="sku_id"
+                prop="itemNumber"
                 label="货号"
                 min-width="100"
                 align="center"
@@ -119,7 +120,7 @@
               ></el-table-column>
               <el-table-column label="供应商" min-width="120" align="center">
                 <template #default="scope">
-                  <div>{{ scope.row.supplier || '商家科技有限公司' }}</div>
+                  <div>{{ scope.row.itemSupplier || '商家科技有限公司' }}</div>
                 </template>
               </el-table-column>
               <el-table-column label="操作" min-width="150" fixed="right">
@@ -128,6 +129,12 @@
                     <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="确认"
@@ -171,16 +178,18 @@
 <script setup>
   import { onMounted, reactive, ref } from 'vue';
   import { api } from '../goods.service';
-  import { ElMessageBox } from 'element-plus';
-  import { ArrowUp } from '@element-plus/icons-vue';
+  import { ElMessageBox, ElMessage } from 'element-plus';
   import { useModal, usePagination } from '@/sheep/hooks';
 
   import GoodsEdit from './edit.vue';
+  import TabEdit from './tab-edit.vue';
 
   // getType
   const statusList = reactive({
     data: [],
     color: {
+      1: 'success',
+      0: 'danger',
       all: '',
       up: 'success',
       down: 'danger',
@@ -188,10 +197,10 @@
     },
   });
   async function getType() {
-    const { data } = await api.goods.getType();
+    const { code, data } = await api.category.list({ size: 100 });
     // 设置分类数据
-    if (data.categories) {
-      searchFields.category_id.options = data.categories.map((cat) => ({
+    if (code === '200') {
+      searchFields.cateId.options = data.list.map((cat) => ({
         label: cat.name,
         value: cat.id,
       }));
@@ -201,13 +210,13 @@
 
   // 搜索字段配置
   const searchFields = reactive({
-    title: {
+    storeName: {
       type: 'input',
       label: '商品名称',
       placeholder: '请输入商品名称',
       width: 200,
     },
-    category_id: {
+    cateId: {
       type: 'select',
       label: '商品分类',
       placeholder: '请选择商品分类',
@@ -218,13 +227,13 @@
 
   // 默认搜索值
   const defaultSearchValues = reactive({
-    title: '',
-    category_id: '',
+    storeName: '',
+    cateId: '',
     status: '',
     supplier: '',
     create_time: [],
-    priceMin: '',
-    priceMax: '',
+    minPrice: '',
+    maxPrice: '',
   });
 
   // 搜索可见性控制
@@ -232,10 +241,100 @@
   // 当前状态标签
   const currentStatus = ref('all');
 
+  // 当前搜索条件
+  const currentSearchParams = ref({});
+
+  // 状态数量
+  const statusCounts = reactive({
+    all: 0,
+    alreadyListed: 0,
+    removed: 0,
+    recycle: 0,
+  });
+
   // 标签切换处理
   const handleTabChange = (status) => {
     currentStatus.value = status;
-    getData(1, { status: status === 'all' ? '' : status });
+
+    // 根据不同状态设置不同的筛选参数
+    let statusParams = {};
+
+    switch (status) {
+      case 'all':
+        // 全部:不传递任何状态参数
+        break;
+      case 'up':
+        // 已上架:isShow = 1
+        statusParams.isShow = 1;
+        break;
+      case 'down':
+        // 已下架:isShow = 0
+        statusParams.isShow = 0;
+        break;
+      case 'isRecycle':
+        statusParams.isRecycle = 1;
+        break;
+    }
+
+    // 整合搜索条件和状态筛选
+    const allParams = { ...currentSearchParams.value, ...statusParams };
+    getData(1, allParams);
+  };
+
+  // 搜索处理
+  const handleSearch = (searchParams) => {
+    currentSearchParams.value = searchParams;
+
+    // 获取当前tab的状态参数
+    let statusParams = {};
+    switch (currentStatus.value) {
+      case 'up':
+        statusParams.isShow = 1;
+        break;
+      case 'down':
+        statusParams.isShow = 0;
+        break;
+      case 'isRecycle':
+        statusParams.isRecycle = 1;
+        break;
+    }
+
+    // 整合搜索条件和状态筛选
+    const allParams = { ...searchParams, ...statusParams };
+    getData(1, allParams);
+  };
+
+  // 重置处理
+  const handleReset = () => {
+    currentSearchParams.value = {};
+
+    // 获取当前tab的状态参数
+    let statusParams = {};
+    switch (currentStatus.value) {
+      case 'up':
+        statusParams.isShow = 1;
+        break;
+      case 'down':
+        statusParams.isShow = 0;
+        break;
+      case 'isRecycle':
+        statusParams.isRecycle = 1;
+        break;
+    }
+
+    getData(1, statusParams);
+  };
+
+  // 获取状态数量
+  const getStatusCounts = async () => {
+    try {
+      const response = await api.goods.getStatusNum();
+      if (response.code == '200') {
+        Object.assign(statusCounts, response.data);
+      }
+    } catch (error) {
+      console.error('获取状态数量失败:', error);
+    }
   };
 
   const loading = ref(true);
@@ -252,52 +351,23 @@
 
   // 获取数据
   async function getData(page, searchParams = {}) {
+    if (page) pageData.page = page;
     loading.value = true;
-    try {
-      if (page) pageData.page = page;
-
-      // 处理搜索参数
-      let search = {};
-      if (Object.keys(searchParams).length > 0) {
-        // 处理价格区间
-        if (searchParams.priceMin || searchParams.priceMax) {
-          const min = searchParams.priceMin || '0';
-          const max = searchParams.priceMax || '999999';
-          search.price = `${min} - ${max}`;
-        }
-
-        // 处理其他搜索参数
-        Object.keys(searchParams).forEach((key) => {
-          if (key !== 'priceMin' && key !== 'priceMax' && searchParams[key]) {
-            search[key] = searchParams[key];
-          }
-        });
-      }
-
-      const response = await api.goods.list({
-        page: pageData.page,
-        size: pageData.size,
-        ...search,
-        order: table.order,
-        sort: table.sort,
-      });
-
-      console.log('API 响应:', response);
-
-      if (response && response.code == '200' && response.data) {
-        table.data = response.data.data || [];
-        pageData.page = response.data.current_page || 1;
-        pageData.size = response.data.per_page || 10;
-        pageData.total = response.data.total || 0;
-      } else {
-        table.data = [];
-        console.error('获取商品数据失败:', response);
-      }
-    } catch (error) {
-      console.error('API 调用异常:', error);
-      table.data = [];
+    const { code, data } = await api.goods.list({
+      page: pageData.page,
+      size: pageData.size,
+      ...searchParams,
+    });
+    if (code == '200') {
+      table.data = data.list;
+      pageData.page = data.pageNum;
+      pageData.size = data.pageSize;
+      pageData.total = data.total;
     }
     loading.value = false;
+
+    // 刷新状态数量
+    getStatusCounts();
   }
 
   // table 字段排序
@@ -338,6 +408,7 @@
     table.selected.forEach((row) => {
       ids.push(row.id);
     });
+
     switch (type) {
       case 'delete':
         ElMessageBox.confirm('此操作将删除, 是否继续?', '提示', {
@@ -348,6 +419,24 @@
           deleteRow(ids.join(','));
         });
         break;
+      case 'up':
+        ElMessageBox.confirm('此操作将批量上架商品, 是否继续?', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning',
+        }).then(() => {
+          batchShowStatus(ids.join(','), 1);
+        });
+        break;
+      case 'down':
+        ElMessageBox.confirm('此操作将批量下架商品, 是否继续?', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning',
+        }).then(() => {
+          batchShowStatus(ids.join(','), 0);
+        });
+        break;
       default:
         handleCommand({ id: ids.join(','), type: type });
     }
@@ -368,6 +457,49 @@
       },
     );
   }
+
+  // 使用Tab编辑器新增商品
+  function addRowWithTab() {
+    useModal(
+      TabEdit,
+      {
+        title: '新增商品(Tab版)',
+        type: 'add',
+        width: '80%',
+        height: '80%',
+      },
+      {
+        confirm: () => {
+          getData();
+        },
+        success: () => {
+          getData();
+        },
+      },
+    );
+  }
+
+  // 使用Tab编辑器编辑商品
+  function editRowWithTab(row) {
+    useModal(
+      TabEdit,
+      {
+        title: '编辑商品(Tab版)',
+        type: 'edit',
+        id: row.id,
+        width: '80%',
+        height: '80%',
+      },
+      {
+        confirm: () => {
+          getData();
+        },
+        success: () => {
+          getData();
+        },
+      },
+    );
+  }
   function editRow(row) {
     useModal(
       GoodsEdit,
@@ -385,11 +517,32 @@
     );
   }
 
-  async function deleteRow(id) {
-    await api.goods.delete(id);
+  async function deleteRow(ids) {
+    // 支持单个ID或多个ID(逗号分隔)
+    const idsParam = Array.isArray(ids) ? ids.join(',') : ids;
+    await api.goods.delete({ ids: idsParam });
     getData();
   }
 
+  // 批量上下架
+  async function batchShowStatus(ids, showStatus) {
+    try {
+      const response = await api.goods.batchShowStatus({
+        ids: ids,
+        showStatus: showStatus,
+      });
+
+      if (response.code == '200') {
+        ElMessage.success(showStatus === 1 ? '批量上架成功' : '批量下架成功');
+        getData(); // 刷新列表
+      } else {
+        ElMessage.error(response.message || '操作失败');
+      }
+    } catch (error) {
+      ElMessage.error('操作失败:' + error.message);
+    }
+  }
+
   async function handleCommand(e) {
     await api.goods.edit(e.id, {
       status: e.type,
@@ -400,6 +553,7 @@
   onMounted(() => {
     getType();
     getData();
+    getStatusCounts();
   });
 </script>
 <style lang="scss">

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

@@ -0,0 +1,1983 @@
+<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>

+ 917 - 0
src/app/shop/admin/goods/goods/tab-edit.vue

@@ -0,0 +1,917 @@
+<template>
+  <el-container class="goods-edit">
+    <el-header>
+      <el-tabs class="sa-tabs sa-m-t-10" v-model="activeTab" @tab-change="handleTabChange">
+        <el-tab-pane name="basic">
+          <template #label>
+            <div class="sa-flex" :class="basicFormErrors ? 'is-error' : ''">
+              基本信息
+              <el-icon v-if="basicFormErrors" class="sa-m-l-5">
+                <WarningFilled />
+              </el-icon>
+              <el-icon v-if="basicSaved" class="sa-m-l-5 text-success">
+                <CircleCheckFilled />
+              </el-icon>
+            </div>
+          </template>
+        </el-tab-pane>
+        <el-tab-pane name="attributes" :disabled="!goodsId && !isEdit">
+          <template #label>
+            <div class="sa-flex" :class="attributesFormErrors ? 'is-error' : ''">
+              商品属性
+              <el-icon v-if="attributesFormErrors" class="sa-m-l-5">
+                <WarningFilled />
+              </el-icon>
+              <el-icon v-if="attributesSaved" class="sa-m-l-5 text-success">
+                <CircleCheckFilled />
+              </el-icon>
+              <span v-if="!goodsId && !isEdit" class="tab-disabled-tip">(需先保存基本信息)</span>
+            </div>
+          </template>
+        </el-tab-pane>
+      </el-tabs>
+    </el-header>
+
+    <el-main class="sa-p-t-10">
+      <!-- 基本信息Tab -->
+      <div v-show="activeTab === 'basic'">
+        <el-form ref="basicFormRef" :model="basicFormData" :rules="basicRules" label-width="120px">
+          <el-row :gutter="40">
+            <!-- 左侧表单 -->
+            <el-col :span="14">
+              <el-form-item label="商品分类" prop="cateId" required>
+                <el-select
+                  v-model="basicFormData.cateId"
+                  placeholder="请选择商品分类"
+                  clearable
+                  style="width: 100%"
+                >
+                  <el-option
+                    v-for="category in categoryOptions"
+                    :key="category.id"
+                    :label="category.name"
+                    :value="category.id"
+                  />
+                </el-select>
+              </el-form-item>
+
+              <el-form-item label="商品名称" prop="storeName" required>
+                <el-input
+                  v-model="basicFormData.storeName"
+                  placeholder="请输入商品名称(限100字符)"
+                  maxlength="100"
+                  show-word-limit
+                />
+              </el-form-item>
+
+              <el-form-item label="副标题" prop="keyword">
+                <el-input
+                  v-model="basicFormData.keyword"
+                  placeholder="请输入副标题(限50字符)"
+                  maxlength="50"
+                  show-word-limit
+                />
+              </el-form-item>
+
+              <el-form-item label="商品品牌" prop="itemBrand" required>
+                <el-input v-model="basicFormData.itemBrand" placeholder="请输入商品品牌" />
+              </el-form-item>
+
+              <el-form-item label="商品介绍" prop="storeInfo">
+                <el-input
+                  v-model="basicFormData.storeInfo"
+                  type="textarea"
+                  :rows="4"
+                  placeholder="请输入商品介绍(限500字符)"
+                  maxlength="500"
+                  show-word-limit
+                />
+              </el-form-item>
+
+              <el-form-item label="运费模板" prop="tempId">
+                <div class="mt-1px">包邮</div>
+              </el-form-item>
+
+              <el-form-item label="商品货号" prop="itemNumber" required>
+                <el-input v-model="basicFormData.itemNumber" placeholder="请输入商品货号" />
+                <div class="form-tip">如果您不输入商品货号,系统将自动生成一个唯一的货号</div>
+              </el-form-item>
+
+              <el-form-item label="商品售价" prop="price" required>
+                <el-input
+                  v-model="basicFormData.price"
+                  placeholder="请输入商品售价"
+                  type="number"
+                  min="0"
+                  step="0.01"
+                >
+                  <template #append>৳</template>
+                </el-input>
+              </el-form-item>
+
+              <el-form-item label="市场价" prop="otPrice">
+                <el-input
+                  v-model="basicFormData.otPrice"
+                  placeholder="请输入市场价"
+                  type="number"
+                  min="0"
+                  step="0.01"
+                >
+                  <template #append>৳</template>
+                </el-input>
+              </el-form-item>
+
+              <el-form-item label="商品库存" prop="stock" required>
+                <el-input
+                  v-model="basicFormData.stock"
+                  placeholder="请输入商品库存"
+                  type="number"
+                  min="0"
+                />
+                <div class="form-tip"
+                  >该设置只对单品有效,当商品存在多规格货品时为不可编辑状态,库存数值取决于货品数量</div
+                >
+              </el-form-item>
+
+              <el-form-item label="库存预警值" prop="stockThreshold">
+                <el-input
+                  v-model="basicFormData.stockThreshold"
+                  placeholder="请输入库存预警值"
+                  type="number"
+                  min="0"
+                />
+              </el-form-item>
+
+              <el-form-item label="商品状态" prop="isShow" required>
+                <el-radio-group v-model="basicFormData.isShow">
+                  <el-radio :label="1">上架</el-radio>
+                  <el-radio :label="0">下架</el-radio>
+                </el-radio-group>
+              </el-form-item>
+
+              <el-form-item label="商品供应商" prop="itemSupplier" required>
+                <el-input v-model="basicFormData.itemSupplier" placeholder="请输入商品供应商" />
+              </el-form-item>
+            </el-col>
+
+            <!-- 右侧图片上传 -->
+            <el-col :span="10">
+              <el-form-item label="商品主图" prop="image" required>
+                <sa-upload-image
+                  v-model="basicFormData.image"
+                  :max-count="5"
+                  :accept="['jpg', 'jpeg', 'png']"
+                  :max-size="5"
+                  :direct-upload="true"
+                  :size="100"
+                  placeholder="上传商品主图"
+                />
+                <div class="form-tip">作用于商城列表、分享图片;建议尺寸:750*750 px</div>
+              </el-form-item>
+
+              <el-form-item label="轮播图" prop="sliderImage">
+                <sa-upload-image
+                  v-model="basicFormData.sliderImage"
+                  :max-count="5"
+                  :accept="['jpg', 'jpeg', 'png']"
+                  :max-size="5"
+                  :direct-upload="true"
+                  :size="100"
+                  placeholder="上传轮播图"
+                />
+                <div class="form-tip">作用于商品详情顶部轮播显示,轮播图可以拖拽调整顺序</div>
+              </el-form-item>
+
+              <el-form-item label="详情图" prop="flatPattern" required>
+                <sa-upload-image
+                  v-model="basicFormData.flatPattern"
+                  :max-count="10"
+                  :accept="['jpg', 'jpeg', 'png']"
+                  :max-size="5"
+                  :direct-upload="true"
+                  :size="100"
+                  placeholder="上传详情图"
+                />
+                <div class="form-tip">详情图片,用于商品详情页展示</div>
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </el-form>
+
+        <!-- 基本信息保存按钮 -->
+        <div class="tab-footer">
+          <el-button
+            type="primary"
+            @click="saveBasicInfo"
+            :loading="savingStates.basic"
+            size="large"
+          >
+            保存基本信息
+          </el-button>
+          <el-button @click="closeDialog" size="large">取消</el-button>
+        </div>
+      </div>
+
+      <!-- 商品属性Tab -->
+      <div v-show="activeTab === 'attributes'">
+        <!-- 如果没有商品ID,显示提示 -->
+        <div v-if="!goodsId && !isEdit" class="tab-placeholder">
+          <el-empty description="请先保存基本信息后再编辑商品属性">
+            <el-button type="primary" @click="activeTab = 'basic'"> 去保存基本信息 </el-button>
+          </el-empty>
+        </div>
+
+        <!-- 有商品ID时显示属性表单 -->
+        <div v-else>
+          <el-form
+            ref="attributesFormRef"
+            :model="attributesFormData"
+            :rules="attributesRules"
+            label-width="120px"
+          >
+            <el-form-item label="规格类型" prop="specType">
+              <el-radio-group v-model="attributesFormData.specType">
+                <el-radio :label="0">单规格</el-radio>
+                <el-radio :label="1">多规格</el-radio>
+              </el-radio-group>
+            </el-form-item>
+
+            <!-- 多规格设置 -->
+            <div v-if="attributesFormData.specType === 1">
+              <el-form-item label="商品规格">
+                <div class="sku-container">
+                  <!-- SKU规格设置区域 -->
+                  <div class="sku-specs">
+                    <div
+                      v-for="(spec, index) in attributesFormData.skus"
+                      :key="spec.temp_id"
+                      class="spec-item"
+                    >
+                      <div class="spec-header">
+                        <span>规格{{ index + 1 }}</span>
+                        <el-button
+                          v-if="attributesFormData.skus.length > 1"
+                          type="danger"
+                          size="small"
+                          @click="removeSpec(index)"
+                        >
+                          删除
+                        </el-button>
+                      </div>
+                      <el-input
+                        v-model="spec.name"
+                        placeholder="请输入规格名称,如:颜色、尺寸"
+                        @input="generateSku"
+                      />
+                      <div class="spec-values">
+                        <el-tag
+                          v-for="(child, childIndex) in spec.children"
+                          :key="child.temp_id"
+                          closable
+                          @close="removeSpecValue(index, childIndex)"
+                        >
+                          {{ child.name }}
+                        </el-tag>
+                        <el-input
+                          v-if="spec.inputVisible"
+                          ref="specInputRef"
+                          v-model="spec.inputValue"
+                          size="small"
+                          @keyup.enter="confirmSpecValue(index)"
+                          @blur="confirmSpecValue(index)"
+                        />
+                        <el-button v-else size="small" @click="showSpecInput(index)">
+                          + 添加规格值
+                        </el-button>
+                      </div>
+                    </div>
+                    <el-button @click="addSpec" type="primary" plain>添加规格</el-button>
+                  </div>
+
+                  <!-- SKU价格库存表格 -->
+                  <div v-if="attributesFormData.sku_prices.length > 0" class="sku-table">
+                    <el-table :data="attributesFormData.sku_prices" border>
+                      <el-table-column label="规格组合" min-width="150">
+                        <template #default="scope">
+                          {{ scope.row.sku_name }}
+                        </template>
+                      </el-table-column>
+                      <el-table-column label="价格" width="120">
+                        <template #default="scope">
+                          <el-input v-model="scope.row.price" type="number" size="small" />
+                        </template>
+                      </el-table-column>
+                      <el-table-column label="库存" width="120">
+                        <template #default="scope">
+                          <el-input v-model="scope.row.stock" type="number" size="small" />
+                        </template>
+                      </el-table-column>
+                      <el-table-column label="图片" width="100">
+                        <template #default="scope">
+                          <sa-upload-image
+                            v-model="scope.row.imageList"
+                            :max-count="1"
+                            :size="50"
+                          />
+                        </template>
+                      </el-table-column>
+                    </el-table>
+                  </div>
+                </div>
+              </el-form-item>
+            </div>
+          </el-form>
+
+          <!-- 商品属性保存按钮 -->
+          <div class="tab-footer">
+            <el-button
+              type="primary"
+              @click="saveAttributes"
+              :loading="savingStates.attributes"
+              size="large"
+            >
+              保存商品属性
+            </el-button>
+          </div>
+        </div>
+      </div>
+    </el-main>
+
+    <!-- 全局操作按钮 -->
+    <el-footer class="dialog-footer">
+      <el-button @click="closeDialog" size="large">关闭</el-button>
+      <el-button type="primary" @click="saveAll" :loading="savingStates.all" size="large">
+        保存全部
+      </el-button>
+    </el-footer>
+  </el-container>
+</template>
+
+<script setup>
+  import { onMounted, reactive, ref, computed, nextTick } from 'vue';
+  import { ElMessage, ElMessageBox } from 'element-plus';
+  import { WarningFilled, CircleCheckFilled } from '@element-plus/icons-vue';
+  import { api } from '../goods.service';
+
+  // Props
+  const props = defineProps({
+    modal: {
+      type: Object,
+      required: true,
+    },
+  });
+
+  // 从modal参数中获取类型和ID
+  const type = computed(() => props.modal?.params?.type || 'add');
+  const goodsIdFromProps = computed(() => props.modal?.params?.id || null);
+
+  // Emits
+  const emit = defineEmits(['modalCallBack']);
+
+  // 响应式数据
+  const activeTab = ref('basic');
+  const goodsId = ref(goodsIdFromProps.value);
+  const isEdit = computed(() => type.value === 'edit');
+
+  // 保存状态
+  const savingStates = reactive({
+    basic: false,
+    attributes: false,
+    all: false,
+  });
+
+  // 保存成功状态
+  const basicSaved = ref(false);
+  const attributesSaved = ref(false);
+
+  // 表单错误状态
+  const basicFormErrors = ref(false);
+  const attributesFormErrors = ref(false);
+
+  // 表单引用
+  const basicFormRef = ref(null);
+  const attributesFormRef = ref(null);
+
+  // 分类选项
+  const categoryOptions = ref([]);
+
+  // 基本信息表单数据
+  const basicFormData = reactive({
+    id: '',
+    cateId: '',
+    storeName: '',
+    keyword: '',
+    itemBrand: '',
+    storeInfo: '',
+    tempId: 1,
+    itemNumber: '',
+    price: '',
+    otPrice: '',
+    stock: '',
+    stockThreshold: '',
+    isShow: 1,
+    itemSupplier: '',
+    sort: 0,
+    isHot: 0,
+    isNew: 0,
+    isBest: 0,
+    isGood: 0,
+    isBenefit: 0,
+    isPostage: 1,
+    cost: '',
+    vipPrice: '',
+    image: [],
+    sliderImage: [],
+    flatPattern: [],
+  });
+
+  // 商品属性表单数据
+  const attributesFormData = reactive({
+    specType: 1,
+    skus: [
+      {
+        id: 0,
+        temp_id: 1,
+        name: '',
+        batchId: '',
+        pid: 0,
+        children: [],
+        inputVisible: false,
+        inputValue: '',
+      },
+    ],
+    sku_prices: [],
+  });
+
+  // 基本信息验证规则
+  const basicRules = {
+    cateId: [{ required: true, message: '请选择商品分类', trigger: 'change' }],
+    storeName: [{ required: true, message: '请输入商品名称', trigger: 'blur' }],
+    itemBrand: [{ required: true, message: '请输入商品品牌', trigger: 'blur' }],
+    itemNumber: [{ required: true, message: '请输入商品货号', trigger: 'blur' }],
+    price: [{ required: true, message: '请输入商品售价', trigger: 'blur' }],
+    stock: [{ required: true, message: '请输入商品库存', trigger: 'blur' }],
+    isShow: [{ required: true, message: '请选择商品状态', trigger: 'change' }],
+    itemSupplier: [{ required: true, message: '请输入商品供应商', trigger: 'blur' }],
+    image: [{ required: true, message: '请上传商品主图', trigger: 'change' }],
+    flatPattern: [{ required: true, message: '请上传商品详情图', trigger: 'change' }],
+  };
+
+  // 商品属性验证规则
+  const attributesRules = {
+    specType: [{ required: true, message: '请选择规格类型', trigger: 'change' }],
+  };
+
+  // 图片数组转换为逗号分隔字符串的函数
+  const convertImagesToString = (imageArray) => {
+    return Array.isArray(imageArray) ? imageArray.join(',') : '';
+  };
+
+  // 图片字符串转换为数组的函数
+  const convertStringToImages = (imageString) => {
+    if (!imageString) return [];
+    return imageString.split(',').filter((img) => img.trim());
+  };
+
+  // Tab切换处理
+  const handleTabChange = (tabName) => {
+    if (tabName === 'attributes' && !goodsId.value && !isEdit.value) {
+      nextTick(() => {
+        ElMessage.info('提示:保存商品属性需要先保存基本信息');
+      });
+    }
+  };
+
+  // 保存基本信息
+  const saveBasicInfo = async () => {
+    try {
+      savingStates.basic = true;
+      basicFormErrors.value = false;
+
+      // 验证表单
+      const valid = await basicFormRef.value?.validate().catch(() => false);
+      if (!valid) {
+        basicFormErrors.value = true;
+        ElMessage.error('请完善基本信息');
+        return false;
+      }
+
+      // 准备提交数据,图片转换为逗号分隔字符串
+      const submitData = {
+        ...basicFormData,
+        image: convertImagesToString(basicFormData.image),
+        sliderImage: convertImagesToString(basicFormData.sliderImage),
+        flatPattern: convertImagesToString(basicFormData.flatPattern),
+      };
+
+      console.log('提交基本信息:', submitData);
+
+      // 调用API
+      const response = isEdit.value
+        ? await api.goods.edit(goodsId.value, submitData)
+        : await api.goods.add(submitData);
+
+      if (response.code == '200') {
+        if (!isEdit.value) {
+          goodsId.value = response.data.id;
+          basicFormData.id = response.data.id;
+        }
+        basicSaved.value = true;
+        ElMessage.success('基本信息保存成功');
+        return true;
+      } else {
+        throw new Error(response.message || '保存失败');
+      }
+    } catch (error) {
+      basicFormErrors.value = true;
+      ElMessage.error('保存失败:' + error.message);
+      return false;
+    } finally {
+      savingStates.basic = false;
+    }
+  };
+
+  // 保存商品属性(内部方法)
+  const saveAttributesInternal = async () => {
+    try {
+      // 验证属性表单
+      const valid = await attributesFormRef.value?.validate().catch(() => false);
+      if (!valid) {
+        attributesFormErrors.value = true;
+        ElMessage.error('请完善商品属性');
+        return false;
+      }
+
+      // 准备属性数据
+      const submitData = {
+        goodsId: goodsId.value,
+        specType: attributesFormData.specType,
+        skus: attributesFormData.skus,
+        sku_prices: attributesFormData.sku_prices.map((item) => ({
+          ...item,
+          image: item.imageList && item.imageList.length > 0 ? item.imageList[0] : '',
+        })),
+      };
+
+      console.log('提交属性数据:', submitData);
+
+      // 这里调用属性保存接口(待实现)
+      // const response = await api.goods.saveAttributes(submitData);
+
+      // 临时模拟成功
+      attributesSaved.value = true;
+      ElMessage.success('商品属性保存成功');
+      return true;
+    } catch (error) {
+      attributesFormErrors.value = true;
+      ElMessage.error('保存失败:' + error.message);
+      return false;
+    }
+  };
+
+  // 保存商品属性(对外方法)
+  const saveAttributes = async () => {
+    try {
+      savingStates.attributes = true;
+      attributesFormErrors.value = false;
+
+      // 检查依赖
+      if (!goodsId.value && !isEdit.value) {
+        const result = await ElMessageBox.confirm(
+          '保存商品属性需要先保存基本信息,是否现在保存基本信息?',
+          '提示',
+          {
+            confirmButtonText: '保存基本信息并继续',
+            cancelButtonText: '取消',
+            type: 'warning',
+          },
+        );
+
+        if (result === 'confirm') {
+          // 先保存基本信息
+          const basicSaved = await saveBasicInfo();
+
+          if (basicSaved) {
+            // 基本信息保存成功后,保存属性
+            await saveAttributesInternal();
+          }
+        }
+        return;
+      }
+
+      // 直接保存属性
+      await saveAttributesInternal();
+    } finally {
+      savingStates.attributes = false;
+    }
+  };
+
+  // 保存全部
+  const saveAll = async () => {
+    try {
+      savingStates.all = true;
+
+      // 1. 如果没有商品ID,先保存基本信息
+      if (!goodsId.value && !isEdit.value) {
+        const basicSaved = await saveBasicInfo();
+        if (!basicSaved) return;
+      }
+
+      // 2. 如果属性有修改,保存属性
+      await saveAttributesInternal();
+
+      ElMessage.success('保存成功');
+      emit('modalCallBack', { event: 'confirm' });
+    } catch (error) {
+      ElMessage.error('保存失败:' + error.message);
+    } finally {
+      savingStates.all = false;
+    }
+  };
+
+  // 关闭对话框
+  const closeDialog = () => {
+    emit('modalCallBack', { event: 'close' });
+  };
+
+  // SKU相关方法
+  let tempId = 2;
+
+  // 添加规格
+  const addSpec = () => {
+    attributesFormData.skus.push({
+      id: 0,
+      temp_id: tempId++,
+      name: '',
+      batchId: '',
+      pid: 0,
+      children: [],
+      inputVisible: false,
+      inputValue: '',
+    });
+  };
+
+  // 删除规格
+  const removeSpec = (index) => {
+    attributesFormData.skus.splice(index, 1);
+    generateSku();
+  };
+
+  // 显示规格值输入框
+  const showSpecInput = (index) => {
+    attributesFormData.skus[index].inputVisible = true;
+    nextTick(() => {
+      // 聚焦到输入框
+    });
+  };
+
+  // 确认规格值
+  const confirmSpecValue = (index) => {
+    const spec = attributesFormData.skus[index];
+    const inputValue = spec.inputValue;
+
+    if (inputValue && !spec.children.find((child) => child.name === inputValue)) {
+      spec.children.push({
+        id: 0,
+        temp_id: tempId++,
+        name: inputValue,
+        pid: spec.temp_id,
+      });
+      generateSku();
+    }
+
+    spec.inputVisible = false;
+    spec.inputValue = '';
+  };
+
+  // 删除规格值
+  const removeSpecValue = (specIndex, valueIndex) => {
+    attributesFormData.skus[specIndex].children.splice(valueIndex, 1);
+    generateSku();
+  };
+
+  // 生成SKU组合
+  const generateSku = () => {
+    const validSpecs = attributesFormData.skus.filter(
+      (spec) => spec.name && spec.children.length > 0,
+    );
+
+    if (validSpecs.length === 0) {
+      attributesFormData.sku_prices = [];
+      return;
+    }
+
+    // 生成笛卡尔积
+    const combinations = cartesianProduct(validSpecs.map((spec) => spec.children));
+
+    attributesFormData.sku_prices = combinations.map((combination) => {
+      const skuName = combination.map((item) => item.name).join(' / ');
+      const skuKey = combination.map((item) => item.temp_id).join('_');
+
+      // 查找是否已存在
+      const existing = attributesFormData.sku_prices.find((item) => item.sku_key === skuKey);
+
+      return (
+        existing || {
+          sku_key: skuKey,
+          sku_name: skuName,
+          price: '',
+          stock: '',
+          imageList: [],
+        }
+      );
+    });
+  };
+
+  // 笛卡尔积计算
+  const cartesianProduct = (arrays) => {
+    return arrays.reduce(
+      (acc, curr) => {
+        const result = [];
+        acc.forEach((a) => {
+          curr.forEach((c) => {
+            result.push([...a, c]);
+          });
+        });
+        return result;
+      },
+      [[]],
+    );
+  };
+
+  // 获取分类数据
+  const getCategoryData = async () => {
+    try {
+      const response = await api.category.list({ size: 100 });
+      if (response.code == '200') {
+        categoryOptions.value = response.data.list.map((cat) => ({
+          label: cat.name,
+          value: cat.id,
+          id: cat.id,
+          name: cat.name,
+        }));
+      }
+    } catch (error) {
+      console.error('获取分类数据失败:', error);
+    }
+  };
+
+  // 加载商品详情
+  const loadGoodsDetail = async () => {
+    if (!goodsId.value) return;
+
+    try {
+      const response = await api.goods.detail(goodsId.value);
+      if (response.code == '200') {
+        // 处理图片字段:将逗号分隔的字符串转换为数组
+        const data = { ...response.data };
+
+        // 转换图片字段
+        data.image = convertStringToImages(data.image);
+        data.sliderImage = convertStringToImages(data.sliderImage);
+        data.flatPattern = convertStringToImages(data.flatPattern);
+
+        // 将处理后的数据填充到表单中
+        Object.assign(basicFormData, data);
+
+        // 标记为已保存
+        basicSaved.value = true;
+      }
+    } catch (error) {
+      console.error('加载商品详情失败:', error);
+      ElMessage.error('加载商品详情失败');
+    }
+  };
+
+  // 初始化
+  const init = async () => {
+    await getCategoryData();
+
+    if (isEdit.value && goodsId.value) {
+      await loadGoodsDetail();
+    }
+  };
+
+  // 组件挂载
+  onMounted(() => {
+    init();
+  });
+</script>
+
+<style scoped lang="scss">
+  .goods-edit {
+    height: 100%;
+
+    .el-header {
+      height: auto;
+      padding: 0;
+    }
+
+    .el-main {
+      padding: 20px;
+    }
+
+    .el-footer {
+      height: auto;
+      padding: 20px;
+      border-top: 1px solid var(--el-border-color-light);
+      display: flex;
+      justify-content: flex-end;
+      gap: 12px;
+    }
+  }
+
+  .tab-footer {
+    margin-top: 30px;
+    padding-top: 20px;
+    border-top: 1px solid var(--el-border-color-light);
+    display: flex;
+    gap: 12px;
+  }
+
+  .tab-placeholder {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    min-height: 300px;
+  }
+
+  .tab-disabled-tip {
+    font-size: 12px;
+    color: var(--el-color-info);
+    margin-left: 4px;
+  }
+
+  .is-error {
+    color: var(--el-color-danger);
+  }
+
+  .text-success {
+    color: var(--el-color-success);
+  }
+
+  .sku-container {
+    border: 1px solid var(--el-border-color);
+    border-radius: 4px;
+    padding: 16px;
+  }
+
+  .sku-specs {
+    margin-bottom: 20px;
+  }
+
+  .spec-item {
+    margin-bottom: 16px;
+    padding: 12px;
+    border: 1px solid var(--el-border-color-light);
+    border-radius: 4px;
+  }
+
+  .spec-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 8px;
+    font-weight: 500;
+  }
+
+  .spec-values {
+    margin-top: 8px;
+    display: flex;
+    flex-wrap: wrap;
+    gap: 8px;
+    align-items: center;
+  }
+
+  .sku-table {
+    margin-top: 20px;
+  }
+
+  :deep(.el-tabs__header) {
+    margin: 0;
+  }
+
+  :deep(.el-tabs__nav-wrap::after) {
+    height: 1px;
+  }
+
+  :deep(.el-tabs__item) {
+    padding: 0 20px;
+    font-size: 14px;
+  }
+
+  :deep(.el-tabs__nav) {
+    border: none;
+  }
+
+  .form-tip {
+    font-size: 12px;
+    color: var(--el-color-info);
+    margin-top: 4px;
+    line-height: 1.4;
+  }
+
+  .mt-1px {
+    margin-top: 1px;
+  }
+</style>

+ 2 - 2
src/sheep/request/crud.js

@@ -44,11 +44,11 @@ export const EDIT = (url, data) =>
   });
 
 // 删除(软删除/真实删除)
-export const DELETE = (url, id) =>
+export const DELETE = (url, data) =>
   request({
     url: url + `/delete`,
     method: 'POST',
-    params: { id },
+    params: data,
     options: {
       showSuccessMessage: true,
     },

+ 1 - 1
vite.config.js

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