vue3实现商城系统详情页(前端实现)

潜水阿宝 2024-08-07 14:03:03 阅读 84

目录

写在前面

预览

实现

图片部分

详情部分

代码

源码地址

总结


写在前面

笔者不是上一个月毕业了么?找工作没找到,准备在家躺平两个月。正好整理一下当时的毕业设计,是一个商城系统。还是写篇文章记录下吧

预览

商品图片切换显示

sku规格切换

文章主要描述左侧图片组件,右侧sku的切换实现

在阅读本篇文章之前,你需要了解的是SPU和SKU是什么。

实现

图片部分

图片部分组件代码

<code><template>

<div>

<div style="display: flex;justify-content: center">code>

<!-- 大图-->

<el-image :src="$img+showBigImg" style="width: 400px; height: 400px">code>

<template #error>

<div class="image-slot">code>

<el-icon>

<icon-picture/>

</el-icon>

</div>

</template>

</el-image>

</div>

<!-- 小图-->

<div style="display: flex;justify-content: space-between;margin-top: 20px">code>

<el-icon style="width: 50px;height: 50px" @click="last">code>

<ArrowLeftBold/>

</el-icon>

<div style="display: flex;justify-content: center;">code>

<div v-for="item in showList" :key="item" :class="item===showBigImg ? 'is_show' : 'not_show' "code>

@mouseover="show(item)" @click="show(item)" style="margin-left: 20px">code>

<el-image :src="$img+item" fit="fill" style="width: 50px;height: 50px"/>code>

</div>

</div>

<el-icon @click="next" style="width: 50px;height: 50px">code>

<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>code>

<template #default>

<div class="detail_sku_box">code>

<el-row :gutter="20">code>

<!-- 左侧商品图部分-->

<el-col :span="8" :offset="1">code>

<img-list :imgList="imgUrlList" v-if="showImg"></img-list>code>

</el-col>

<!-- 右侧SKU信息以及销售属性部分-->

<el-col :span="14">code>

<div>

<h2>{ { skuItemInfo?.skuInfo?.skuTitle }}</h2>

<h5>{ { skuItemInfo?.skuInfo?.skuSubtitle }}</h5>

<div>

<span class="price_class">¥{ { skuItemInfo?.skuInfo?.price }}</span>code>

</div>

<div style="margin-top: 20px">code>

<div v-for="item in skuItemInfo?.skuItemSaleAttrVos" :key="item.id">code>

<el-row :gutter="20">code>

<el-col :span="3">{ { item.attrName }}</el-col>code>

<el-col :span="4" v-for="(valueItem,i) in item.attValues" :key="i">code>

<button

:class="valueItem?.skuIds.indexOf(skuItemInfo.skuInfo?.id)!=-1 ? 'skuValue_act_class' : 'skuValue_inc_class'"code>

@click="selectItem(item,valueItem.skuIds)">{ { valueItem.attrValue }}code>

</button>

</el-col>

</el-row>

<hr>

</div>

</div>

<div style="margin-top: 50px">code>

库存:{ { skuItemInfo.skuStock }}

</div>

<div style="margin-top: 50px">code>

<el-input-number v-model="num"/>code>

</div>

<div style="margin-top: 50px">code>

<button class="button button2" @click="addCart"><span>加入购物车</span></button>code>

<button class="button button2" @click="toConfirmOrder"><span>立即购买</span></button>code>

<button class="button button2" @click="goToCustomerService"><span>联系客服</span></button>code>

</div>

</div>

</el-col>

</el-row>

</div>

<!-- 下侧SPU、规格、售后、评价部分-->

<div class="msg_box">code>

<div>

<el-row>

<el-col :span="4" :class=" showComponentData==0 ? 'active_class': 'no_active'"><spancode>

@click="showComponents(0)">商品介绍</span></el-col>code>

<el-col :span="4" :class=" showComponentData==1 ? 'active_class': 'no_active'"><spancode>

@click="showComponents(1)">规格与包装</span></el-col>code>

<el-col :span="4" :class=" showComponentData==2 ? 'active_class': 'no_active'"><spancode>

@click="showComponents(2)">售后保障</span></el-col>code>

<el-col :span="4" :class=" showComponentData==3 ? 'active_class': 'no_active'"><spancode>

@click="showComponents(3)">商品评价</span></el-col>code>

</el-row>

<hr>

</div>

</div>

<div style="padding-left: 100px;">code>

<!-- 商品介绍-->

<spu-describes

v-if="showComponentData==0"code>

:descImgUrl="spuDescImgUrl" :descInfo="skuItemInfo"></spu-describes>code>

<!-- 规格与包装-->

<spu-specification

:group-data="skuItemInfo.groupAttrs"code>

v-else-if="showComponentData==1"code>

></spu-specification>

<!-- 商品评价-->

<div v-else-if="showComponentData===2">code>

<div>

<el-image :src="require('@/assets/shouhou.jpg')"></el-image>code>

</div>

</div>

<appraise-list :skuId="skuItemInfo.skuInfo.id" v-else-if="showComponentData===3"/>code>

</div>

</template>

<!-- 骨架屏内容-->

<template #template>

<div style="height: 100%">code>

<el-row :gutter="20">code>

<el-col :span="7" :offset="1">code>

<el-skeleton-item variant="image" style="height: 500px"/>code>

</el-col>

<el-col :span="13" :offset="1">code>

<div>

<el-skeleton-item variant="text" style="width: 100%;height: 40px"/>code>

<el-skeleton-item variant="text" style="width: 100%;height: 40px;margin-top: 20px"/>code>

<div style="margin-top: 30px">code>

<span class="price_class"><el-skeleton-item variant="text" style="height: 30px;width: 80%"/></span>code>

</div>

<div style="margin-top: 150px">code>

<div v-for="item in 2" :key="item">code>

<el-row :gutter="20">code>

<el-col :span="3">code>

<el-skeleton-item variant="text" style="height: 30px"/>code>

</el-col>

<el-col :span="3" v-for="(i) in 3" :key="i">code>

<el-skeleton-item variant="text" style="height: 30px"/>code>

</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数组,计算他们的交集。听起来很简单?我当时想了一会,没弄出来,最后丢给文心一言实现了(文心一言都错了三四次哈哈)。代码如下

说实话我现在也没懂,对算法这一块还处于文盲状态。不过能实现就行!

源码地址

毕业设计:轻松购: 使用vue3+springboot构建的商城系统,集前台用户和后台管理于一体,本项目已经部署在云服务器上, 访问地址: 前台系统:123.207.205.51 后台系统:123.207.205.51:8080 (gitee.com)

icon-default.png?t=N7T8

https://gitee.com/zfb12345/my-graduation-project

总结

sku计算那里,是这个详情页面唯一的难点,其他的部分我就不多说了。如果读者觉得有哪些部分不全,或者想要了解其他部分,随时评论。



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。