Pārlūkot izejas kodu

!176 mall seckillActivity
Merge pull request !176 from puhui999/dev-to-dev

芋道源码 1 gadu atpakaļ
vecāks
revīzija
a6fc5f775a

+ 7 - 11
src/api/mall/product/spu.ts

@@ -47,17 +47,13 @@ export interface Spu {
   recommendBest?: boolean // 是否精品
   recommendNew?: boolean // 是否新品
   recommendGood?: boolean // 是否优品
-}
-
-// TODO @puhui999: SpuRespVO 合并到 SPU 里?前端少点 VO 类哈;
-export interface SpuRespVO extends Spu {
-  price: number
-  salesCount: number
-  marketPrice: number
-  costPrice: number
-  stock: number
-  createTime: Date
-  status: number
+  price?: number // 商品价格
+  salesCount?: number // 商品销量
+  marketPrice?: number // 市场价
+  costPrice?: number // 成本价
+  stock?: number // 商品库存
+  createTime?: Date // 商品创建时间
+  status?: number // 商品状态
 }
 
 // 获得 Spu 列表

+ 4 - 2
src/api/mall/promotion/seckill/seckillActivity.ts

@@ -1,5 +1,5 @@
 import request from '@/config/axios'
-import { Sku, SpuRespVO } from '@/api/mall/product/spu'
+import { Sku, Spu } from '@/api/mall/product/spu'
 
 export interface SeckillActivityVO {
   id: number
@@ -21,6 +21,7 @@ export interface SeckillActivityVO {
   products: SeckillProductVO[]
 }
 
+// 秒杀活动所需属性
 export interface SeckillProductVO {
   spuId: number
   skuId: number
@@ -28,11 +29,12 @@ export interface SeckillProductVO {
   stock: number
 }
 
+// 扩展 Sku 配置
 type SkuExtension = Sku & {
   productConfig: SeckillProductVO
 }
 
-export interface SpuExtension extends SpuRespVO {
+export interface SpuExtension extends Spu {
   skus: SkuExtension[] // 重写类型
 }
 

+ 4 - 4
src/views/mall/product/spu/addForm.vue

@@ -55,9 +55,9 @@ const { delView } = useTagsViewStore() // 视图操作
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 const activeName = ref('basicInfo') // Tag 激活的窗口
 const isDetail = ref(false) // 是否查看详情
-const basicInfoRef = ref<ComponentRef<typeof BasicInfoForm>>() // 商品信息Ref
-const descriptionRef = ref<ComponentRef<typeof DescriptionForm>>() // 商品详情Ref
-const otherSettingsRef = ref<ComponentRef<typeof OtherSettingsForm>>() // 其他设置Ref
+const basicInfoRef = ref() // 商品信息Ref
+const descriptionRef = ref() // 商品详情Ref
+const otherSettingsRef = ref() // 其他设置Ref
 // spu 表单数据
 const formData = ref<ProductSpuApi.Spu>({
   name: '', // 商品名称
@@ -98,7 +98,7 @@ const formData = ref<ProductSpuApi.Spu>({
 
 /** 获得详情 */
 const getDetail = async () => {
-  if ('productSpuDetail' === name) {
+  if ('ProductSpuDetail' === name) {
     isDetail.value = true
   }
   const id = params.spuId as number

+ 4 - 26
src/views/mall/product/spu/components/BasicInfoForm.vue

@@ -176,7 +176,7 @@ import { checkSelectedNode, defaultProps, handleTree, treeToString } from '@/uti
 import { createImageViewer } from '@/components/ImageViewer'
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { UploadImg, UploadImgs } from '@/components/UploadFile'
-import { ProductAttributes, ProductPropertyAddForm, SkuList } from './index'
+import { getPropertyList, ProductAttributes, ProductPropertyAddForm, SkuList } from './index'
 import { basicInfoSchema } from './spu.data'
 import type { Spu } from '@/api/mall/product/spu'
 import * as ProductCategoryApi from '@/api/mall/product/category'
@@ -242,7 +242,7 @@ const rules = reactive({
   introduction: [required],
   picUrl: [required],
   sliderPicUrls: [required],
-  // deliveryTemplateId: [required],
+  deliveryTemplateId: [required],
   brandId: [required],
   specType: [required],
   subCommissionType: [required]
@@ -261,26 +261,7 @@ watch(
     formData.sliderPicUrls = data['sliderPicUrls']?.map((item) => ({
       url: item
     }))
-    // 只有是多规格才处理
-    if (!formData.specType) {
-      return
-    }
-    //  直接拿返回的 skus 属性逆向生成出 propertyList
-    const properties = []
-    formData.skus?.forEach((sku) => {
-      sku.properties?.forEach(({ propertyId, propertyName, valueId, valueName }) => {
-        // 添加属性
-        if (!properties?.some((item) => item.id === propertyId)) {
-          properties.push({ id: propertyId, name: propertyName, values: [] })
-        }
-        // 添加属性值
-        const index = properties?.findIndex((item) => item.id === propertyId)
-        if (!properties[index].values?.some((value) => value.id === valueId)) {
-          properties[index].values?.push({ id: valueId, name: valueName })
-        }
-      })
-    })
-    propertyList.value = properties
+    propertyList.value = getPropertyList(data)
   },
   {
     immediate: true
@@ -293,10 +274,7 @@ watch(
 const emit = defineEmits(['update:activeName'])
 const validate = async () => {
   // 校验 sku
-  if (!skuListRef.value.validateSku()) {
-    message.warning('商品相关价格不能低于 0.01 元!!')
-    throw new Error('商品相关价格不能低于 0.01 元!!')
-  }
+  skuListRef.value.validateSku()
   // 校验表单
   if (!productSpuBasicInfoRef) return
   return await unref(productSpuBasicInfoRef).validate((valid) => {

+ 34 - 11
src/views/mall/product/spu/components/SkuList.vue

@@ -259,8 +259,10 @@ import { UploadImg } from '@/components/UploadFile'
 import type { Property, Sku, Spu } from '@/api/mall/product/spu'
 import { createImageViewer } from '@/components/ImageViewer'
 import { RuleConfig } from '@/views/mall/product/spu/components/index'
+import { Properties } from './index'
 
 defineOptions({ name: 'SkuList' })
+const message = useMessage() // 消息弹窗
 
 const props = defineProps({
   propFormData: {
@@ -268,7 +270,7 @@ const props = defineProps({
     default: () => {}
   },
   propertyList: {
-    type: Array,
+    type: Array as PropType<Properties[]>,
     default: () => []
   },
   ruleConfig: {
@@ -323,26 +325,47 @@ const tableHeaders = ref<{ prop: string; label: string }[]>([]) // 多属性表
 /**
  * 保存时,每个商品规格的表单要校验下。例如说,销售金额最低是 0.01 这种。
  */
-const validateSku = (): boolean => {
+const validateSku = () => {
   const checks = ['price', 'marketPrice', 'costPrice']
+  let warningInfo = '请检查商品各行相关属性配置,'
   let validate = true // 默认通过
-  for (const sku of formData.value!.skus) {
+  for (const sku of formData.value!.skus!) {
     // 作为活动组件的校验
     if (props.isActivityComponent) {
       for (const rule of props.ruleConfig) {
-        if (sku[rule.name] < rule.geValue) {
+        const arg = getValue(sku, rule.name)
+        if (!rule.rule(arg)) {
           validate = false // 只要有一个不通过则直接不通过
+          warningInfo += rule.message
           break
         }
       }
     } else {
       if (checks.some((check) => sku[check] < 0.01)) {
         validate = false // 只要有一个不通过则直接不通过
+        warningInfo = '商品相关价格不能低于 0.01 元!!'
         break
       }
     }
+    // 只要有一个不通过则结束后续的校验
+    if (!validate) {
+      message.warning(warningInfo)
+      throw new Error(warningInfo)
+    }
+  }
+}
+const getValue = (obj, arg) => {
+  const keys = arg.split('.')
+  let value = obj
+  for (const key of keys) {
+    if (value && typeof value === 'object' && key in value) {
+      value = value[key]
+    } else {
+      value = undefined
+      break
+    }
   }
-  return validate
+  return value
 }
 
 const emit = defineEmits<{
@@ -417,13 +440,13 @@ const generateTableData = (propertyList: any[]) => {
  * 生成 skus 前置校验
  */
 const validateData = (propertyList: any[]) => {
-  const skuPropertyIds = []
+  const skuPropertyIds: number[] = []
   formData.value!.skus!.forEach((sku) =>
     sku.properties
       ?.map((property) => property.propertyId)
-      .forEach((propertyId) => {
-        if (skuPropertyIds.indexOf(propertyId) === -1) {
-          skuPropertyIds.push(propertyId)
+      ?.forEach((propertyId) => {
+        if (skuPropertyIds.indexOf(propertyId!) === -1) {
+          skuPropertyIds.push(propertyId!)
         }
       })
   )
@@ -457,7 +480,7 @@ const build = (propertyValuesList: Property[][]) => {
 /** 监听属性列表,生成相关参数和表头 */
 watch(
   () => props.propertyList,
-  (propertyList) => {
+  (propertyList: Properties[]) => {
     // 如果不是多规格则结束
     if (!formData.value!.specType) {
       return
@@ -497,7 +520,7 @@ watch(
       return
     }
     // 添加新属性没有属性值也不做处理
-    if (propertyList.some((item) => item.values.length === 0)) {
+    if (propertyList.some((item) => item.values!.length === 0)) {
       return
     }
     // 生成 table 数据,即 sku 列表

+ 13 - 2
src/views/mall/product/spu/components/index.ts

@@ -14,8 +14,19 @@ interface Properties {
 }
 
 interface RuleConfig {
-  name: string // 需要校验的字段
-  geValue: number // TODO 暂定大于一个数字
+  // 需要校验的字段
+  // 例:name: 'name' 则表示校验 sku.name 的值
+  // 例:name: 'productConfig.stock' 则表示校验 sku.productConfig.name 的值,此处 productConfig 表示我在 Sku 上扩展的属性
+  name: string
+  // 校验规格为一个毁掉函数,其中 arg 为需要校验的字段的值。
+  // 例:需要校验价格必须大于0.01
+  // {
+  //  name:'price',
+  //  rule:(arg) => arg > 0.01
+  // }
+  rule: (arg: any) => boolean
+  // 校验不通过时的消息提示
+  message: string
 }
 
 /**

+ 37 - 72
src/views/mall/promotion/seckill/activity/components/SpuAndSkuList.vue → src/views/mall/promotion/components/SpuAndSkuList.vue

@@ -1,5 +1,5 @@
 <template>
-  <el-table :data="spuData">
+  <el-table :data="spuData" :default-expand-all="true">
     <el-table-column type="expand" width="30">
       <template #default="{ row }">
         <SkuList
@@ -46,87 +46,39 @@
     <el-table-column align="center" label="库存" min-width="90" prop="stock" />
   </el-table>
 </template>
-<script lang="ts" name="SpuAndSkuList" setup>
+<script generic="T extends Spu" lang="ts" setup>
 // TODO 后续计划重新封装作为活动商品配置通用组件;可以等其他活动做到的时候,在统一处理 SPU 选择组件哈
 import { formatToFraction } from '@/utils'
 import { createImageViewer } from '@/components/ImageViewer'
-import * as ProductSpuApi from '@/api/mall/product/spu'
-import { SpuRespVO } from '@/api/mall/product/spu'
-import {
-  getPropertyList,
-  Properties,
-  RuleConfig,
-  SkuList
-} from '@/views/mall/product/spu/components'
-import { SeckillProductVO, SpuExtension } from '@/api/mall/promotion/seckill/seckillActivity'
-const message = useMessage() // 消息弹窗
+import { Spu } from '@/api/mall/product/spu'
+import { RuleConfig, SkuList } from '@/views/mall/product/spu/components'
+import { SeckillProductVO } from '@/api/mall/promotion/seckill/seckillActivity'
+import { SpuProperty } from '@/views/mall/promotion/components/index'
 
-// TODO @puhui999:是不是改成传递一个 spu 就好啦?
-const props = defineProps({
-  spuList: {
-    type: Array,
-    default: () => []
-  }
-})
-const spuData = ref<SpuRespVO[]>([]) // spu 详情数据列表
+defineOptions({ name: 'PromotionSpuAndSkuList' })
+
+// TODO @puhui999:是不是改成传递一个 spu 就好啦? 因为活动商品可以多选所以展示编辑的时候需要展示多个
+const props = defineProps<{
+  spuList: T[]
+  ruleConfig: RuleConfig[]
+  spuPropertyListP: SpuProperty<T>[]
+}>()
+
+const spuData = ref<Spu[]>([]) // spu 详情数据列表
 const skuListRef = ref() // 商品属性列表Ref
-interface spuProperty {
-  spuId: number
-  spuDetail: SpuExtension
-  propertyList: Properties[]
-} // TODO @puhui999:类名首字母大写哈
 
-const spuPropertyList = ref<spuProperty[]>([]) // spuId 对应的 sku 的属性列表
-/**
- * 获取 SPU 详情
- * @param spuIds
- */
-const getSpuDetails = async (spuIds: number[]) => {
-  const spuProperties: spuProperty[] = []
-  // TODO puhui999: 考虑后端添加通过 spuIds 批量获取
-  for (const spuId of spuIds) {
-    // 获取 SPU 详情
-    const res = (await ProductSpuApi.getSpu(spuId)) as SpuExtension
-    if (!res) {
-      continue
-    }
-    // 初始化每个 sku 秒杀配置
-    res.skus?.forEach((sku) => {
-      const config: SeckillProductVO = {
-        spuId,
-        skuId: sku.id!,
-        stock: 0,
-        seckillPrice: 0
-      }
-      sku.productConfig = config
-    })
-    spuProperties.push({ spuId, spuDetail: res, propertyList: getPropertyList(res) })
-  }
-  spuPropertyList.value = spuProperties
-}
-const ruleConfig: RuleConfig[] = [
-  {
-    name: 'stock',
-    geValue: 10
-  },
-  {
-    name: 'seckillPrice',
-    geValue: 0.01
-  }
-]
+const spuPropertyList = ref<SpuProperty<T>[]>([]) // spuId 对应的 sku 的属性列表
+
 /**
  * 获取所有 sku 秒杀配置
+ * @param extendedAttribute 在 sku 上扩展的属性,例:秒杀活动 sku 扩展属性 productConfig 请参考 seckillActivity.ts
  */
-const getSkuConfigs = (): SeckillProductVO[] => {
-  if (!skuListRef.value.validateSku()) {
-    // TODO 作为通用组件是需要进一步完善
-    message.warning('请检查商品相关属性配置!!')
-    throw new Error('请检查商品相关属性配置!!')
-  }
+const getSkuConfigs: <V>(extendedAttribute: string) => V[] = (extendedAttribute: string) => {
+  skuListRef.value.validateSku()
   const seckillProducts: SeckillProductVO[] = []
   spuPropertyList.value.forEach((item) => {
     item.spuDetail.skus.forEach((sku) => {
-      seckillProducts.push(sku.productConfig)
+      seckillProducts.push(sku[extendedAttribute])
     })
   })
   return seckillProducts
@@ -149,8 +101,21 @@ watch(
   () => props.spuList,
   (data) => {
     if (!data) return
-    spuData.value = data as SpuRespVO[]
-    getSpuDetails(spuData.value.map((spu) => spu.id!))
+    spuData.value = data as Spu[]
+  },
+  {
+    deep: true,
+    immediate: true
+  }
+)
+/**
+ * 将传进来的值赋值给 skuList
+ */
+watch(
+  () => props.spuPropertyListP,
+  (data) => {
+    if (!data) return
+    spuPropertyList.value = data as SpuProperty<T>[]
   },
   {
     deep: true,

+ 27 - 38
src/views/mall/promotion/seckill/activity/components/SpuAndSkuSelectForm.vue → src/views/mall/promotion/components/SpuSelect.vue

@@ -20,7 +20,6 @@
             class="w-1/1"
             node-key="id"
             placeholder="请选择商品分类"
-            @change="nodeClick"
           />
         </el-col>
         <el-col :span="6">
@@ -110,18 +109,20 @@
   </Dialog>
 </template>
 
-<script lang="ts" name="SeckillActivitySpuAndSkuSelect" setup>
+<script lang="ts" setup>
 import { getPropertyList, Properties, SkuList } from '@/views/mall/product/spu/components'
 import { ElTable } from 'element-plus'
 import { dateFormatter } from '@/utils/formatTime'
 import { createImageViewer } from '@/components/ImageViewer'
 import { formatToFraction } from '@/utils'
-import { checkSelectedNode, defaultProps, handleTree } from '@/utils/tree'
+import { defaultProps, handleTree } from '@/utils/tree'
 
 import * as ProductCategoryApi from '@/api/mall/product/category'
 import * as ProductSpuApi from '@/api/mall/product/spu'
 import { propTypes } from '@/utils/propTypes'
 
+defineOptions({ name: 'PromotionSpuSelect' })
+
 const props = defineProps({
   // 默认不需要(不需要的情况下只返回 spu,需要的情况下返回 选中的 spu 和 sku 列表)
   // 其它活动需要选择商品和商品属性导入此组件即可,需添加组件属性 :isSelectSku='true'
@@ -159,7 +160,7 @@ const expandChange = async (row: ProductSpuApi.Spu, expandedRows: ProductSpuApi.
     return
   }
   // 获取 SPU 详情
-  const res = (await ProductSpuApi.getSpu(row.id as number)) as ProductSpuApi.SpuRespVO
+  const res = (await ProductSpuApi.getSpu(row.id as number)) as ProductSpuApi.Spu
   propertyList.value = getPropertyList(res)
   spuData.value = res
   isExpand.value = true
@@ -167,53 +168,50 @@ const expandChange = async (row: ProductSpuApi.Spu, expandedRows: ProductSpuApi.
 }
 
 //============ 商品选择相关 ============
-const selectedSpu = ref<ProductSpuApi.Spu>() // 选中的商品 spu 只能选择一个
-const selectedSku = ref<ProductSpuApi.Sku[]>() // 选中的商品 sku
+const selectedSpuIds = ref<number[]>([]) // 选中的商品 spuIds
+const selectedSkuIds = ref<number[]>([]) // 选中的商品 skuIds
 const selectSku = (val: ProductSpuApi.Sku[]) => {
-  selectedSku.value = val
+  selectedSkuIds.value = val.map((sku) => sku.id!)
 }
 const selectSpu = (val: ProductSpuApi.Spu[]) => {
-  // 只选择一个
-  selectedSpu.value = val[0]
-  // 如果大于1个
-  if (val.length > 1) {
-    // 清空选择
-    spuListRef.value.clearSelection()
-    // 变更为最后一次选择的
-    spuListRef.value.toggleRowSelection(val.pop(), true)
-  }
+  selectedSpuIds.value = val.map((spu) => spu.id!)
+  // // 只选择一个
+  // selectedSpu.value = val[0]
+  // // 如果大于1个
+  // if (val.length > 1) {
+  //   // 清空选择
+  //   spuListRef.value.clearSelection()
+  //   // 变更为最后一次选择的
+  //   spuListRef.value.toggleRowSelection(val.pop(), true)
+  // }
 }
 // 确认选择时的触发事件
 const emits = defineEmits<{
-  (e: 'confirm', value: ProductSpuApi.Spu, value1?: ProductSpuApi.Sku[]): void
+  (e: 'confirm', spuIds: number[], skuIds?: number[]): void
 }>()
 /**
  * 确认选择返回选中的 spu 和 sku (如果需要选择sku的话)
  */
 const confirm = () => {
-  if (typeof selectedSpu.value === 'undefined') {
+  if (selectedSpuIds.value.length === 0) {
     message.warning('没有选择任何商品')
     return
   }
-  if (
-    (props.isSelectSku && typeof selectedSku.value === 'undefined') ||
-    selectedSku.value?.length === 0
-  ) {
+  if (props.isSelectSku && selectedSkuIds.value.length === 0) {
     message.warning('没有选择任何商品属性')
     return
   }
-  // TODO 返回选择 sku 没测试过,后续测试完善
+  // 返回各自 id 列表
   props.isSelectSku
-    ? emits('confirm', selectedSpu.value!, selectedSku.value!)
-    : emits('confirm', selectedSpu.value!)
+    ? emits('confirm', selectedSpuIds.value, selectedSkuIds.value)
+    : emits('confirm', selectedSpuIds.value)
   // 关闭弹窗
   dialogVisible.value = false
 }
 
-// TODO @puhui999:直接叫商品选择;不用外部传入标题;
-/** 打开弹窗 TODO 没做国际化 */
-const open = (title: string) => {
-  dialogTitle.value = title
+/** 打开弹窗 */
+const open = () => {
+  dialogTitle.value = '商品选择'
   dialogVisible.value = true
 }
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
@@ -259,15 +257,6 @@ const imagePreview = (imgUrl: string) => {
 const categoryList = ref() // 分类树
 
 // TODO @puhui999:商品搜索的时候,可以通过一级搜二级;所以这个校验可以去掉哈;也就是说,只允许挂在二级,但是一级可搜索到
-/**
- * 校验所选是否为二级及以下节点
- */
-const nodeClick = () => {
-  if (!checkSelectedNode(categoryList.value, queryParams.value.categoryId)) {
-    queryParams.value.categoryId = null
-    message.warning('必须选择二级及以下节点!!')
-  }
-}
 /** 初始化 **/
 onMounted(async () => {
   await getList()

+ 13 - 0
src/views/mall/promotion/components/index.ts

@@ -0,0 +1,13 @@
+import SpuSelect from './SpuSelect.vue'
+import SpuAndSkuList from './SpuAndSkuList.vue'
+import { Properties } from '@/views/mall/product/spu/components'
+
+type SpuProperty<T> = {
+  spuId: number
+  spuDetail: T
+  propertyList: Properties[]
+}
+/**
+ * 提供商品活动商品选择通用组件
+ */
+export { SpuSelect, SpuAndSkuList, SpuProperty }

+ 71 - 16
src/views/mall/promotion/seckill/activity/SeckillActivityForm.vue

@@ -8,10 +8,14 @@
       :schema="allSchemas.formSchema"
     >
       <!-- 先选择 -->
-      <template #spuId>
-        <el-button @click="spuAndSkuSelectForm.open('秒杀商品选择')">选择商品</el-button>
-        <!-- TODO @puhui999:默认展开 SKU 哈,毕竟 SKU 是主角,SPU 是配角 -->
-        <SpuAndSkuList ref="spuAndSkuListRef" :spu-list="spuList" />
+      <template #spuIds>
+        <el-button @click="spuSelectRef.open()">选择商品</el-button>
+        <SpuAndSkuList
+          ref="spuAndSkuListRef"
+          :rule-config="ruleConfig"
+          :spu-list="spuList"
+          :spu-property-list-p="spuPropertyList"
+        />
       </template>
     </Form>
     <template #footer>
@@ -19,15 +23,17 @@
       <el-button @click="dialogVisible = false">取 消</el-button>
     </template>
   </Dialog>
-  <!-- TODO @puhui999:这个组件是不是 SpuSelect,不需要带 sku 或者 Form 呀 -->
-  <SpuAndSkuSelectForm ref="spuAndSkuSelectForm" @confirm="selectSpu" />
+  <SpuSelect ref="spuSelectRef" @confirm="selectSpu" />
 </template>
-<script lang="ts" name="PromotionSeckillActivityForm" setup>
-import { SpuAndSkuList, SpuAndSkuSelectForm } from './components'
+<script lang="ts" setup>
+import { SpuAndSkuList, SpuProperty, SpuSelect } from '../../components'
 import { allSchemas, rules } from './seckillActivity.data'
-import { Spu } from '@/api/mall/product/spu'
 
 import * as SeckillActivityApi from '@/api/mall/promotion/seckill/seckillActivity'
+import { getPropertyList, RuleConfig } from '@/views/mall/product/spu/components'
+import * as ProductSpuApi from '@/api/mall/product/spu'
+
+defineOptions({ name: 'PromotionSeckillActivityForm' })
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
@@ -37,14 +43,26 @@ const dialogTitle = ref('') // 弹窗的标题
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 const formType = ref('') // 表单的类型:create - 新增;update - 修改
 const formRef = ref() // 表单 Ref
-const spuAndSkuSelectForm = ref() // 商品和属性选择 Ref
+const spuSelectRef = ref() // 商品和属性选择 Ref
 const spuAndSkuListRef = ref() // sku 秒杀配置组件Ref
-
+const ruleConfig: RuleConfig[] = [
+  {
+    name: 'productConfig.stock',
+    rule: (arg) => arg > 1,
+    message: '商品秒杀库存必须大于 1 !!!'
+  },
+  {
+    name: 'productConfig.seckillPrice',
+    rule: (arg) => arg > 0.01,
+    message: '商品秒杀价格必须大于 0.01 !!!'
+  }
+]
 /** 打开弹窗 */
 const open = async (type: string, id?: number) => {
   dialogVisible.value = true
   dialogTitle.value = t('action.' + type)
   formType.value = type
+  resetForm()
   // 修改时,设置数据 TODO 没测试估计有问题
   if (id) {
     formLoading.value = true
@@ -58,12 +76,49 @@ const open = async (type: string, id?: number) => {
 }
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 
-const spuList = ref<Spu[]>([]) // 选择的 spu
-const selectSpu = (val: Spu) => {
-  formRef.value.setValues({ spuId: val.id })
-  spuList.value = [val]
+const spuList = ref<SeckillActivityApi.SpuExtension[]>([]) // 选择的 spu
+const spuPropertyList = ref<SpuProperty<SeckillActivityApi.SpuExtension>[]>([])
+const selectSpu = (spuIds: number[]) => {
+  formRef.value.setValues({ spuIds })
+  getSpuDetails(spuIds)
+}
+/**
+ * 获取 SPU 详情
+ * TODO 获取 SPU 详情,放到各自活动表单来做,让 SpuAndSkuList 职责单一点
+ * @param spuIds
+ */
+const getSpuDetails = async (spuIds: number[]) => {
+  const spuProperties: SpuProperty<SeckillActivityApi.SpuExtension>[] = []
+  spuList.value = []
+  // TODO puhui999: 考虑后端添加通过 spuIds 批量获取
+  for (const spuId of spuIds) {
+    // 获取 SPU 详情
+    const res = (await ProductSpuApi.getSpu(spuId)) as SeckillActivityApi.SpuExtension
+    if (!res) {
+      continue
+    }
+    spuList.value.push(res)
+    // 初始化每个 sku 秒杀配置
+    res.skus?.forEach((sku) => {
+      const config: SeckillActivityApi.SeckillProductVO = {
+        spuId,
+        skuId: sku.id!,
+        stock: 0,
+        seckillPrice: 0
+      }
+      sku.productConfig = config
+    })
+    spuProperties.push({ spuId, spuDetail: res, propertyList: getPropertyList(res) })
+  }
+  spuPropertyList.value = spuProperties
 }
 
+/** 重置表单 */
+const resetForm = () => {
+  spuList.value = []
+  spuPropertyList.value = []
+  formRef.value.getElFormRef().resetFields()
+}
 /** 提交表单 */
 const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
 const submitForm = async () => {
@@ -76,7 +131,7 @@ const submitForm = async () => {
   try {
     const data = formRef.value.formModel as SeckillActivityApi.SeckillActivityVO
     data.spuIds = spuList.value.map((spu) => spu.id!)
-    data.products = spuAndSkuListRef.value.getSkuConfigs()
+    data.products = spuAndSkuListRef.value.getSkuConfigs('productConfig')
     if (formType.value === 'create') {
       await SeckillActivityApi.createSeckillActivity(data)
       message.success(t('common.createSuccess'))

+ 0 - 4
src/views/mall/promotion/seckill/activity/components/index.ts

@@ -1,4 +0,0 @@
-import SpuAndSkuSelectForm from './SpuAndSkuSelectForm.vue'
-import SpuAndSkuList from './SpuAndSkuList.vue'
-
-export { SpuAndSkuSelectForm, SpuAndSkuList }

+ 26 - 3
src/views/mall/promotion/seckill/activity/index.vue

@@ -24,11 +24,19 @@
       v-model:pageSize="tableObject.pageSize"
       :columns="allSchemas.tableColumns"
       :data="tableObject.tableList"
+      :expand="true"
       :loading="tableObject.loading"
       :pagination="{
         total: tableObject.total
       }"
+      @expand-change="expandChange"
     >
+      <template #expand> 展示活动商品和商品相关属性活动配置</template>
+      <template #configIds="{ row }">
+        <el-tag v-for="(name, index) in convertSeckillConfigNames(row)" :key="index" class="mr-5px">
+          {{ name }}
+        </el-tag>
+      </template>
       <template #action="{ row }">
         <el-button
           v-hasPermi="['promotion:seckill-activity:update']"
@@ -53,11 +61,14 @@
   <!-- 表单弹窗:添加/修改 -->
   <SeckillActivityForm ref="formRef" @success="getList" />
 </template>
-<script lang="ts" name="PromotionSeckillActivity" setup>
+<script lang="ts" setup>
 import { allSchemas } from './seckillActivity.data'
+import { getListAllSimple } from '@/api/mall/promotion/seckill/seckillConfig'
 import * as SeckillActivityApi from '@/api/mall/promotion/seckill/seckillActivity'
 import SeckillActivityForm from './SeckillActivityForm.vue'
 
+defineOptions({ name: 'PromotionSeckillActivity' })
+
 // tableObject:表格的属性对象,可获得分页大小、条数等属性
 // tableMethods:表格的操作对象,可进行获得分页、删除记录等操作
 // 详细可见:https://doc.iocoder.cn/vue3/crud-schema/
@@ -78,9 +89,21 @@ const openForm = (type: string, id?: number) => {
 const handleDelete = (id: number) => {
   tableMethods.delList(id, false)
 }
+const seckillConfigAllSimple = ref([]) // 时段配置精简列表
+const convertSeckillConfigNames = computed(
+  () => (row) =>
+    seckillConfigAllSimple.value
+      ?.filter((item) => row.configIds.includes(item.id))
+      ?.map((config) => config.name)
+)
+const expandChange = (row, expandedRows) => {
+  // TODO puhui:等 CRUD 完事后弄
+  console.log(row, expandedRows)
+}
 
 /** 初始化 **/
-onMounted(() => {
-  getList()
+onMounted(async () => {
+  await getList()
+  seckillConfigAllSimple.value = await getListAllSimple()
 })
 </script>

+ 20 - 20
src/views/mall/promotion/seckill/activity/seckillActivity.data.ts

@@ -16,7 +16,6 @@ export const rules = reactive({
 })
 
 // CrudSchema https://doc.iocoder.cn/vue3/crud-schema/
-// TODO @puhui999:table 宽度调整下,有点太长啦;部分字段可以隐藏哈,根据需求;
 const crudSchemas = reactive<CrudSchema[]>([
   {
     label: '秒杀活动名称',
@@ -40,8 +39,7 @@ const crudSchemas = reactive<CrudSchema[]>([
       component: 'DatePicker',
       componentProps: {
         valueFormat: 'YYYY-MM-DD',
-        type: 'daterange',
-        defaultTime: [new Date('1 00:00:00'), new Date('1 23:59:59')]
+        type: 'daterange'
       }
     },
     form: {
@@ -52,7 +50,7 @@ const crudSchemas = reactive<CrudSchema[]>([
       }
     },
     table: {
-      width: 300
+      width: 120
     }
   },
   {
@@ -64,8 +62,7 @@ const crudSchemas = reactive<CrudSchema[]>([
       component: 'DatePicker',
       componentProps: {
         valueFormat: 'YYYY-MM-DD',
-        type: 'daterange',
-        defaultTime: [new Date('1 00:00:00'), new Date('1 23:59:59')]
+        type: 'daterange'
       }
     },
     form: {
@@ -76,11 +73,11 @@ const crudSchemas = reactive<CrudSchema[]>([
       }
     },
     table: {
-      width: 300
+      width: 120
     }
   },
   {
-    label: '秒杀时段', // todo @PUHUI999: 在列表界面,格式化不对
+    label: '秒杀时段',
     field: 'configIds',
     form: {
       component: 'Select',
@@ -106,7 +103,7 @@ const crudSchemas = reactive<CrudSchema[]>([
       value: 0
     },
     table: {
-      width: 300
+      width: 120
     }
   },
   {
@@ -118,7 +115,7 @@ const crudSchemas = reactive<CrudSchema[]>([
       value: 0
     },
     table: {
-      width: 300
+      width: 120
     }
   },
   {
@@ -130,7 +127,7 @@ const crudSchemas = reactive<CrudSchema[]>([
       value: 0
     },
     table: {
-      width: 300
+      width: 120
     }
   },
   {
@@ -141,7 +138,7 @@ const crudSchemas = reactive<CrudSchema[]>([
       value: 0
     },
     table: {
-      width: 300
+      width: 120
     }
   },
   {
@@ -152,7 +149,7 @@ const crudSchemas = reactive<CrudSchema[]>([
       value: 0
     },
     table: {
-      width: 300
+      width: 120
     }
   },
   {
@@ -164,7 +161,7 @@ const crudSchemas = reactive<CrudSchema[]>([
       value: 0
     },
     table: {
-      width: 300
+      width: 120
     }
   },
   {
@@ -175,12 +172,14 @@ const crudSchemas = reactive<CrudSchema[]>([
       value: 0
     },
     table: {
-      width: 300
+      width: 120
     }
   },
   {
-    label: '秒杀活动商品', // TODO @puhui999:格式化的商品不对;
-    field: 'spuId',
+    label: '秒杀活动商品',
+    field: 'spuIds',
+    isTable: false,
+    isSearch: false,
     form: {
       colProps: {
         span: 24
@@ -204,7 +203,7 @@ const crudSchemas = reactive<CrudSchema[]>([
     },
     isForm: false,
     table: {
-      width: 300
+      width: 120
     }
   },
   {
@@ -215,12 +214,12 @@ const crudSchemas = reactive<CrudSchema[]>([
       value: 0
     },
     table: {
-      width: 300
+      width: 80
     }
   },
   {
     label: '状态',
-    field: 'status', // TODO @puhui999:状态在 table 格式化不对;
+    field: 'status', // TODO @puhui999:状态在 table 格式化不对;建表插入的数据状态值不对,改为 0 或 1 就好了
     dictType: DICT_TYPE.COMMON_STATUS,
     dictClass: 'number',
     isForm: false,
@@ -235,6 +234,7 @@ const crudSchemas = reactive<CrudSchema[]>([
   {
     label: '备注',
     field: 'remark',
+    isSearch: false,
     form: {
       component: 'Input',
       componentProps: {