diff --git a/src/api/mall/product/spu.ts b/src/api/mall/product/spu.ts index b6bec97e..5ddaa92e 100644 --- a/src/api/mall/product/spu.ts +++ b/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 }) diff --git a/src/api/mall/promotion/seckill/seckillActivity.ts b/src/api/mall/promotion/seckill/seckillActivity.ts index 2c59319c..fc2d1871 100644 --- a/src/api/mall/promotion/seckill/seckillActivity.ts +++ b/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[] // 重写类型 } // 查询秒杀活动列表 diff --git a/src/views/mall/product/spu/components/SkuList.vue b/src/views/mall/product/spu/components/SkuList.vue index 6aa26a2b..04718f3d 100644 --- a/src/views/mall/product/spu/components/SkuList.vue +++ b/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 diff --git a/src/views/mall/product/spu/components/index.ts b/src/views/mall/product/spu/components/index.ts index 549a9815..8c269e56 100644 --- a/src/views/mall/product/spu/components/index.ts +++ b/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 } diff --git a/src/views/mall/promotion/seckill/activity/SeckillActivityForm.vue b/src/views/mall/promotion/seckill/activity/SeckillActivityForm.vue index c47c475f..37cc836d 100644 --- a/src/views/mall/promotion/seckill/activity/SeckillActivityForm.vue +++ b/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')) diff --git a/src/views/mall/promotion/seckill/activity/components/SpuAndSkuList.vue b/src/views/mall/promotion/seckill/activity/components/SpuAndSkuList.vue new file mode 100644 index 00000000..fc7ed15a --- /dev/null +++ b/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> diff --git a/src/views/mall/promotion/seckill/activity/components/SpuAndSkuSelectForm.vue b/src/views/mall/promotion/seckill/activity/components/SpuAndSkuSelectForm.vue index cf538fc2..c3de9a2b 100644 --- a/src/views/mall/promotion/seckill/activity/components/SpuAndSkuSelectForm.vue +++ b/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!) diff --git a/src/views/mall/promotion/seckill/activity/components/index.ts b/src/views/mall/promotion/seckill/activity/components/index.ts index 69dd429a..ef92a41a 100644 --- a/src/views/mall/promotion/seckill/activity/components/index.ts +++ b/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 }