叶静 2 tygodni temu
rodzic
commit
f2b20dbf3d

+ 22 - 2
src/app/shop/admin/data/report/components/chart-tips-box.vue

@@ -31,7 +31,7 @@
 </template>
 
 <script setup>
-import { ref, watch, nextTick, onUnmounted } from 'vue'
+import { ref, watch, nextTick, onMounted, onUnmounted } from 'vue'
 
 // Props
 const props = defineProps({
@@ -57,7 +57,26 @@ const isDragging = ref(false)
 const dragOffset = ref({ x: 0, y: 0 })
 const position = ref({ x: 0, y: 0 })
 
-// 移除遮罩层点击处理,不再需要
+// 全局点击关闭处理
+const handleGlobalClick = (event) => {
+  if (!props.visible) return
+
+  // 如果点击的是提示框内部,不关闭
+  if (tipsBoxRef.value && tipsBoxRef.value.contains(event.target)) {
+    return
+  }
+
+  // 点击外部区域,关闭提示框
+  emit('close')
+}
+
+// 生命周期
+onMounted(() => {
+  // 监听全局点击事件
+  document.addEventListener('click', handleGlobalClick, true)
+})
+
+
 
 // 格式化数字
 function formatNumber(value) {
@@ -166,6 +185,7 @@ const cleanup = () => {
   document.removeEventListener('mousemove', handleDragMove)
   document.removeEventListener('mouseup', handleDragEnd)
   document.removeEventListener('keydown', handleKeyDown)
+  document.removeEventListener('click', handleGlobalClick, true)
 }
 
 onUnmounted(() => {

+ 225 - 87
src/app/shop/admin/data/report/components/report-chart.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="report-chart">
+  <div class="report-chart" ref="reportChartRef">
     <el-card>
       <template #header>
         <div class="chart-header">
@@ -83,7 +83,8 @@
               <template #description>
                 <span class="empty-description">{{ t('dataReport.chartPlaceholder', {
                   title: title ||
-                    t('dataReport.chart') }) }}</span>
+                    t('dataReport.chart')
+                }) }}</span>
               </template>
             </el-empty>
           </div>
@@ -112,6 +113,46 @@ import VChart from 'vue-echarts'
 import * as echarts from 'echarts'
 import ChartTipsBox from './chart-tips-box.vue'
 
+// 全局提示框管理器
+class TipsBoxManager {
+  constructor() {
+    this.currentTipsBox = null
+    this.listeners = new Set()
+  }
+
+  // 注册提示框
+  register(tipsBoxInstance) {
+    this.listeners.add(tipsBoxInstance)
+  }
+
+  // 注销提示框
+  unregister(tipsBoxInstance) {
+    this.listeners.delete(tipsBoxInstance)
+  }
+
+  // 显示新的提示框,关闭其他的
+  showTipsBox(tipsBoxInstance) {
+    // 关闭其他所有提示框
+    this.listeners.forEach(instance => {
+      if (instance !== tipsBoxInstance) {
+        instance.close()
+      }
+    })
+    this.currentTipsBox = tipsBoxInstance
+  }
+
+  // 关闭当前提示框
+  closeCurrent() {
+    if (this.currentTipsBox) {
+      this.currentTipsBox.close()
+      this.currentTipsBox = null
+    }
+  }
+}
+
+// 全局单例
+const tipsBoxManager = new TipsBoxManager()
+
 // 使用国际化
 const { t } = useI18n();
 
@@ -177,6 +218,7 @@ const emit = defineEmits(['refresh', 'dateChange', 'chartClick', 'typeChange'])
 
 // 响应式数据
 const chartRef = ref()
+const reportChartRef = ref() // 添加图表容器的引用
 const currentType = ref(props.type)
 const dateRange = ref([])
 const isFullscreen = ref(false)
@@ -199,10 +241,20 @@ const availableTypes = computed(() => [
 ])
 
 // 计算属性
-const chartStyle = computed(() => ({
-  height: typeof props.height === 'number' ? `${props.height}px` : props.height,
-  width: '100%'
-}))
+const chartStyle = computed(() => {
+  if (isFullscreen.value) {
+    // 全屏时使用视窗高度减去头部高度
+    return {
+      height: 'calc(100vh - 120px)',
+      width: '100%'
+    }
+  }
+  // 正常状态使用props中的高度
+  return {
+    height: typeof props.height === 'number' ? `${props.height}px` : props.height,
+    width: '100%'
+  }
+})
 
 const chartOption = computed(() => {
   if (!props.data || props.data.length === 0) {
@@ -555,6 +607,9 @@ const handleChartClick = (params) => {
   // 处理图表点击事件 - 显示提示框
   console.log('图表点击事件:', params)
 
+  // 先关闭其他图表的提示框
+  tipsBoxManager.closeCurrent()
+
   // 构建提示框数据
   const clickData = params.data || params
   const chartData = props.data || []
@@ -589,21 +644,19 @@ const handleChartClick = (params) => {
       percent: totalValue > 0 ? `${((item[props.yAxisKey] || 0) / totalValue * 100).toFixed(2)}%` : '0%',
       color: props.colors[index % props.colors.length]
     }))
-
-    // 添加汇总行
-    if (items.length > 1) {
-      items.push({
-        name: `${chartTitle}汇总`,
-        value: totalValue,
-        percent: '100%',
-        color: '#999'
-      })
-    }
   }
 
   tipsBoxData.value = { title, items }
   tipsBoxVisible.value = true
 
+  // 注册当前提示框到管理器
+  const currentTipsBoxInstance = {
+    close: () => {
+      tipsBoxVisible.value = false
+    }
+  }
+  tipsBoxManager.showTipsBox(currentTipsBoxInstance)
+
   // 同时发送原有的点击事件,保持向后兼容
   emit('chartClick', {
     ...params,
@@ -618,13 +671,52 @@ const closeTipsBox = () => {
   tipsBoxVisible.value = false
 }
 
-const toggleFullscreen = () => {
-  isFullscreen.value = !isFullscreen.value
-  nextTick(() => {
-    if (chartRef.value) {
-      chartRef.value.resize()
+const toggleFullscreen = async () => {
+  try {
+    if (!isFullscreen.value) {
+      // 进入全屏 - 使用当前组件的引用
+      const element = reportChartRef.value
+      if (!element) {
+        console.warn('图表容器引用不存在')
+        return
+      }
+
+      if (element.requestFullscreen) {
+        await element.requestFullscreen()
+      } else if (element.webkitRequestFullscreen) {
+        await element.webkitRequestFullscreen()
+      } else if (element.mozRequestFullScreen) {
+        await element.mozRequestFullScreen()
+      } else if (element.msRequestFullscreen) {
+        await element.msRequestFullscreen()
+      }
+      isFullscreen.value = true
+    } else {
+      // 退出全屏
+      if (document.exitFullscreen) {
+        await document.exitFullscreen()
+      } else if (document.webkitExitFullscreen) {
+        await document.webkitExitFullscreen()
+      } else if (document.mozCancelFullScreen) {
+        await document.mozCancelFullScreen()
+      } else if (document.msExitFullscreen) {
+        await document.msExitFullscreen()
+      }
+      isFullscreen.value = false
     }
-  })
+
+    // 延迟调整图表大小,确保DOM更新完成
+    nextTick(() => {
+      setTimeout(() => {
+        if (chartRef.value) {
+          chartRef.value.resize()
+        }
+      }, 200) // 增加延迟时间,确保全屏动画完成
+    })
+  } catch (error) {
+    console.warn('全屏操作失败:', error)
+    ElMessage.warning('您的浏览器不支持全屏功能')
+  }
 }
 
 // 监听数据变化
@@ -641,6 +733,17 @@ watch(() => props.type, (newType) => {
   currentType.value = newType
 })
 
+// 监听全屏状态变化
+watch(isFullscreen, () => {
+  nextTick(() => {
+    setTimeout(() => {
+      if (chartRef.value) {
+        chartRef.value.resize()
+      }
+    }, 300) // 等待全屏动画完成
+  })
+})
+
 // 窗口大小变化处理
 const handleResize = () => {
   if (chartRef.value) {
@@ -648,13 +751,51 @@ const handleResize = () => {
   }
 }
 
+// 全屏状态变化监听
+const handleFullscreenChange = () => {
+  const currentFullscreenElement =
+    document.fullscreenElement ||
+    document.webkitFullscreenElement ||
+    document.mozFullScreenElement ||
+    document.msFullscreenElement
+
+  // 检查当前全屏的元素是否是这个组件
+  const isThisComponentFullscreen = currentFullscreenElement === reportChartRef.value
+
+  if (!currentFullscreenElement && isFullscreen.value) {
+    // 用户按ESC退出全屏,且之前是这个组件全屏,更新状态
+    isFullscreen.value = false
+    setTimeout(() => {
+      if (chartRef.value) {
+        chartRef.value.resize()
+      }
+    }, 100)
+  } else if (isThisComponentFullscreen && !isFullscreen.value) {
+    // 这个组件进入全屏,但状态还没更新
+    isFullscreen.value = true
+  } else if (!isThisComponentFullscreen && isFullscreen.value) {
+    // 其他元素全屏了,但这个组件状态还是全屏,需要重置
+    isFullscreen.value = false
+  }
+}
+
 // 生命周期
 onMounted(() => {
   window.addEventListener('resize', handleResize)
+  // 监听全屏状态变化
+  document.addEventListener('fullscreenchange', handleFullscreenChange)
+  document.addEventListener('webkitfullscreenchange', handleFullscreenChange)
+  document.addEventListener('mozfullscreenchange', handleFullscreenChange)
+  document.addEventListener('MSFullscreenChange', handleFullscreenChange)
 })
 
 onUnmounted(() => {
   window.removeEventListener('resize', handleResize)
+  // 移除全屏状态监听
+  document.removeEventListener('fullscreenchange', handleFullscreenChange)
+  document.removeEventListener('webkitfullscreenchange', handleFullscreenChange)
+  document.removeEventListener('mozfullscreenchange', handleFullscreenChange)
+  document.removeEventListener('MSFullscreenChange', handleFullscreenChange)
 })
 
 // 暴露方法
@@ -683,6 +824,7 @@ defineExpose({
 </script>
 
 <style scoped>
+/* 全屏样式需要使用 :global 确保在全屏时能正确应用 */
 @import '../styles/report-common.scss';
 
 .chart-container {
@@ -698,76 +840,72 @@ defineExpose({
     }
   }
 
-  &.fullscreen {
-    position: fixed;
-    top: 0;
-    left: 0;
-    width: 100vw;
-    height: 100vh;
-    z-index: 9999;
-    background: white;
-    display: flex;
-    flex-direction: column;
-
-    .fullscreen-header {
-      display: flex;
-      justify-content: space-between;
-      align-items: center;
-      padding: 16px 24px;
-      border-bottom: 1px solid #e4e7ed;
-      background: white;
-      z-index: 10000;
-
-      .chart-title {
-        margin: 0;
-        font-size: 18px;
-        font-weight: 600;
-        color: #303133;
-      }
+}
+</style>
+
+<style>
+/* 全屏样式 - 全局样式,确保在全屏时正确应用 */
+.chart-container.fullscreen {
+  position: fixed !important;
+  top: 0 !important;
+  left: 0 !important;
+  width: 100vw !important;
+  height: 100vh !important;
+  z-index: 9999 !important;
+  background: white !important;
+  display: flex !important;
+  flex-direction: column !important;
+}
 
-      .chart-controls {
-        display: flex;
-        align-items: center;
-        gap: 16px;
-      }
-    }
+.chart-container.fullscreen .fullscreen-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 16px 24px;
+  border-bottom: 1px solid #e4e7ed;
+  background: white;
+  z-index: 10000;
+}
 
-    .echarts {
-      flex: 1;
-      height: auto !important;
-    }
+.chart-container.fullscreen .fullscreen-header .chart-title {
+  margin: 0;
+  font-size: 18px;
+  font-weight: 600;
+  color: #303133;
+}
 
-    .no-data {
-      .empty-chart-placeholder {
-        display: flex;
-        align-items: center;
-        justify-content: center;
-        background: #fafafa;
-        border: 1px dashed #d9d9d9;
-        border-radius: 6px;
-        min-height: 300px;
-
-        .empty-chart-icon {
-          font-size: 48px;
-          margin-bottom: 16px;
-          opacity: 0.6;
-        }
+.chart-container.fullscreen .fullscreen-header .chart-controls {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+}
 
-        .empty-description {
-          color: #909399;
-          font-size: 14px;
-        }
+.chart-container.fullscreen .chart-content {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+}
 
-        :deep(.el-empty) {
-          padding: 40px 0;
-        }
+.chart-container.fullscreen .chart-content .echarts {
+  flex: 1;
+  height: auto !important;
+  min-height: calc(100vh - 120px);
+}
 
-        :deep(.el-empty__image) {
-          width: auto;
-          height: auto;
-        }
-      }
-    }
-  }
+.chart-container.fullscreen .no-data {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+}
+
+.chart-container.fullscreen .no-data .empty-chart-placeholder {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: #fafafa;
+  border: 1px dashed #d9d9d9;
+  border-radius: 6px;
+  min-height: calc(100vh - 120px);
 }
 </style>

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

@@ -0,0 +1,1112 @@
+<template>
+  <el-container>
+    <el-header>
+      <el-tabs class="sa-tabs bg-#fff sa-m-t-10 z-999" 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-30">
+      <!-- 基本信息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 ml-10px">如果您不输入商品货号,系统将自动生成一个唯一的货号</div>
+              </el-form-item>
+
+              <el-form-item label="商品售价" prop="price" required>
+                <el-select v-model="basicFormData.price" placeholder="请选择商品售价" clearable>
+                  <el-option :value="300" label="300৳" />
+                  <el-option :value="500" label="500৳" />
+                  <el-option :value="1000" label="1000৳" />
+                  <el-option :value="2000" label="2000৳" />
+                  <el-option :value="3000" label="3000৳" />
+                </el-select>
+              </el-form-item>
+
+              <el-form-item label="市场价" prop="otPrice">
+                <el-select v-model="basicFormData.otPrice" placeholder="请选择市场价" clearable>
+                  <el-option :value="300" label="300৳" />
+                  <el-option :value="500" label="500৳" />
+                  <el-option :value="1000" label="1000৳" />
+                  <el-option :value="2000" label="2000৳" />
+                  <el-option :value="3000" label="3000৳" />
+                </el-select>
+              </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 ml-10px">该设置只对单品有效,当商品存在多规格货品时为不可编辑状态,库存数值取决于货品数量</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>
+
+      <!-- 商品属性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>
+          <el-form ref="attributesFormRef" :model="attributesFormData" :rules="attributesRules" label-width="120px">
+            <!-- 多规格设置 -->
+            <el-card class="spec-card">
+              <template #header>
+                <div class="card-header">
+                  <span>商品规格设置</span>
+                </div>
+              </template>
+
+              <!-- 操作 -->
+              <div class="sku-wrap">
+                <div class="sku" v-for="(s, k) in attributesFormData.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"
+                        @input="buildSkuPriceTable"></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"
+                        @input="buildSkuPriceTable"></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="sa-m-t-20" v-if="attributesFormData.sku_prices.length > 0">
+                <el-form-item label="批量设置" label-width="80px">
+                  <div class="sku sa-m-r-20" v-for="(item, index) in attributesFormData.skus" :key="index">
+                    <el-select v-model="item.batchId" placeholder="请选择规格" class="sa-w-150" clearable>
+                      <template v-for="(citem, cindex) in item.children">
+                        <el-option :key="cindex" :label="citem.name" :value="citem.temp_id"
+                          v-if="citem.temp_id && citem.name"></el-option>
+                      </template>
+                    </el-select>
+                  </div>
+                  <div class="warning-title" style="margin-left: 8px">
+                    未选择规格默认为全选批量设置
+                  </div>
+                </el-form-item>
+                <div class="sa-flex sa-flex-wrap">
+                  <el-select v-model="allEditObj.price" placeholder="请选择售价(৳)" class="sa-w-200 sa-m-r-10 sa-m-b-10"
+                    clearable>
+                    <el-option :value="300" label="300৳" />
+                    <el-option :value="500" label="500৳" />
+                    <el-option :value="1000" label="1000৳" />
+                    <el-option :value="2000" label="2000৳" />
+                    <el-option :value="3000" label="3000৳" />
+                  </el-select>
+                  <el-input v-model="allEditObj.stock" placeholder="请输入库存(件)" class="sa-w-200 sa-m-r-10 sa-m-b-10">
+                    <template #prepend>库存(件)</template>
+                  </el-input>
+                  <el-input v-model="allEditObj.stock_warning" placeholder="请输入库存预警值(件)"
+                    class="sa-w-200 sa-m-r-10 sa-m-b-10">
+                    <template #prepend>库存预警值(件)</template>
+                  </el-input>
+                  <el-button type="primary" @click="batchEdit" class="sa-m-b-10">批量设置</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 attributesFormData.skus" :key="i">
+                        <th v-if="item.children.length">{{ item.name }}</th>
+                      </template>
+                      <th>图片</th>
+                      <th><span class="required">*</span>销售价格(৳)</th>
+                      <th><span class="required">*</span>商品库存</th>
+                      <th>库存预警值</th>
+                      <th>SKU编码</th>
+                      <th>操作</th>
+                    </tr>
+                  </thead>
+                  <tbody>
+                    <tr v-for="(item, i) in attributesFormData.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-upload-image v-model="item.imageList" :max-count="1" :accept="['jpg', 'jpeg', 'png']"
+                          :max-size="5" :direct-upload="true" :size="30" :show-tip="false" :compact="true"
+                          placeholder="" />
+                      </td>
+                      <td>
+                        <el-select v-model="item.price" placeholder="选择价格" size="small"
+                          :class="{ 'is-error': !item.price || item.price <= 0 }">
+                          <el-option :value="300" label="300" />
+                          <el-option :value="500" label="500" />
+                          <el-option :value="1000" label="1000" />
+                          <el-option :value="2000" label="2000" />
+                          <el-option :value="3000" label="3000" />
+                        </el-select>
+                      </td>
+                      <td class="stock">
+                        <el-input v-model="item.stock" placeholder="请输入库存" size="small" type="number" :step="1" :min="0"
+                          :class="{ 'is-error': !item.stock || item.stock < 0 }"></el-input>
+                      </td>
+                      <td class="stock_warning">
+                        <el-input v-model="item.stock_warning" placeholder="请输入预警值" size="small" type="number" :step="1"
+                          :min="0"></el-input>
+                      </td>
+                      <td class="sn">
+                        <el-input v-model="item.sn" placeholder="请输入SKU编码" size="small"></el-input>
+                      </td>
+                      <td>
+                        <el-button type="danger" size="small" text @click="deleteSkuPrice(i)">删除</el-button>
+                      </td>
+                    </tr>
+                  </tbody>
+                </table>
+              </div>
+            </el-card>
+          </el-form>
+        </div>
+      </div>
+    </el-main>
+
+    <!-- 统一操作按钮 -->
+    <el-footer class="sa-footer--submit">
+      <el-button @click="closeDialog" size="large">关闭</el-button>
+
+      <!-- 基本信息Tab的按钮 -->
+      <template v-if="activeTab === 'basic'">
+        <el-button type="primary" @click="saveBasicInfo" :loading="savingStates.basic" size="large">
+          保存基本信息
+        </el-button>
+      </template>
+
+      <!-- 商品属性Tab的按钮 -->
+      <template v-else-if="activeTab === 'attributes'">
+        <el-button type="primary" @click="saveAttributes" :loading="savingStates.attributes" size="large">
+          保存商品属性
+        </el-button>
+      </template>
+
+      <!-- 保存全部按钮(始终显示) -->
+      <el-button type="primary" plain @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 { WarningFilled, CircleCheckFilled, CircleCloseFilled } from '@element-plus/icons-vue';
+import { api } from '../goods.service';
+const emit = defineEmits(['modalCallBack']);
+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);
+
+// 响应式数据
+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, // 规格 0单 1多,默认多规格
+  skus: [
+    {
+      id: 0,
+      temp_id: 1,
+      name: '',
+      batchId: '',
+      pid: 0,
+      children: [],
+    },
+  ],
+  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 () => {
+  savingStates.basic = true;
+  basicFormErrors.value = false;
+
+  const valid = await basicFormRef.value?.validate().catch(() => false);
+  if (!valid) {
+    basicFormErrors.value = true;
+    savingStates.basic = false;
+    return false;
+  }
+
+  const submitData = {
+    ...basicFormData,
+    image: convertImagesToString(basicFormData.image),
+    sliderImage: convertImagesToString(basicFormData.sliderImage),
+    flatPattern: convertImagesToString(basicFormData.flatPattern),
+  };
+
+  const { code, data } = isEdit.value
+    ? await api.goods.edit(goodsId.value, submitData)
+    : await api.goods.add(submitData);
+
+  if (code === '200') {
+    if (!isEdit.value) {
+      goodsId.value = data.id;
+      basicFormData.id = data.id;
+    }
+    basicSaved.value = true;
+    savingStates.basic = false;
+    return true;
+  }
+
+  basicFormErrors.value = true;
+  savingStates.basic = false;
+  return false;
+};
+
+// 保存商品属性(内部方法)
+const saveAttributesInternal = async () => {
+  try {
+    // 验证属性表单
+    const valid = await attributesFormRef.value?.validate().catch(() => false);
+    if (!valid) {
+      attributesFormErrors.value = true;
+      ElMessage.error('请完善商品属性');
+      return false;
+    }
+
+    // 验证SKU
+    if (!validateSku()) {
+      attributesFormErrors.value = true;
+      return false;
+    }
+
+    // 准备属性数据 - 转换为后端需要的格式
+    const submitData = {
+      goodsId: goodsId.value,
+      specType: attributesFormData.specType,
+      attrValue: generateAttrValueData(),
+      attr: generateAttrData(),
+    };
+
+    // 这里调用属性保存接口(待实现)
+    const { code, data } = await api.rule.add(submitData);
+    console.log(code, data);
+
+    // 临时模拟成功
+    attributesSaved.value = true;
+    ElMessage.success(t('message.goodsAttributeSaveSuccess'));
+    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(t('message.saveSuccess'));
+    emit('modalCallBack', { event: 'confirm' });
+  } catch (error) {
+    ElMessage.error(t('message.saveFailed') + ':' + error.message);
+  } finally {
+    savingStates.all = false;
+  }
+};
+
+// 关闭对话框
+const closeDialog = () => {
+  emit('modalCallBack', { event: 'close' });
+};
+
+// SKU相关方法
+const countId = ref(2);
+const childrenModal = [];
+const isResetSku = ref(0);
+
+// 批量操作相关变量
+const allEditObj = ref({
+  price: 0,
+  stock: 0,
+  stock_warning: 0,
+});
+
+// 添加主规格
+const addMainSku = () => {
+  attributesFormData.skus.push({
+    id: 0,
+    temp_id: countId.value++,
+    name: '',
+    batchId: '',
+    pid: 0,
+    children: [],
+  });
+  buildSkuPriceTable();
+};
+
+// 删除主规格
+const deleteMainSku = (k) => {
+  let data = attributesFormData.skus[k];
+
+  // 删除主规格
+  attributesFormData.skus.splice(k, 1);
+
+  // 如果当前删除的主规格存在子规格,则清空 skuPrice
+  if (data.children.length > 0) {
+    attributesFormData.sku_prices = [];
+    isResetSku.value = 1;
+  }
+  buildSkuPriceTable();
+};
+
+// 添加子规格
+const addChildrenSku = (k) => {
+  let isExist = false;
+  attributesFormData.skus[k].children.forEach((e) => {
+    if (e.name == childrenModal[k] && e.name != '') {
+      isExist = true;
+    }
+  });
+  if (isExist) {
+    ElMessage.warning('子规格已存在');
+    return false;
+  }
+
+  attributesFormData.skus[k].children.push({
+    id: 0,
+    temp_id: countId.value++,
+    name: childrenModal[k] || '',
+    pid: attributesFormData.skus[k].id,
+  });
+  childrenModal[k] = '';
+
+  // 如果是添加的第一个子规格,清空 skuPrice
+  if (attributesFormData.skus[k].children.length == 1) {
+    attributesFormData.sku_prices = [];
+    isResetSku.value = 1;
+  }
+  buildSkuPriceTable();
+};
+
+// 删除子规格
+const deleteChildrenSku = (k, i) => {
+  let data = attributesFormData.skus[k].children[i];
+  attributesFormData.skus[k].children.splice(i, 1);
+
+  // 查询 sku_prices 中包含被删除的子规格的项,然后移除
+  let deleteArr = [];
+  attributesFormData.sku_prices.forEach((item, index) => {
+    item.goods_sku_text.forEach((e) => {
+      if (e == data.name) {
+        deleteArr.push(index);
+      }
+    });
+  });
+  deleteArr.sort(function (a, b) {
+    return b - a;
+  });
+  // 移除有相关子规格的项
+  deleteArr.forEach((idx) => {
+    attributesFormData.sku_prices.splice(idx, 1);
+  });
+
+  // 当前规格项,所有子规格都被删除,清空 sku_prices
+  if (attributesFormData.skus[k].children.length <= 0) {
+    attributesFormData.sku_prices = [];
+    isResetSku.value = 1;
+  }
+  buildSkuPriceTable();
+};
+
+// 组成新的规格
+const buildSkuPriceTable = () => {
+  let arr = [];
+  // 遍历sku子规格生成新数组,然后执行递归笛卡尔积
+  attributesFormData.skus.forEach((s1) => {
+    let children = s1.children;
+    let childrenIdArray = [];
+    if (children.length > 0) {
+      children.forEach((s2) => {
+        childrenIdArray.push(s2.temp_id);
+      });
+      // 如果 children 子规格数量为 0,则不渲染当前规格
+      arr.push(childrenIdArray);
+    }
+  });
+  recursionSku(arr, 0, []);
+};
+
+// 递归找笛卡尔规格集合
+const recursionSku = (arr, k, temp) => {
+  if (k == arr.length && k != 0) {
+    let tempDetail = [];
+    let tempDetailIds = [];
+    temp.forEach((item) => {
+      for (let sku of attributesFormData.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 < attributesFormData.sku_prices.length; i++) {
+      if (
+        attributesFormData.sku_prices[i].goods_sku_temp_ids.join(',') == tempDetailIds.join(',')
+      ) {
+        flag = i;
+        break;
+      }
+    }
+
+    if (flag === false) {
+      attributesFormData.sku_prices.push({
+        id: 0,
+        temp_id: attributesFormData.sku_prices.length + 1,
+        goods_sku_ids: '',
+        goods_id: 0,
+        image: '',
+        imageList: [],
+        price: 0,
+        stock: 0,
+        stock_warning: 0,
+        sn: '',
+        goods_sku_text: tempDetail,
+        goods_sku_temp_ids: tempDetailIds,
+      });
+    } else {
+      attributesFormData.sku_prices[flag].goods_sku_text = tempDetail;
+      attributesFormData.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);
+    }
+  }
+};
+
+// 批量操作
+const batchEdit = () => {
+  const batchIds = attributesFormData.skus
+    .map((item) => item.batchId)
+    .filter((item) => Boolean(item));
+  attributesFormData.sku_prices.forEach((item) => {
+    if (
+      batchIds.length ? batchIds.every((citem) => item.goods_sku_temp_ids.includes(citem)) : true
+    ) {
+      const { price, stock, stock_warning } = allEditObj.value;
+      if (price) item.price = price;
+      if (stock) item.stock = stock;
+      if (stock_warning) item.stock_warning = stock_warning;
+    }
+  });
+
+  // 清空输入框
+  allEditObj.value = {
+    price: 0,
+    stock: 0,
+    stock_warning: 0,
+  };
+
+  // 清空选择的规格
+  attributesFormData.skus.forEach((item) => {
+    item.batchId = '';
+  });
+
+  ElMessage.success('批量设置成功');
+};
+
+// 删除规格组合
+const deleteSkuPrice = (index) => {
+  attributesFormData.sku_prices.splice(index, 1);
+  ElMessage.success(t('message.deleteSuccess'));
+};
+
+// SKU校验
+const validateSku = () => {
+  if (attributesFormData.sku_prices.length === 0) {
+    ElMessage.error('请先添加商品规格');
+    return false;
+  }
+
+  for (let i = 0; i < attributesFormData.sku_prices.length; i++) {
+    const item = attributesFormData.sku_prices[i];
+    if (!item.price || item.price <= 0) {
+      ElMessage.error(`第${i + 1}个规格的销售价格不能为空且必须大于0`);
+      return false;
+    }
+    if (item.stock === null || item.stock === undefined || item.stock < 0) {
+      ElMessage.error(`第${i + 1}个规格的商品库存不能为空且不能小于0`);
+      return false;
+    }
+  }
+  return true;
+};
+
+// 生成后端需要的 attrValue 数据格式
+const generateAttrValueData = () => {
+  return attributesFormData.sku_prices.map((item) => {
+    // 构建规格属性对象,如 {"颜色": "红色", "尺寸": "S"}
+    const attrObj = {};
+    const attrValueObj = {};
+
+    // 根据 goods_sku_text 和对应的规格名称构建属性对象
+    item.goods_sku_text.forEach((value, index) => {
+      const specName = attributesFormData.skus[index]?.name;
+      if (specName) {
+        attrObj[specName] = value;
+        attrValueObj[specName] = value;
+      }
+    });
+
+    return {
+      image: item.imageList && item.imageList.length > 0 ? item.imageList[0] : '',
+      price: item.price || '0',
+      stock: item.stock || 0,
+      barCode: item.sn || '',
+      stock_warning: item.stock_warning || 0,
+      attrValue: JSON.stringify(attrValueObj),
+      ...attrObj, // 展开规格属性,如 "颜色": "红色", "尺寸": "S"
+      id: 0,
+      productId: 0,
+    };
+  });
+};
+
+// 生成后端需要的 attr 数据格式
+const generateAttrData = () => {
+  return attributesFormData.skus
+    .filter((sku) => sku.name && sku.children.length > 0)
+    .map((sku) => ({
+      attrName: sku.name,
+      attrValues: sku.children.map((child) => child.name).join(','),
+    }));
+};
+
+// 获取分类数据
+const getCategoryData = async () => {
+  const { code, data } = await api.category.list({ size: 100 });
+  code === '200' &&
+    (categoryOptions.value = data.list.map((cat) => ({
+      label: cat.name,
+      value: cat.id,
+      id: cat.id,
+      name: cat.name,
+    })));
+};
+
+// 加载商品详情
+const loadGoodsDetail = async () => {
+  if (!goodsId.value) return;
+
+  const { code, data } = await api.goods.detail(goodsId.value);
+  if (code === '200') {
+    // 转换图片字段
+    data.image = convertStringToImages(data.image);
+    data.sliderImage = convertStringToImages(data.sliderImage);
+    data.flatPattern = convertStringToImages(data.flatPattern);
+
+    Object.assign(basicFormData, data);
+    basicSaved.value = true;
+  }
+};
+
+// 初始化
+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;
+  }
+}
+
+.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);
+}
+
+.spec-card {
+  .card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    font-weight: 500;
+  }
+}
+
+.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;
+  }
+}
+
+.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;
+    }
+
+    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;
+      }
+
+      &.sn {
+        min-width: 116px;
+      }
+    }
+  }
+}
+
+: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;
+}
+
+/* 必填项样式 */
+.required {
+  color: #f56c6c;
+  margin-right: 4px;
+}
+
+/* 错误状态样式 */
+.is-error .el-input__wrapper {
+  border-color: #f56c6c !important;
+  box-shadow: 0 0 0 1px #f56c6c inset !important;
+}
+
+.warning-title {
+  font-size: 12px;
+  color: #909399;
+}
+
+.th-center {
+  text-align: center;
+}
+</style>