营销:适配商城装修组件【商品栏】

This commit is contained in:
owen 2023-11-19 18:27:25 +08:00
parent ebb19cfe8c
commit 3198688eb5
5 changed files with 292 additions and 2 deletions

View File

@ -0,0 +1,64 @@
import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
/** 商品卡片属性 */
export interface ProductListProperty {
// 布局类型:双列 | 三列 | 水平滑动
layoutType: 'twoCol' | 'threeCol' | 'horizSwiper'
// 商品字段
fields: {
// 商品名称
name: ProductListFieldProperty
// 商品价格
price: ProductListFieldProperty
}
// 角标
badge: {
// 是否显示
show: boolean
// 角标图片
imgUrl: string
}
// 上圆角
borderRadiusTop: number
// 下圆角
borderRadiusBottom: number
// 间距
space: number
// 商品编号列表
spuIds: number[]
// 组件样式
style: ComponentStyle
}
// 商品字段
export interface ProductListFieldProperty {
// 是否显示
show: boolean
// 颜色
color: string
}
// 定义组件
export const component = {
id: 'ProductList',
name: '商品栏',
icon: 'system-uicons:carousel',
property: {
layoutType: 'twoCol',
fields: {
name: { show: true, color: '#000' },
price: { show: true, color: '#ff3000' }
},
badge: { show: false, imgUrl: '' },
borderRadiusTop: 8,
borderRadiusBottom: 8,
space: 8,
spuIds: [],
style: {
bgType: 'color',
bgColor: '',
marginLeft: 8,
marginRight: 8,
marginBottom: 8
} as ComponentStyle
}
} as DiyComponent<ProductListProperty>

View File

