Browse Source

fix:完善秒杀活动管理①

puhui999 1 year ago
parent
commit
b1e7f14963

+ 10 - 0
src/api/mall/product/spu.ts

@@ -49,6 +49,16 @@ export interface Spu {
   recommendGood?: boolean // 是否优品
 }
 
+export interface SpuRespVO extends Spu {
+  price: number
+  salesCount: number
+  marketPrice: number
+  costPrice: number
+  stock: number
+  createTime: Date
+  status: number
+}
+
 // 获得 Spu 列表
 export const getSpuPage = (params: PageParam) => {
   return request.get({ url: '/product/spu/page', params })

+ 18 - 1
src/api/mall/promotion/seckill/seckillActivity.ts

@@ -1,8 +1,9 @@
 import request from '@/config/axios'
+import { Sku, SpuRespVO } from '@/api/mall/product/spu'
 
 export interface SeckillActivityVO {
   id: number
-  spuId: number
+  spuIds: number[]
   name: string
   status: number
   remark: string
@@ -17,6 +18,22 @@ export interface SeckillActivityVO {
   singleLimitCount: number
   stock: number
   totalStock: number
+  products: SeckillProductVO[]
+}
+
+export interface SeckillProductVO {
+  spuId: number
+  skuId: number
+  seckillPrice: number
+  stock: number
+}
+
+type SkuExtension = Sku & {
+  productConfig: SeckillProductVO
+}
+
+export interface SpuExtension extends SpuRespVO {
+  skus: SkuExtension[] // 重写类型
 }
 
 // 查询秒杀活动列表

+ 82 - 5
src/views/mall/product/spu/components/SkuList.vue

@@ -1,7 +1,7 @@
 <template>
   <!-- 情况一:添加/修改 -->
   <el-table
-    v-if="!isDetail"
+    v-if="!isDetail && !isActivityComponent"
     :data="isBatch ? skuList : formData!.skus"
     border
     class="tabNumWidth"
@@ -190,6 +190,66 @@
       </el-table-column>
     </template>
   </el-table>
+
+  <!-- 情况三:作为活动组件 -->
+  <el-table
+    v-if="isActivityComponent"
+    :data="formData!.skus"
+    border
+    max-height="500"
+    size="small"
+    style="width: 99%"
+  >
+    <el-table-column v-if="isComponent" type="selection" width="45" />
+    <el-table-column align="center" label="图片" min-width="80">
+      <template #default="{ row }">
+        <el-image :src="row.picUrl" class="w-60px h-60px" @click="imagePreview(row.picUrl)" />
+      </template>
+    </el-table-column>
+    <template v-if="formData!.specType">
+      <!--  根据商品属性动态添加 -->
+      <el-table-column
+        v-for="(item, index) in tableHeaders"
+        :key="index"
+        :label="item.label"
+        align="center"
+        min-width="80"
+      >
+        <template #default="{ row }">
+          <span style="font-weight: bold; color: #40aaff">
+            {{ row.properties[index]?.valueName }}
+          </span>
+        </template>
+      </el-table-column>
+    </template>
+    <el-table-column align="center" label="商品条码" min-width="100">
+      <template #default="{ row }">
+        {{ row.barCode }}
+      </template>
+    </el-table-column>
+    <el-table-column align="center" label="销售价(元)" min-width="80">
+      <template #default="{ row }">
+        {{ row.price }}
+      </template>
+    </el-table-column>
+    <el-table-column align="center" label="市场价(元)" min-width="80">
+      <template #default="{ row }">
+        {{ row.marketPrice }}
+      </template>
+    </el-table-column>
+    <el-table-column align="center" label="成本价(元)" min-width="80">
+      <template #default="{ row }">
+        {{ row.costPrice }}
+      </template>
+    </el-table-column>
+    <el-table-column align="center" label="库存" min-width="80">
+      <template #default="{ row }">
+        {{ row.stock }}
+      </template>
+    </el-table-column>
+    <!--  方便扩展每个活动配置的属性不一样  -->
+    <slot name="extension"></slot>
+  </el-table>
 </template>
 <script lang="ts" name="SkuList" setup>
 import { PropType, Ref } from 'vue'
@@ -198,6 +258,7 @@ import { propTypes } from '@/utils/propTypes'
 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'
 
 const props = defineProps({
   propFormData: {
@@ -208,9 +269,14 @@ const props = defineProps({
     type: Array,
     default: () => []
   },
+  ruleConfig: {
+    type: Array as PropType<RuleConfig[]>,
+    default: () => []
+  },
   isBatch: propTypes.bool.def(false), // 是否作为批量操作组件
   isDetail: propTypes.bool.def(false), // 是否作为 sku 详情组件
-  isComponent: propTypes.bool.def(false) // 是否作为 sku 选择组件
+  isComponent: propTypes.bool.def(false), // 是否作为 sku 选择组件
+  isActivityComponent: propTypes.bool.def(false) // 是否作为 sku 活动配置组件
 })
 const formData: Ref<Spu | undefined> = ref<Spu>() // 表单数据
 const skuList = ref<Sku[]>([
@@ -231,6 +297,7 @@ const skuList = ref<Sku[]>([
 /** 商品图预览 */
 const imagePreview = (imgUrl: string) => {
   createImageViewer({
+    zIndex: 9999999,
     urlList: [imgUrl]
   })
 }
@@ -258,9 +325,19 @@ const validateSku = (): boolean => {
   const checks = ['price', 'marketPrice', 'costPrice']
   let validate = true // 默认通过
   for (const sku of formData.value!.skus) {
-    if (checks.some((check) => sku[check] < 0.01)) {
-      validate = false // 只要有一个不通过则直接不通过
-      break
+    // 作为活动组件的校验
+    if (props.isActivityComponent) {
+      for (const rule of props.ruleConfig) {
+        if (sku[rule.name] < rule.geValue) {
+          validate = false // 只要有一个不通过则直接不通过
+          break
+        }
+      }
+    } else {
+      if (checks.some((check) => sku[check] < 0.01)) {
+        validate = false // 只要有一个不通过则直接不通过
+        break
+      }
     }
   }
   return validate

+ 43 - 1
src/views/mall/product/spu/components/index.ts

@@ -5,11 +5,53 @@ import ProductAttributes from './ProductAttributes.vue'
 import ProductPropertyAddForm from './ProductPropertyAddForm.vue'
 import SkuList from './SkuList.vue'
 
+import { Spu } from '@/api/mall/product/spu'
+
+interface Properties {
+  id: number
+  name: string
+  values?: Properties[]
+}
+
+interface RuleConfig {
+  name: string // 需要校验的字段
+  geValue: number // TODO 暂定大于一个数字
+}
+
+/**
+ *  商品通用函数
+ * @param spu
+ */
+const getPropertyList = (spu: Spu): Properties[] => {
+  //  直接拿返回的 skus 属性逆向生成出 propertyList
+  const properties: Properties[] = []
+  // 只有是多规格才处理
+  if (spu.specType) {
+    spu.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! })
+        }
+      })
+    })
+  }
+  return properties
+}
+
 export {
   BasicInfoForm,
   DescriptionForm,
   OtherSettingsForm,
   ProductAttributes,
   ProductPropertyAddForm,
-  SkuList
+  SkuList,
+  getPropertyList,
+  Properties,
+  RuleConfig
 }

+ 8 - 42
src/views/mall/promotion/seckill/activity/SeckillActivityForm.vue

@@ -9,36 +9,8 @@
     >
       <!-- 先选择 -->
       <template #spuId>
-        <el-button @click="spuAndSkuSelectForm.open('秒杀商品选择')">选择商品</el-button>
-        <el-table :data="spuList">
-          <el-table-column key="id" align="center" label="商品编号" prop="id" />
-          <el-table-column label="商品图" min-width="80">
-            <template #default="{ row }">
-              <el-image :src="row.picUrl" class="w-30px h-30px" @click="imagePreview(row.picUrl)" />
-            </template>
-          </el-table-column>
-          <el-table-column
-            :show-overflow-tooltip="true"
-            label="商品名称"
-            min-width="300"
-            prop="name"
-          />
-          <el-table-column align="center" label="商品售价" min-width="90" prop="price">
-            <template #default="{ row }">
-              {{ formatToFraction(row.price) }}
-            </template>
-          </el-table-column>
-          <el-table-column align="center" label="销量" min-width="90" prop="salesCount" />
-          <el-table-column align="center" label="库存" min-width="90" prop="stock" />
-          <el-table-column align="center" label="排序" min-width="70" prop="sort" />
-          <el-table-column
-            :formatter="dateFormatter"
-            align="center"
-            label="创建时间"
-            prop="createTime"
-            width="180"
-          />
-        </el-table>
+        <el-button @click="spuAndSkuSelectForm.open('秒杀商品选择')">添加商品</el-button>
+        <SpuAndSkuList ref="spuAndSkuListRef" :spu-list="spuList" />
       </template>
     </Form>
     <template #footer>
@@ -49,14 +21,11 @@
   <SpuAndSkuSelectForm ref="spuAndSkuSelectForm" @confirm="selectSpu" />
 </template>
 <script lang="ts" name="PromotionSeckillActivityForm" setup>
-import { SpuAndSkuSelectForm } from './components'
+import { SpuAndSkuList, SpuAndSkuSelectForm } 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 { dateFormatter } from '@/utils/formatTime'
-import { formatToFraction } from '@/utils'
-import { createImageViewer } from '@/components/ImageViewer'
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
@@ -67,12 +36,13 @@ const formLoading = ref(false) // 表单的加载中:1)修改时的数据加
 const formType = ref('') // 表单的类型:create - 新增;update - 修改
 const formRef = ref() // 表单 Ref
 const spuAndSkuSelectForm = ref() // 商品和属性选择 Ref
+const spuAndSkuListRef = ref() // sku 秒杀配置组件Ref
 /** 打开弹窗 */
 const open = async (type: string, id?: number) => {
   dialogVisible.value = true
   dialogTitle.value = t('action.' + type)
   formType.value = type
-  // 修改时,设置数据
+  // 修改时,设置数据 TODO 没测试估计有问题
   if (id) {
     formLoading.value = true
     try {
@@ -90,13 +60,7 @@ const selectSpu = (val: Spu) => {
   formRef.value.setValues({ spuId: val.id })
   spuList.value = [val]
 }
-/** 商品图预览 */
-const imagePreview = (imgUrl: string) => {
-  createImageViewer({
-    zIndex: 99999999,
-    urlList: [imgUrl]
-  })
-}
+
 /** 提交表单 */
 const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
 const submitForm = async () => {
@@ -108,6 +72,8 @@ const submitForm = async () => {
   formLoading.value = true
   try {
     const data = formRef.value.formModel as SeckillActivityApi.SeckillActivityVO
+    data.spuIds = spuList.value.map((spu) => spu.id!)
+    data.products = spuAndSkuListRef.value.getSkuConfigs()
     if (formType.value === 'create') {
       await SeckillActivityApi.createSeckillActivity(data)
       message.success(t('common.createSuccess'))

+ 157 - 0
src/views/mall/promotion/seckill/activity/components/SpuAndSkuList.vue

@@ -0,0 +1,157 @@
+<template>
+  <el-table :data="spuData">
+    <el-table-column type="expand" width="30">
+      <template #default="{ row }">
+        <SkuList
+          ref="skuListRef"
+          :is-activity-component="true"
+          :prop-form-data="spuPropertyList.find((item) => item.spuId === row.id)?.spuDetail"
+          :property-list="spuPropertyList.find((item) => item.spuId === row.id)?.propertyList"
+          :rule-config="ruleConfig"
+        >
+          <template #extension>
+            <el-table-column align="center" label="秒杀库存" min-width="168">
+              <template #default="{ row: sku }">
+                <el-input-number v-model="sku.productConfig.stock" :min="0" class="w-100%" />
+              </template>
+            </el-table-column>
+            <el-table-column align="center" label="秒杀价格(元)" min-width="168">
+              <template #default="{ row: sku }">
+                <el-input-number
+                  v-model="sku.productConfig.seckillPrice"
+                  :min="0"
+                  :precision="2"
+                  :step="0.1"
+                  class="w-100%"
+                />
+              </template>
+            </el-table-column>
+          </template>
+        </SkuList>
+      </template>
+    </el-table-column>
+    <el-table-column key="id" align="center" label="商品编号" prop="id" />
+    <el-table-column label="商品图" min-width="80">
+      <template #default="{ row }">
+        <el-image :src="row.picUrl" class="w-30px h-30px" @click="imagePreview(row.picUrl)" />
+      </template>
+    </el-table-column>
+    <el-table-column :show-overflow-tooltip="true" label="商品名称" min-width="300" prop="name" />
+    <el-table-column align="center" label="商品售价" min-width="90" prop="price">
+      <template #default="{ row }">
+        {{ formatToFraction(row.price) }}
+      </template>
+    </el-table-column>
+    <el-table-column align="center" label="销量" min-width="90" prop="salesCount" />
+    <el-table-column align="center" label="库存" min-width="90" prop="stock" />
+  </el-table>
+</template>
+<script lang="ts" name="SpuAndSkuList" setup>
+// TODO 后续计划重新封装作为活动商品配置通用组件
+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 props = defineProps({
+  spuList: {
+    type: Array,
+    default: () => []
+  }
+})
+const spuData = ref<SpuRespVO[]>([]) // spu 详情数据列表
+const skuListRef = ref() // 商品属性列表Ref
+interface spuProperty {
+  spuId: number
+  spuDetail: SpuExtension
+  propertyList: Properties[]
+}
+
+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 message = useMessage() // 消息弹窗
+/**
+ * 获取所有 sku 秒杀配置
+ */
+const getSkuConfigs = (): SeckillProductVO[] => {
+  if (!skuListRef.value.validateSku()) {
+    // TODO 作为通用组件是需要进一步完善
+    message.warning('请检查商品相关属性配置!!')
+    throw new Error('请检查商品相关属性配置!!')
+  }
+  const seckillProducts: SeckillProductVO[] = []
+  spuPropertyList.value.forEach((item) => {
+    item.spuDetail.skus.forEach((sku) => {
+      seckillProducts.push(sku.productConfig)
+    })
+  })
+  return seckillProducts
+}
+// 暴露出给表单提交时使用
+defineExpose({ getSkuConfigs })
+/** 商品图预览 */
+const imagePreview = (imgUrl: string) => {
+  createImageViewer({
+    zIndex: 99999999,
+    urlList: [imgUrl]
+  })
+}
+/**
+ * 将传进来的值赋值给 skuList
+ */
+watch(
+  () => props.spuList,
+  (data) => {
+    if (!data) return
+    spuData.value = data as SpuRespVO[]
+    getSpuDetails(spuData.value.map((spu) => spu.id!))
+  },
+  {
+    deep: true,
+    immediate: true
+  }
+)
+</script>

+ 7 - 25
src/views/mall/promotion/seckill/activity/components/SpuAndSkuSelectForm.vue

@@ -51,7 +51,7 @@
         :data="list"
         :expand-row-keys="expandRowKeys"
         row-key="id"
-        @expandChange="getPropertyList"
+        @expand-change="expandChange"
         @selection-change="selectSpu"
       >
         <el-table-column v-if="isSelectSku" type="expand" width="30">
@@ -111,7 +111,7 @@
 </template>
 
 <script lang="ts" name="SeckillActivitySpuAndSkuSelect" setup>
-import { SkuList } from '@/views/mall/product/spu/components'
+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'
@@ -142,13 +142,13 @@ const queryParams = ref({
   categoryId: null,
   createTime: []
 }) // 查询参数
-const propertyList = ref([]) // 商品属性列表
+const propertyList = ref<Properties[]>([]) // 商品属性列表
 const spuListRef = ref<InstanceType<typeof ElTable>>()
 const spuData = ref<ProductSpuApi.Spu | {}>() // 商品详情
 const isExpand = ref(false) // 控制 SKU 列表显示
 const expandRowKeys = ref<number[]>() // 控制展开行需要设置 row-key 属性才能使用,该属性为展开行的 keys 数组。
 // 计算商品属性
-const getPropertyList = async (row: ProductSpuApi.Spu, expandedRows: ProductSpuApi.Spu[]) => {
+const expandChange = async (row: ProductSpuApi.Spu, expandedRows: ProductSpuApi.Spu[]) => {
   spuData.value = {}
   propertyList.value = []
   isExpand.value = false
@@ -158,26 +158,8 @@ const getPropertyList = async (row: ProductSpuApi.Spu, expandedRows: ProductSpuA
     return
   }
   // 获取 SPU 详情
-  const res = (await ProductSpuApi.getSpu(row.id as number)) as ProductSpuApi.Spu
-  // 只有是多规格才处理
-  if (res.specType) {
-    //  直接拿返回的 skus 属性逆向生成出 propertyList
-    const properties = []
-    res.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
-  }
+  const res = (await ProductSpuApi.getSpu(row.id as number)) as ProductSpuApi.SpuRespVO
+  propertyList.value = getPropertyList(res)
   spuData.value = res
   isExpand.value = true
   expandRowKeys.value = [row.id!]
@@ -219,7 +201,7 @@ const confirm = () => {
     message.warning('没有选择任何商品属性')
     return
   }
-
+  // TODO 返回选择 sku 没测试过,后续测试完善
   props.isSelectSku
     ? emits('confirm', selectedSpu.value!, selectedSku.value!)
     : emits('confirm', selectedSpu.value!)

+ 2 - 1
src/views/mall/promotion/seckill/activity/components/index.ts

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