Skip to content

Commit 3198688

Browse files
committed
营销:适配商城装修组件【商品栏】
1 parent ebb19cf commit 3198688

File tree

5 files changed

+292
-2
lines changed

5 files changed

+292
-2
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
2+
3+
/** 商品卡片属性 */
4+
export interface ProductListProperty {
5+
// 布局类型:双列 | 三列 | 水平滑动
6+
layoutType: 'twoCol' | 'threeCol' | 'horizSwiper'
7+
// 商品字段
8+
fields: {
9+
// 商品名称
10+
name: ProductListFieldProperty
11+
// 商品价格
12+
price: ProductListFieldProperty
13+
}
14+
// 角标
15+
badge: {
16+
// 是否显示
17+
show: boolean
18+
// 角标图片
19+
imgUrl: string
20+
}
21+
// 上圆角
22+
borderRadiusTop: number
23+
// 下圆角
24+
borderRadiusBottom: number
25+
// 间距
26+
space: number
27+
// 商品编号列表
28+
spuIds: number[]
29+
// 组件样式
30+
style: ComponentStyle
31+
}
32+
// 商品字段
33+
export interface ProductListFieldProperty {
34+
// 是否显示
35+
show: boolean
36+
// 颜色
37+
color: string
38+
}
39+
40+
// 定义组件
41+
export const component = {
42+
id: 'ProductList',
43+
name: '商品栏',
44+
icon: 'system-uicons:carousel',
45+
property: {
46+
layoutType: 'twoCol',
47+
fields: {
48+
name: { show: true, color: '#000' },
49+
price: { show: true, color: '#ff3000' }
50+
},
51+
badge: { show: false, imgUrl: '' },
52+
borderRadiusTop: 8,
53+
borderRadiusBottom: 8,
54+
space: 8,
55+
spuIds: [],
56+
style: {
57+
bgType: 'color',
58+
bgColor: '',
59+
marginLeft: 8,
60+
marginRight: 8,
61+
marginBottom: 8
62+
} as ComponentStyle
63+
}
64+
} as DiyComponent<ProductListProperty>
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
<template>
2+
<el-scrollbar class="z-1 min-h-30px" wrap-class="w-full" ref="containerRef">
3+
<!-- 商品网格 -->
4+
<div
5+
class="grid overflow-x-auto"
6+
:style="{
7+
gridGap: `${property.space}px`,
8+
gridTemplateColumns,
9+
width: scrollbarWidth,
10+
}"
11+
>
12+
<!-- 商品 -->
13+
<div
14+
class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white"
15+
:style="{
16+
borderTopLeftRadius: `${property.borderRadiusTop}px`,
17+
borderTopRightRadius: `${property.borderRadiusTop}px`,
18+
borderBottomLeftRadius: `${property.borderRadiusBottom}px`,
19+
borderBottomRightRadius: `${property.borderRadiusBottom}px`
20+
}"
21+
v-for="(spu, index) in spuList"
22+
:key="index"
23+
>
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 fit="cover" :src="property.badge.imgUrl" class="h-26px w-38px" />
30+
</div>
31+
<!-- 商品封面图 -->
32+
<el-image fit="cover" :src="spu.picUrl" :style="{ width: imageSize, height: imageSize }" />
33+
<div
34+
:class="[
35+
'flex flex-col gap-8px p-8px box-border',
36+
{
37+
'w-[calc(100%-64px)]': columns === 2,
38+
'w-full': columns === 3
39+
}
40+
]"
41+
>
42+
<!-- 商品名称 -->
43+
<div
44+
v-if="property.fields.name.show"
45+
class="truncate text-12px"
46+
:style="{ color: property.fields.name.color }"
47+
>
48+
{{ spu.name }}
49+
</div>
50+
<div>
51+
<!-- 商品价格 -->
52+
<span
53+
v-if="property.fields.price.show"
54+
class="text-12px"
55+
:style="{ color: property.fields.price.color }"
56+
>
57+
¥{{ spu.price }}
58+
</span>
59+
</div>
60+
</div>
61+
</div>
62+
</div>
63+
</el-scrollbar>
64+
</template>
65+
<script setup lang="ts">
66+
import { ProductListProperty } from "./config"
67+
import * as ProductSpuApi from "@/api/mall/product/spu"
68+
69+
/** 商品卡片 */
70+
defineOptions({ name: "ProductList" })
71+
// 定义属性
72+
const props = defineProps<{ property: ProductListProperty }>()
73+
// 商品列表
74+
const spuList = ref<ProductSpuApi.Spu[]>([])
75+
watch(
76+
() => props.property.spuIds,
77+
async () => {
78+
spuList.value = await ProductSpuApi.getSpuDetailList(props.property.spuIds)
79+
},
80+
{
81+
immediate: true,
82+
deep: true
83+
}
84+
)
85+
// 手机宽度
86+
const phoneWidth = ref(375)
87+
// 容器
88+
const containerRef = ref()
89+
// 商品的列数
90+
const columns = ref(2)
91+
// 滚动条宽度
92+
const scrollbarWidth = ref("100%")
93+
// 商品图大小
94+
const imageSize = ref("0")
95+
// 商品网络列数
96+
const gridTemplateColumns = ref("")
97+
// 计算布局参数
98+
watch(
99+
() => [props.property, phoneWidth, spuList.value.length],
100+
() => {
101+
// 计算列数
102+
columns.value = props.property.layoutType === "twoCol" ? 2 : 3
103+
// 提取手机宽度
104+
// 每列的宽度为:(总宽度 - 间距 * (列数 - 1))/ 列数
105+
const productWidth = (phoneWidth.value - props.property.space * (columns.value - 1)) / columns.value
106+
// 商品图布局:2列时,左右布局 3列时,上下布局
107+
imageSize.value = columns.value === 2 ? "64px" : `${productWidth}px`
108+
// 根据布局类型,计算行数、列数
109+
if (props.property.layoutType === "horizSwiper") {
110+
// 单行显示
111+
gridTemplateColumns.value = `repeat(auto-fill, ${productWidth}px)`
112+
// 显示滚动条
113+
scrollbarWidth.value = `${productWidth * spuList.value.length + props.property.space * (spuList.value.length - 1)}px`
114+
} else {
115+
// 指定列数
116+
gridTemplateColumns.value = `repeat(${columns.value}, auto)`
117+
// 不滚动
118+
scrollbarWidth.value = "100%"
119+
}
120+
},
121+
{ immediate: true, deep: true }
122+
)
123+
onMounted(() => {
124+
phoneWidth.value = containerRef.value?.wrapRef?.offsetWidth || 375;
125+
})
126+
</script>
127+
128+
<style scoped lang="scss"></style>
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<template>
2+
<ComponentContainerProperty v-model="formData.style">
3+
<el-form label-width="80px" :model="formData">
4+
<el-card header="商品列表" class="property-group" shadow="never">
5+
<SpuShowcase v-model="formData.spuIds" />
6+
</el-card>
7+
<el-card header="商品样式" class="property-group" shadow="never">
8+
<el-form-item label="布局" prop="type">
9+
<el-radio-group v-model="formData.layoutType">
10+
<el-tooltip class="item" content="双列" placement="bottom">
11+
<el-radio-button label="twoCol">
12+
<Icon icon="fluent:text-column-two-24-filled" />
13+
</el-radio-button>
14+
</el-tooltip>
15+
<el-tooltip class="item" content="三列" placement="bottom">
16+
<el-radio-button label="threeCol">
17+
<Icon icon="fluent:text-column-three-24-filled" />
18+
</el-radio-button>
19+
</el-tooltip>
20+
<el-tooltip class="item" content="水平滑动" placement="bottom">
21+
<el-radio-button label="horizSwiper">
22+
<Icon icon="system-uicons:carousel" />
23+
</el-radio-button>
24+
</el-tooltip>
25+
</el-radio-group>
26+
</el-form-item>
27+
<el-form-item label="商品名称" prop="fields.name.show">
28+
<div class="flex gap-8px">
29+
<ColorInput v-model="formData.fields.name.color" />
30+
<el-checkbox v-model="formData.fields.name.show" />
31+
</div>
32+
</el-form-item>
33+
<el-form-item label="商品价格" prop="fields.price.show">
34+
<div class="flex gap-8px">
35+
<ColorInput v-model="formData.fields.price.color" />
36+
<el-checkbox v-model="formData.fields.price.show" />
37+
</div>
38+
</el-form-item>
39+
</el-card>
40+
<el-card header="角标" class="property-group" shadow="never">
41+
<el-form-item label="角标" prop="badge.show">
42+
<el-switch v-model="formData.badge.show" />
43+
</el-form-item>
44+
<el-form-item label="角标" prop="badge.imgUrl" v-if="formData.badge.show">
45+
<UploadImg v-model="formData.badge.imgUrl" height="44px" width="72px">
46+
<template #tip> 建议尺寸:36 * 22 </template>
47+
</UploadImg>
48+
</el-form-item>
49+
</el-card>
50+
<el-card header="商品样式" class="property-group" shadow="never">
51+
<el-form-item label="上圆角" prop="borderRadiusTop">
52+
<el-slider
53+
v-model="formData.borderRadiusTop"
54+
:max="100"
55+
:min="0"
56+
show-input
57+
input-size="small"
58+
:show-input-controls="false"
59+
/>
60+
</el-form-item>
61+
<el-form-item label="下圆角" prop="borderRadiusBottom">
62+
<el-slider
63+
v-model="formData.borderRadiusBottom"
64+
:max="100"
65+
:min="0"
66+
show-input
67+
input-size="small"
68+
:show-input-controls="false"
69+
/>
70+
</el-form-item>
71+
<el-form-item label="间隔" prop="space">
72+
<el-slider
73+
v-model="formData.space"
74+
:max="100"
75+
:min="0"
76+
show-input
77+
input-size="small"
78+
:show-input-controls="false"
79+
/>
80+
</el-form-item>
81+
</el-card>
82+
</el-form>
83+
</ComponentContainerProperty>
84+
</template>
85+
86+
<script setup lang="ts">
87+
import { ProductListProperty } from './config'
88+
import { usePropertyForm } from '@/components/DiyEditor/util'
89+
import SpuShowcase from '@/views/mall/product/spu/components/SpuShowcase.vue'
90+
91+
// 商品卡片属性面板
92+
defineOptions({ name: 'ProductListProperty' })
93+
94+
const props = defineProps<{ modelValue: ProductListProperty }>()
95+
const emit = defineEmits(['update:modelValue'])
96+
const { formData } = usePropertyForm(props.modelValue, emit)
97+
</script>
98+
99+
<style scoped lang="scss"></style>

src/components/DiyEditor/util.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ export const PAGE_LIBS = [
107107
extended: true,
108108
components: ['ImageBar', 'Carousel', 'TitleBar', 'VideoPlayer', 'Divider', 'MagicCube']
109109
},
110-
{ name: '商品组件', extended: true, components: ['ProductCard'] },
110+
{ name: '商品组件', extended: true, components: ['ProductCard', 'ProductList'] },
111111
{
112112
name: '会员组件',
113113
extended: true,

src/views/mall/product/spu/components/SpuShowcase.vue

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ watch(
5959
productSpus.value.length === 0 ||
6060
productSpus.value.some((spu) => !props.modelValue.includes(spu.id))
6161
) {
62-
debugger
6362
productSpus.value = await ProductSpuApi.getSpuDetailList(props.modelValue)
6463
}
6564
},

0 commit comments

Comments
 (0)