@ -0,0 +1,128 @@
<template>
<el-scrollbar class="z-1 min-h-30px" wrap-class="w-full" ref="containerRef">
<!-- 商品网格 -->
<div
class="grid overflow-x-auto"
:style="{
gridGap: `${property.space}px`,
gridTemplateColumns,
width: scrollbarWidth,
}"
>
<!-- 商品 -->
<div
class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white"
:style="{
borderTopLeftRadius: `${property.borderRadiusTop}px`,
borderTopRightRadius: `${property.borderRadiusTop}px`,
borderBottomLeftRadius: `${property.borderRadiusBottom}px`,
borderBottomRightRadius: `${property.borderRadiusBottom}px`
}"
v-for="(spu, index) in spuList"
:key="index"
>
<!-- 角标 -->
<div
v-if="property.badge.show"
class="absolute left-0 top-0 z-1 items-center justify-center"
>
<el-image fit="cover" :src="property.badge.imgUrl" class="h-26px w-38px" />
</div>
<!-- 商品封面图 -->
<el-image fit="cover" :src="spu.picUrl" :style="{ width: imageSize, height: imageSize }" />
<div
:class="[
'flex flex-col gap-8px p-8px box-border',
{
'w-[calc(100%-64px)]': columns === 2,
'w-full': columns === 3
}
]"
>
<!-- 商品名称 -->
<div
v-if="property.fields.name.show"
class="truncate text-12px"
:style="{ color: property.fields.name.color }"
>
{{ spu.name }}
</div>
<div>
<!-- 商品价格 -->
<span
v-if="property.fields.price.show"
class="text-12px"
:style="{ color: property.fields.price.color }"
>
{{ spu.price }}
</span>
</div>
</div>
</div>
</div>
</el-scrollbar>
</template>
<script setup lang="ts">
import { ProductListProperty } from "./config"
import * as ProductSpuApi from "@/api/mall/product/spu"
/** 商品卡片 */
defineOptions({ name: "ProductList" })
//
const props = defineProps<{ property: ProductListProperty }>()
//
const spuList = ref<ProductSpuApi.Spu[]>([])
watch(
() => props.property.spuIds,
async () => {
spuList.value = await ProductSpuApi.getSpuDetailList(props.property.spuIds)
},
{
immediate: true,
deep: true
}
)
//
const phoneWidth = ref(375)
//
const containerRef = ref()
//
const columns = ref(2)
//
const scrollbarWidth = ref("100%")
//
const imageSize = ref("0")
//
const gridTemplateColumns = ref("")
//
watch(
() => [props.property, phoneWidth, spuList.value.length],
() => {
//
columns.value = props.property.layoutType === "twoCol" ? 2 : 3
//
// - * ( - 1)/
const productWidth = (phoneWidth.value - props.property.space * (columns.value - 1)) / columns.value
// 2 3
imageSize.value = columns.value === 2 ? "64px" : `${productWidth}px`
//
if (props.property.layoutType === "horizSwiper") {
//
gridTemplateColumns.value = `repeat(auto-fill, ${productWidth}px)`
//
scrollbarWidth.value = `${productWidth * spuList.value.length + props.property.space * (spuList.value.length - 1)}px`
} else {
//
gridTemplateColumns.value = `repeat(${columns.value}, auto)`
//
scrollbarWidth.value = "100%"
}
},
{ immediate: true, deep: true }
)
onMounted(() => {
phoneWidth.value = containerRef.value?.wrapRef?.offsetWidth || 375;
})
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,99 @@
<template>
<ComponentContainerProperty v-model="formData.style">
<el-form label-width="80px" :model="formData">
<el-card header="商品列表" class="property-group" shadow="never">
<SpuShowcase v-model="formData.spuIds" />
</el-card>
<el-card header="商品样式" class="property-group" shadow="never">
<el-form-item label="布局" prop="type">
<el-radio-group v-model="formData.layoutType">
<el-tooltip class="item" content="双列" placement="bottom">
<el-radio-button label="twoCol">
<Icon icon="fluent:text-column-two-24-filled" />
</el-radio-button>
</el-tooltip>
<el-tooltip class="item" content="三列" placement="bottom">
<el-radio-button label="threeCol">
<Icon icon="fluent:text-column-three-24-filled" />
</el-radio-button>
</el-tooltip>
<el-tooltip class="item" content="水平滑动" placement="bottom">
<el-radio-button label="horizSwiper">
<Icon icon="system-uicons:carousel" />
</el-radio-button>
</el-tooltip>
</el-radio-group>
</el-form-item>
<el-form-item label="商品名称" prop="fields.name.show">
<div class="flex gap-8px">
<ColorInput v-model="formData.fields.name.color" />
<el-checkbox v-model="formData.fields.name.show" />
</div>
</el-form-item>
<el-form-item label="商品价格" prop="fields.price.show">
<div class="flex gap-8px">
<ColorInput v-model="formData.fields.price.color" />
<el-checkbox v-model="formData.fields.price.show" />
</div>
</el-form-item>
</el-card>
<el-card header="角标" class="property-group" shadow="never">
<el-form-item label="角标" prop="badge.show">
<el-switch v-model="formData.badge.show" />
</el-form-item>
<el-form-item label="角标" prop="badge.imgUrl" v-if="formData.badge.show">
<UploadImg v-model="formData.badge.imgUrl" height="44px" width="72px">
<template #tip> 建议尺寸36 * 22 </template>
</UploadImg>
</el-form-item>
</el-card>
<el-card header="商品样式" class="property-group" shadow="never">
<el-form-item label="上圆角" prop="borderRadiusTop">
<el-slider
v-model="formData.borderRadiusTop"
:max="100"
:min="0"
show-input
input-size="small"
:show-input-controls="false"
/>
</el-form-item>
<el-form-item label="下圆角" prop="borderRadiusBottom">
<el-slider
v-model="formData.borderRadiusBottom"
:max="100"
:min="0"
show-input
input-size="small"
:show-input-controls="false"
/>
</el-form-item>
<el-form-item label="间隔" prop="space">
<el-slider
v-model="formData.space"
:max="100"
:min="0"
show-input
input-size="small"
:show-input-controls="false"
/>
</el-form-item>
</el-card>
</el-form>
</ComponentContainerProperty>
</template>
<script setup lang="ts">
import { ProductListProperty } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
import SpuShowcase from '@/views/mall/product/spu/components/SpuShowcase.vue'
//
defineOptions({ name: 'ProductListProperty' })
const props = defineProps<{ modelValue: ProductListProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
</script>
<style scoped lang="scss"></style>

View File

@ -107,7 +107,7 @@ export const PAGE_LIBS = [
extended: true, extended: true,
components: ['ImageBar', 'Carousel', 'TitleBar', 'VideoPlayer', 'Divider', 'MagicCube'] components: ['ImageBar', 'Carousel', 'TitleBar', 'VideoPlayer', 'Divider', 'MagicCube']
}, },
{ name: '商品组件', extended: true, components: ['ProductCard'] }, { name: '商品组件', extended: true, components: ['ProductCard', 'ProductList'] },
{ {
name: '会员组件', name: '会员组件',
extended: true, extended: true,

View File

@ -59,7 +59,6 @@ watch(
productSpus.value.length === 0 || productSpus.value.length === 0 ||
productSpus.value.some((spu) => !props.modelValue.includes(spu.id)) productSpus.value.some((spu) => !props.modelValue.includes(spu.id))
) { ) {
debugger
productSpus.value = await ProductSpuApi.getSpuDetailList(props.modelValue) productSpus.value = await ProductSpuApi.getSpuDetailList(props.modelValue)
} }
}, },