目录
写在前面
笔者不是上一个月毕业了么?找工作没找到,准备在家躺平两个月。正好整理一下当时的毕业设计,是一个商城系统。还是写篇文章记录下吧
预览
商品图片切换显示
sku规格切换
文章主要描述左侧图片组件,右侧sku的切换实现
在阅读本篇文章之前,你需要了解的是SPU和SKU是什么。
实现
图片部分
图片部分组件代码
<template> <div> <div style="display: flex;justify-content: center"> <!-- 大图--> <el-image :src="$img+showBigImg" style="width: 400px; height: 400px"> <template #error> <div class="image-slot"> <el-icon> <icon-picture/> </el-icon> </div> </template> </el-image> </div> <!-- 小图--> <div style="display: flex;justify-content: space-between;margin-top: 20px"> <el-icon style="width: 50px;height: 50px" @click="last"> <ArrowLeftBold/> </el-icon> <div style="display: flex;justify-content: center;"> <div v-for="item in showList" :key="item" :class="item===showBigImg ? 'is_show' : 'not_show' " @mouseover="show(item)" @click="show(item)" style="margin-left: 20px"> <el-image :src="$img+item" fit="fill" style="width: 50px;height: 50px"/> </div> </div> <el-icon @click="next" style="width: 50px;height: 50px"> <ArrowRightBold/> </el-icon> </div> </div> </template> <script setup> import {ref, defineProps, watch,} from 'vue' let props = defineProps({ imgList: { type: Array, default: () => [] } }) let showBigImg = ref(props.imgList[0])//页面初始化的时候显示第一个图片 let pageNum = ref(0)//当前展示图片处于多少页 let showList = ref(props.imgList.slice(0, 4))//初始化只展示前四个 function show(big) { showBigImg.value = big } //下一页 function last() { // 通过判断showBigImg的值来判断当前处于元素 if (Array.isArray(showList.value)) { //查找索引,判断是否是最后一个元素(注意此处是原数组) if (pageNum.value === 0) { return } pageNum.value-- } else { console.log("不是数组类型") } } //上一页 function next() { if (Array.isArray(props.imgList)) { //计算最大的页码,向上取整 let mixPage = Math.ceil(props.imgList.length / 4); if ((pageNum.value + 1) === mixPage) { //如果是最后一页,则不能翻页 return } //如果不是最后一页,则翻页 pageNum.value++ } else { console.log("不是数组类型") } } watch(() => props.imgList, () => { //监听父组件传递数据的变化,修改展示的数据 showBigImg.value=props.imgList[0] showList.value = props.imgList.slice(0, 4) }) watch(pageNum, () => { // 监听pageNum的值变换,修改showList的显示数据 showList.value = props.imgList.slice(pageNum.value * 4, (pageNum.value + 1) * 4) showBigImg.value = showList.value[0] }) </script> <style scoped> .not_show { width: 50px; height: 50px; border: 1px; } .is_show { width: 50px; height: 50px; border: solid 3px #098CC0; } </style>
注:
- 父组件需要控制这个图片组件的渲染时机,最好保证图片数据获取到了之后再加载组件。虽然,子组件已经监听了prop的数据,但是为了避免问题,还是尽量数据获取之后再加载组件
- 需要监听父组件传递的图片路径数据变化,当sku变化的时候,父组件会传递的图片路径也会变化。
详情部分
详情部分思路比较多,我之前周实训也是写的商城,那个时候的思路是页面显示SKU的属性名称。每点击一个SKU销售属性值,就将当前选中的SKU销售属性值发送给后端,然后后端去数据库中查找哪个SKU具有这两个属性值(后端计算)。然后再返回这个sku的信息。当时是以SPU为主。即首页展示的商品信息都是SPU。点击SPU后获取这个SPU下的所有SKU的属性组合,并随机获取一个SKU的信息用于初始展示。(如果读者想了解我之前的写法的话,请回复评论,我可以去找找,毕竟有将近两年了)
这次参考谷粒商城的方式后,采用的是以SKU为主,即首页展示的商品列表都是SKU。点击一个SKU后查询SPU的所有SKU属性值。当用户点击属性时,然后前端需要计算当前选中的是哪一个sku,然后再请求后端获取数据
代码
老样子先粘代码,再解读
<template> <div> <el-skeleton :loading="loading" animated> <template #default> <div class="detail_sku_box"> <el-row :gutter="20"> <!-- 左侧商品图部分--> <el-col :span="8" :offset="1"> <img-list :imgList="imgUrlList" v-if="showImg"></img-list> </el-col> <!-- 右侧SKU信息以及销售属性部分--> <el-col :span="14"> <div> <h2>{{ skuItemInfo?.skuInfo?.skuTitle }}</h2> <h5>{{ skuItemInfo?.skuInfo?.skuSubtitle }}</h5> <div> <span class="price_class">¥{{ skuItemInfo?.skuInfo?.price }}</span> </div> <div style="margin-top: 20px"> <div v-for="item in skuItemInfo?.skuItemSaleAttrVos" :key="item.id"> <el-row :gutter="20"> <el-col :span="3">{{ item.attrName }}</el-col> <el-col :span="4" v-for="(valueItem,i) in item.attValues" :key="i"> <button :class="valueItem?.skuIds.indexOf(skuItemInfo.skuInfo?.id)!=-1 ? 'skuValue_act_class' : 'skuValue_inc_class'" @click="selectItem(item,valueItem.skuIds)">{{ valueItem.attrValue }} </button> </el-col> </el-row> <hr> </div> </div> <div style="margin-top: 50px"> 库存:{{ skuItemInfo.skuStock }} </div> <div style="margin-top: 50px"> <el-input-number v-model="num"/> </div> <div style="margin-top: 50px"> <button class="button button2" @click="addCart"><span>加入购物车</span></button> <button class="button button2" @click="toConfirmOrder"><span>立即购买</span></button> <button class="button button2" @click="goToCustomerService"><span>联系客服</span></button> </div> </div> </el-col> </el-row> </div> <!-- 下侧SPU、规格、售后、评价部分--> <div class="msg_box"> <div> <el-row> <el-col :span="4" :class=" showComponentData==0 ? 'active_class': 'no_active'"><span @click="showComponents(0)">商品介绍</span></el-col> <el-col :span="4" :class=" showComponentData==1 ? 'active_class': 'no_active'"><span @click="showComponents(1)">规格与包装</span></el-col> <el-col :span="4" :class=" showComponentData==2 ? 'active_class': 'no_active'"><span @click="showComponents(2)">售后保障</span></el-col> <el-col :span="4" :class=" showComponentData==3 ? 'active_class': 'no_active'"><span @click="showComponents(3)">商品评价</span></el-col> </el-row> <hr> </div> </div> <div style="padding-left: 100px;"> <!-- 商品介绍--> <spu-describes v-if="showComponentData==0" :descImgUrl="spuDescImgUrl" :descInfo="skuItemInfo"></spu-describes> <!-- 规格与包装--> <spu-specification :group-data="skuItemInfo.groupAttrs" v-else-if="showComponentData==1" ></spu-specification> <!-- 商品评价--> <div v-else-if="showComponentData===2"> <div> <el-image :src="require('@/assets/shouhou.jpg')"></el-image> </div> </div> <appraise-list :skuId="skuItemInfo.skuInfo.id" v-else-if="showComponentData===3"/> </div> </template> <!-- 骨架屏内容--> <template #template> <div style="height: 100%"> <el-row :gutter="20"> <el-col :span="7" :offset="1"> <el-skeleton-item variant="image" style="height: 500px"/> </el-col> <el-col :span="13" :offset="1"> <div> <el-skeleton-item variant="text" style="width: 100%;height: 40px"/> <el-skeleton-item variant="text" style="width: 100%;height: 40px;margin-top: 20px"/> <div style="margin-top: 30px"> <span class="price_class"><el-skeleton-item variant="text" style="height: 30px;width: 80%"/></span> </div> <div style="margin-top: 150px"> <div v-for="item in 2" :key="item"> <el-row :gutter="20"> <el-col :span="3"> <el-skeleton-item variant="text" style="height: 30px"/> </el-col> <el-col :span="3" v-for="(i) in 3" :key="i"> <el-skeleton-item variant="text" style="height: 30px"/> </el-col> </el-row> </div> </div> </div> </el-col> </el-row> </div> </template> </el-skeleton> </div> </template> <script setup> import SpuDescribes from '@/components/commodity/SpuDescribes' import SpuSpecification from '@/components/commodity/SpuSpecification' import AppraiseList from '@/components/commodity/AppraiseList' import {ref, onMounted,} from "vue"; import {useRoute, useRouter} from "vue-router"; import {getSkuItemApi,} from "@/api/goods"; import ImgList from "@/components/commodity/ImgList"; import {addCartApi} from '@/api/cart' import {ElMessage, ElMessageBox} from 'element-plus' let loading = ref(true) const route = useRoute() const router = useRouter() let imgUrlList = ref([]) let skuItemInfo = ref({}) let spuDescImgUrl = ref() let showComponentData = ref(-1)//由于子组件需要父组件传递数据,需要确保父组件数据准备好了再加载子组件 let num = ref(1) let showImg = ref(false) onMounted(() => { if (route.query.skuId) { getSkuItem(route.query.skuId) } setTimeout(() => { loading.value = false }, 1000) }) function getSkuItem(skuId) { getSkuItemApi(skuId).then(res => { skuItemInfo.value = res.data skuItemInfo.value?.skuItemSaleAttrVos.forEach(attr => { attr.attValues.forEach(item => { if (item.skuIds.indexOf(skuItemInfo.value.skuInfo.id) != -1) { attr.selectSkuList = [...item.skuIds]//赋值,但是不引用地址值 } }) }) /*获取图片路径*/ let imgIds = res.data.skuImags.map(item => { return item.imgId }) imgUrlList.value = [...imgIds] showImg.value = true }).then(() => { showComponentData.value = 0//父组件数据已经准备妥当,加载子组件 }) } /** * 添加到购物车 */ function addCart() { addCartApi(skuItemInfo.value.skuInfo?.id, num.value) /*TODO 是否需要跳转购物车列表*/ } function toConfirmOrder() { if (skuItemInfo.value.skuStock - num.value < 0) { return ElMessage({message: "库存不足", type: 'error'}) } //前往确认订单页面,传递参数:选择的skuid,购买的件数 let data = { skuId: skuItemInfo.value.skuInfo.id,//选择的sku buyNumber: num.value//购买的件数 } router.push({ path: '/order-confirm', query: { data: JSON.stringify([data]) } }) } function selectItem(selectGroup, skuIds) { /*先保存当前选中元素的skuIds数组,然后计算*/ selectGroup.selectSkuList = skuIds /*遍历所有的分组,获取选中的skuid,然后计算出交集*/ let arr = skuItemInfo.value?.skuItemSaleAttrVos.map(item => { return item.selectSkuList }) //得出当前的skuId let findSkuId = findIntersection(arr) getSkuItem(findSkuId) //修改路由上的query值 router.push({query:{ skuId:findSkuId }}) } /** * 获取传入的数组中的交集的值 * @param arrays * @returns {null} */ function findIntersection(arrays) { // 创建一个 Set 对象来存储所有数组中的唯一元素 let set = new Set(); // 遍历每个数组,将其中的元素添加到 Set 对象中 for (let array of arrays) { for (let element of array) { set.add(element); } } // 找到交集元素,即唯一存在于所有数组中的元素 let intersection = null; for (let element of set) { if (arrays.every(array => array.includes(element))) { intersection = element; break; } } return intersection; } function goToCustomerService() { ElMessageBox.alert('功能还未实现', '警告', { confirmButtonText: '确定', }) } function showComponents(v) { showComponentData.value = v } </script> <style scoped> .price_class { font-size: 30px; color: red; } .active_class { color: #288FC7; } .msg_box { margin-top: 200px; padding-left: 100px; } .detail_sku_box { height: 400px; /*background-color: #99a9bf;*/ } .skuValue_act_class { background-color: #098CC0; /*color: red;*/ } .no_active:hover{ color: red; } .cart_btn_msg { color: white; } .button { width: 150px; height: 50px; background-color: #288FC7; border: none; color: white; padding: 15px 32px; text-align: center; text-decoration: none; display: inline-block; font-size: 16px; margin: 4px 2px; cursor: pointer; -webkit-transition-duration: 0.4s; transition-duration: 0.4s; } .button2:hover { box-shadow: 0 12px 16px 0 rgba(0, 0, 0, 0.24), 0 17px 50px 0 rgba(0, 0, 0, 0.19); background-color: #1DAEEE; color: red; } </style>
下面是后端响应的VO模型
将SKU的销售属性值组合封装起来
这个方式后端要轻松一点。后端只需要获取获取前端传递的skuId返回这个VO即可。
前端就比较麻烦了,需要在点击不同的销售属性值时,计算出这个组合的SKU是哪一个。
我们来看一下
当页面点击一个属性值的时候,怎么才能计算出他的SKU?这个还是有一定难度的,因为这个
是一个不固定长度的数组,有可能是一个,有可能是两个,有可能是三个、四个都有可能。虽然大多数情况只有两个,比如说手机就有两个:颜色,版本。假设再添加几个属性(分期、保修服务)就更多了。你可能问这有什么关系呢?计算sku需要用到吗?
别急,我们先看看这个销售属性值下面的skuIds是什么:他表示的是当前的属性值哪几个SKU具有!也就是说我们在计算sku的时候用的就是他来计算的!
当页面点击属性值的同时就能获取到这个skuIds值了,然后将其存起来。以供计算
比如当前只有两个属性,那么就会有两个skuIds数组。很简单,我们只需要计算这两个skuIds数组元素的交集即可。如果有三个属性,那么就会有三个skuIds数组,计算他们的交集。听起来很简单?我当时想了一会,没弄出来,最后丢给文心一言实现了(文心一言都错了三四次哈哈)。代码如下
说实话我现在也没懂,对算法这一块还处于文盲状态。不过能实现就行!
源码地址
总结
sku计算那里,是这个详情页面唯一的难点,其他的部分我就不多说了。如果读者觉得有哪些部分不全,或者想要了解其他部分,随时评论。