Skip to content

Commit d5f1e50

Browse files
committed
【功能优化】商城:秒杀装修重构
1 parent 3c34602 commit d5f1e50

File tree

6 files changed

+771
-116
lines changed

6 files changed

+771
-116
lines changed

src/api/mall/promotion/seckill/seckillActivity.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export interface SeckillActivityVO {
1818
singleLimitCount?: number
1919
stock?: number
2020
totalStock?: number
21+
seckillPrice?: number
2122
products?: SeckillProductVO[]
2223
}
2324

@@ -43,6 +44,11 @@ export const getSeckillActivityPage = async (params) => {
4344
return await request.get({ url: '/promotion/seckill-activity/page', params })
4445
}
4546

47+
// 查询拼团活动列表,基于活动编号数组
48+
export const getSeckillActivityListByIds = (ids: number[]) => {
49+
return request.get({ url: `/promotion/seckill-activity/list-by-ids?ids=${ids}` })
50+
}
51+
4652
// 查询秒杀活动详情
4753
export const getSeckillActivity = async (id: number) => {
4854
return await request.get({ url: '/promotion/seckill-activity/get?id=' + id })

src/components/DiyEditor/components/mobile/PromotionSeckill/config.ts

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,21 @@ import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
33
/** 秒杀属性 */
44
export interface PromotionSeckillProperty {
55
// 布局类型:单列 | 三列
6-
layoutType: 'oneCol' | 'threeCol'
6+
layoutType: 'oneColBigImg' | 'oneColSmallImg' | 'twoCol'
77
// 商品字段
88
fields: {
99
// 商品名称
1010
name: PromotionSeckillFieldProperty
11+
// 商品简介
12+
introduction: PromotionSeckillFieldProperty
1113
// 商品价格
1214
price: PromotionSeckillFieldProperty
15+
// 市场价
16+
marketPrice: PromotionSeckillFieldProperty
17+
// 商品销量
18+
salesCount: PromotionSeckillFieldProperty
19+
// 商品库存
20+
stock: PromotionSeckillFieldProperty
1321
}
1422
// 角标
1523
badge: {
@@ -18,17 +26,31 @@ export interface PromotionSeckillProperty {
1826
// 角标图片
1927
imgUrl: string
2028
}
29+
// 按钮
30+
btnBuy: {
31+
// 类型:文字 | 图片
32+
type: 'text' | 'img'
33+
// 文字
34+
text: string
35+
// 文字按钮:背景渐变起始颜色
36+
bgBeginColor: string
37+
// 文字按钮:背景渐变结束颜色
38+
bgEndColor: string
39+
// 图片按钮:图片地址
40+
imgUrl: string
41+
}
2142
// 上圆角
2243
borderRadiusTop: number
2344
// 下圆角
2445
borderRadiusBottom: number
2546
// 间距
2647
space: number
2748
// 秒杀活动编号
28-
activityId: number
49+
activityIds: number[]
2950
// 组件样式
3051
style: ComponentStyle
3152
}
53+
3254
// 商品字段
3355
export interface PromotionSeckillFieldProperty {
3456
// 是否显示
@@ -43,13 +65,23 @@ export const component = {
4365
name: '秒杀',
4466
icon: 'mdi:calendar-time',
4567
property: {
46-
activityId: undefined,
47-
layoutType: 'oneCol',
68+
layoutType: 'oneColBigImg',
4869
fields: {
4970
name: { show: true, color: '#000' },
50-
price: { show: true, color: '#ff3000' }
71+
introduction: { show: true, color: '#999' },
72+
price: { show: true, color: '#ff3000' },
73+
marketPrice: { show: true, color: '#c4c4c4' },
74+
salesCount: { show: true, color: '#c4c4c4' },
75+
stock: { show: false, color: '#c4c4c4' }
5176
},
5277
badge: { show: false, imgUrl: '' },
78+
btnBuy: {
79+
type: 'text',
80+
text: '立即秒杀',
81+
bgBeginColor: '#FF6000',
82+
bgEndColor: '#FE832A',
83+
imgUrl: ''
84+
},
5385
borderRadiusTop: 8,
5486
borderRadiusBottom: 8,
5587
space: 8,
Lines changed: 161 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,135 +1,201 @@
11
<template>
2-
<el-scrollbar ref="containerRef" class="z-1 min-h-30px" wrap-class="w-full">
3-
<!-- 商品网格 -->
2+
<div :class="`box-content min-h-30px w-full flex flex-row flex-wrap`" ref="containerRef">
43
<div
4+
class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white"
55
:style="{
6-
gridGap: `${property.space}px`,
7-
gridTemplateColumns,
8-
width: scrollbarWidth
6+
...calculateSpace(index),
7+
...calculateWidth(),
8+
borderTopLeftRadius: `${property.borderRadiusTop}px`,
9+
borderTopRightRadius: `${property.borderRadiusTop}px`,
10+
borderBottomLeftRadius: `${property.borderRadiusBottom}px`,
11+
borderBottomRightRadius: `${property.borderRadiusBottom}px`
912
}"
10-
class="grid overflow-x-auto"
13+
v-for="(spu, index) in spuList"
14+
:key="index"
1115
>
12-
<!-- 商品 -->
16+
<!-- 角标 -->
17+
<div v-if="property.badge.show" class="absolute left-0 top-0 z-1 items-center justify-center">
18+
<el-image fit="cover" :src="property.badge.imgUrl" class="h-26px w-38px" />
19+
</div>
20+
<!-- 商品封面图 -->
1321
<div
14-
v-for="(spu, index) in spuList"
15-
:key="index"
16-
:style="{
17-
borderTopLeftRadius: `${property.borderRadiusTop}px`,
18-
borderTopRightRadius: `${property.borderRadiusTop}px`,
19-
borderBottomLeftRadius: `${property.borderRadiusBottom}px`,
20-
borderBottomRightRadius: `${property.borderRadiusBottom}px`
21-
}"
22-
class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white"
22+
:class="[
23+
'h-140px',
24+
{
25+
'w-full': property.layoutType !== 'oneColSmallImg',
26+
'w-140px': property.layoutType === 'oneColSmallImg'
27+
}
28+
]"
2329
>
24-
<!-- 角标 -->
25-
<div
26-
v-if="property.badge.show"
27-
class="absolute left-0 top-0 z-1 items-center justify-center"
28-
>
29-
<el-image :src="property.badge.imgUrl" class="h-26px w-38px" fit="cover" />
30-
</div>
31-
<!-- 商品封面图 -->
32-
<el-image :src="spu.picUrl" :style="{ width: imageSize, height: imageSize }" fit="cover" />
30+
<el-image fit="cover" class="h-full w-full" :src="spu.picUrl" />
31+
</div>
32+
<div
33+
:class="[
34+
' flex flex-col gap-8px p-8px box-border',
35+
{
36+
'w-full': property.layoutType !== 'oneColSmallImg',
37+
'w-[calc(100%-140px-16px)]': property.layoutType === 'oneColSmallImg'
38+
}
39+
]"
40+
>
41+
<!-- 商品名称 -->
3342
<div
43+
v-if="property.fields.name.show"
3444
:class="[
35-
'flex flex-col gap-8px p-8px box-border',
45+
'text-14px ',
3646
{
37-
'w-[calc(100%-64px)]': columns === 2,
38-
'w-full': columns === 3
47+
truncate: property.layoutType !== 'oneColSmallImg',
48+
'overflow-ellipsis line-clamp-2': property.layoutType === 'oneColSmallImg'
3949
}
4050
]"
51+
:style="{ color: property.fields.name.color }"
4152
>
42-
<!-- 商品名称 -->
43-
<div
44-
v-if="property.fields.name.show"
45-
:style="{ color: property.fields.name.color }"
46-
class="truncate text-12px"
53+
{{ spu.name }}
54+
</div>
55+
<!-- 商品简介 -->
56+
<div
57+
v-if="property.fields.introduction.show"
58+
class="truncate text-12px"
59+
:style="{ color: property.fields.introduction.color }"
60+
>
61+
{{ spu.introduction }}
62+
</div>
63+
<div>
64+
<!-- 价格 -->
65+
<span
66+
v-if="property.fields.price.show"
67+
class="text-16px"
68+
:style="{ color: property.fields.price.color }"
69+
>
70+
¥{{ fenToYuan(spu.price || Infinity) }}
71+
</span>
72+
<!-- 市场价 -->
73+
<span
74+
v-if="property.fields.marketPrice.show && spu.marketPrice"
75+
class="ml-4px text-10px line-through"
76+
:style="{ color: property.fields.marketPrice.color }"
77+
>¥{{ fenToYuan(spu.marketPrice) }}</span
78+
>
79+
</div>
80+
<div class="text-12px">
81+
<!-- 销量 -->
82+
<span
83+
v-if="property.fields.salesCount.show"
84+
:style="{ color: property.fields.salesCount.color }"
4785
>
48-
{{ spu.name }}
49-
</div>
50-
<div>
51-
<!-- 商品价格 -->
52-
<span
53-
v-if="property.fields.price.show"
54-
:style="{ color: property.fields.price.color }"
55-
class="text-12px"
56-
>
57-
¥{{ fenToYuan(spu.seckillPrice || spu.price || 0) }}
58-
</span>
59-
</div>
86+
已售{{ (spu.salesCount || 0) + (spu.virtualSalesCount || 0) }}件
87+
</span>
88+
<!-- 库存 -->
89+
<span v-if="property.fields.stock.show" :style="{ color: property.fields.stock.color }">
90+
库存{{ spu.stock || 0 }}
91+
</span>
6092
</div>
6193
</div>
94+
<!-- 购买按钮 -->
95+
<div class="absolute bottom-8px right-8px">
96+
<!-- 文字按钮 -->
97+
<span
98+
v-if="property.btnBuy.type === 'text'"
99+
class="rounded-full p-x-12px p-y-4px text-12px text-white"
100+
:style="{
101+
background: `linear-gradient(to right, ${property.btnBuy.bgBeginColor}, ${property.btnBuy.bgEndColor}`
102+
}"
103+
>
104+
{{ property.btnBuy.text }}
105+
</span>
106+
<!-- 图片按钮 -->
107+
<el-image
108+
v-else
109+
class="h-28px w-28px rounded-full"
110+
fit="cover"
111+
:src="property.btnBuy.imgUrl"
112+
/>
113+
</div>
62114
</div>
63-
</el-scrollbar>
115+
</div>
64116
</template>
65-
<script lang="ts" setup>
117+
<script setup lang="ts">
66118
import { PromotionSeckillProperty } from './config'
67119
import * as ProductSpuApi from '@/api/mall/product/spu'
68-
import { Spu } from '@/api/mall/product/spu'
69120
import * as SeckillActivityApi from '@/api/mall/promotion/seckill/seckillActivity'
70-
import { SeckillProductVO } from '@/api/mall/promotion/seckill/seckillActivity'
71121
import { fenToYuan } from '@/utils'
72122
73-
/** 秒杀 */
123+
/** 秒杀卡片 */
74124
defineOptions({ name: 'PromotionSeckill' })
75125
// 定义属性
76126
const props = defineProps<{ property: PromotionSeckillProperty }>()
77127
// 商品列表
78128
const spuList = ref<ProductSpuApi.Spu[]>([])
129+
const spuIdList = ref<number[]>([])
130+
const seckillActivityList = ref<SeckillActivityApi.SeckillActivityVO[]>([])
131+
79132
watch(
80-
() => props.property.activityId,
133+
() => props.property.activityIds,
81134
async () => {
82-
if (!props.property.activityId) return
83-
const activity = await SeckillActivityApi.getSeckillActivity(props.property.activityId)
84-
if (!activity?.spuId) return
85-
spuList.value = [await ProductSpuApi.getSpu(activity.spuId)]
86-
spuList.value = [await ProductSpuApi.getSpu(activity.spuId)]
87-
// 循环活动信息,赋值秒杀最低价格
88-
activity.products.forEach((product: SeckillProductVO) => {
89-
spuList.value.forEach((spu: Spu) => {
90-
spu.seckillPrice = Math.min(spu.seckillPrice || Infinity, product.seckillPrice) // 设置 SPU 的最低价格
91-
})
92-
})
135+
try {
136+
// 新添加的秒杀组件,是没有活动ID的
137+
const activityIds = props.property.activityIds
138+
// 检查活动ID的有效性
139+
if (Array.isArray(activityIds) && activityIds.length > 0) {
140+
// 获取秒杀活动详情列表
141+
seckillActivityList.value =
142+
await SeckillActivityApi.getSeckillActivityListByIds(activityIds)
143+
144+
// 获取秒杀活动的 SPU 详情列表
145+
spuList.value = []
146+
spuIdList.value = seckillActivityList.value
147+
.map((activity) => activity.spuId)
148+
.filter((spuId): spuId is number => typeof spuId === 'number')
149+
if (spuIdList.value.length > 0) {
150+
spuList.value = await ProductSpuApi.getSpuDetailList(spuIdList.value)
151+
}
152+
153+
// 更新 SPU 的最低价格
154+
seckillActivityList.value.forEach((activity) => {
155+
// 匹配spuId
156+
const spu = spuList.value.find((spu) => spu.id === activity.spuId)
157+
if (spu) {
158+
// 赋值活动价格,哪个最便宜就赋值哪个
159+
spu.price = Math.min(activity.seckillPrice || Infinity, spu.price || Infinity)
160+
}
161+
})
162+
}
163+
} catch (error) {
164+
console.error('获取秒杀活动细节或 SPU 细节时出错:', error)
165+
}
93166
},
94167
{
95168
immediate: true,
96169
deep: true
97170
}
98171
)
99-
// 手机宽度
100-
const phoneWidth = ref(375)
172+
173+
/**
174+
* 计算商品的间距
175+
* @param index 商品索引
176+
*/
177+
const calculateSpace = (index: number) => {
178+
// 商品的列数
179+
const columns = props.property.layoutType === 'twoCol' ? 2 : 1
180+
// 第一列没有左边距
181+
const marginLeft = index % columns === 0 ? '0' : props.property.space + 'px'
182+
// 第一行没有上边距
183+
const marginTop = index < columns ? '0' : props.property.space + 'px'
184+
185+
return { marginLeft, marginTop }
186+
}
187+
101188
// 容器
102189
const containerRef = ref()
103-
// 商品的列数
104-
const columns = ref(2)
105-
// 滚动条宽度
106-
const scrollbarWidth = ref('100%')
107-
// 商品图大小
108-
const imageSize = ref('0')
109-
// 商品网络列数
110-
const gridTemplateColumns = ref('')
111-
// 计算布局参数
112-
watch(
113-
() => [props.property, phoneWidth, spuList.value.length],
114-
() => {
115-
// 计算列数
116-
columns.value = props.property.layoutType === 'oneCol' ? 1 : 3
117-
// 每列的宽度为:(总宽度 - 间距 * (列数 - 1))/ 列数
118-
const productWidth =
119-
(phoneWidth.value - props.property.space * (columns.value - 1)) / columns.value
120-
// 商品图布局:2列时,左右布局 3列时,上下布局
121-
imageSize.value = columns.value === 2 ? '64px' : `${productWidth}px`
122-
// 指定列数
123-
gridTemplateColumns.value = `repeat(${columns.value}, auto)`
124-
// 不滚动
125-
scrollbarWidth.value = '100%'
126-
},
127-
{ immediate: true, deep: true }
128-
)
129-
onMounted(() => {
130-
// 提取手机宽度
131-
phoneWidth.value = containerRef.value?.wrapRef?.offsetWidth || 375
132-
})
190+
// 计算商品的宽度
191+
const calculateWidth = () => {
192+
let width = '100%'
193+
// 双列时每列的宽度为:(总宽度 - 间距)/ 2
194+
if (props.property.layoutType === 'twoCol') {
195+
width = `${(containerRef.value.offsetWidth - props.property.space) / 2}px`
196+
}
197+
return { width }
198+
}
133199
</script>
134200

135-
<style lang="scss" scoped></style>
201+
<style scoped lang="scss"></style>

0 commit comments

Comments
 (0)