uniapp-商城-69-shop(2-商品列表,点击商品展示,商品的详情, vuex的使用,rich-text使用)
该页面将我们的数据进行了系统性罗列;针对单个数据的展示,则仍需进一步开发和完善;当我们点击商品时,则会跳出弹窗界面。
同样这里用一个组件来进行实现该弹窗的展示。
本文详细阐述了商品详情弹窗的实现方案。主要基于Vuex进行状态管理, 由多个核心组件协同运作:
1 商品列表组件productItem响应点击事件,并通过Vuex机制传递关键的商品数据及当前窗口状态信息。
2 商品详情组件pro-detail-popup采用u-popup技术模拟弹窗效果,在页面上呈现商品图片信息以及相应的价格与详细描述内容。
3 利用Vuex架构管理窗口状态(detailPopState)与商品相关信息(detailData),并包含对商品描述的格式化处理功能。
4 通过mapMutations与mapGetters方法建立组件与Vuex的数据交互机制,并保证其间的实时数据一致性。
5 考虑到用户操作中的事件冒泡处理需求以及页面加载性能优化等问题设计方案,并最终实现了完整的点击获取详情流程,
确保系统具有良好的可维护性和扩展性。
1、回顾在shop页面中,存在商品的组件productItem

<!-- 下面是滚动栏目 -->
<!-- :scroll-top="rightScrollValue" 是滚动条位置 后面rightScrollValue是个变量 使用v-bind 就是加:-->
<!-- scroll-with-animation 滑动动画,避免太生硬 -->
<!-- @scroll="rightScrollEnt" 监听右侧的滚动事件 -->
<scroll-view scroll-y="true" class="Conent" :scroll-top="rightScrollValue" scroll-with-animation
@scroll="rightScrollEnt">
<view class="productView" v-for="item,index in datalist" :key="item.id">
<u-sticky customNavHeight=0 zIndex="2">
<!-- 这就是吸顶,但是我们自己取消了导航,需要设置一个值 customNavHeight 导航栏高度,自定义导航栏时,需要传入此值 -->
<view class="producttitle">
<!-- 这里需要吸顶,分类的吸顶 -->
<!-- 使用的是uview的sticky 产品类名,分类的 -->
{{item.name}}
</view>
</u-sticky>
<view class="productcontent" v-for="childrenItem,index2 in item.proGroup"
:key="childrenItem.id">
<view class="productitem">
<productItem :item="childrenItem"></productItem>
</view>
</view>
</view>
</scroll-view>
html

2、productItem组件的基本代码
在代码中进行分析,在其中新增了一个操作名为 showDetail,并对该操作编写相应的处理逻辑
2.1、组件传值(我们使用vuex)传递商品id或者是否显示商品详情的信息
<template>
<view class="pro-item" @click="showDetail">
<!-- 给商品添加一个点击,显示商品的详情 -->
<!-- 但是这里的详情也是shop页面,就是商品组件的父级下的另外一个商品详情组件 pro-detail-popup-->
<!-- 点击这里就要把商品的id信息传给商品详情组件 pro-detail-popup-->
<!-- 这样这里就需要 组件传值的功能,但也可以用状态vuex 来管理传值 ,我们这里就采用了状态传值 -->
<view class="pic">
<!-- 组件的image给一个标签名 不然小程序报错 -->
<image class="img" :src="item.thumb[0].url" mode="aspectFill"></image>
<!-- aspectFill 全部显示 -->
</view>
<view class="text">
<view class="title">
<!-- 产品标题有很多字母,这个时间就需要进行一行显示,不完全的就省略号 -->
{{item.name}}
</view>
<view class="price">
<!-- 没有原价不显示 -->
<view class="big" v-if="item.before_price">
¥{{priceFormat(item.before_price)}}
</view>
<view class="small">
低至¥{{priceFormat(item.price)}}
</view>
</view>
<!-- 没有原价,或者折扣为0的就不用显示折扣 -->
<view class="discount" v-if="item.before_price && discount(item.price,item.before_price)">
{{discount(item.price,item.before_price)}}折
</view>
<view class="numbox" v-if="btnState">
<!-- 数据中没有产品属性sku,那属性长度为0就不显示选规格,显示步进器 -->
<!-- <view class="skuSelect" v-if="item.sku_select.length" @click="selectSpecs">选规格</view> -->
<view class="skuSelect" v-if="item.sku_select.length" @click.stop="selectSpecs">选规格</view>
<!--如果这样写 @click="selectSpecs" -->
<!-- 这里有一个点击事件就是规格这个位置被点击,被选择,执行selectSpecs -->
<!-- 但是这里点击和全局的 showDetail 一起被点击了,就会弹出两个框来,属于事件冒泡了-->
<!-- 所以就加一个stop,阻止事件冒泡 @click.stop="selectSpecs" 就可以阻止全局的事件被点击执行-->
<!-- 这里还有一个问题,就是当在选规格的页面上调用时,pro-select-specs,
用户也可能无意间到这个组件,那一点击还是会执行我们这里的全局点击事件 showDetail
那这里就还需要屏蔽掉 选规格 弹窗上 该组件的点击操作 showDetail
所以 这里的点击操作 showDetail 就带了一个判断 看btnState 是true 就可以点击
如果是false 点击无效,直接返回
-->
<view class="uNum" v-else>
<!-- 步进器 三部分 左中右三部分 -->
<pro-num-box :item="item"></pro-num-box>
</view>
</view>
</view>
</view>
</template>
<script>
const goodsCloudObj = uniCloud.importObject("green-mall-goods", {
"customUI": false //每一次都会有一个提示加载,false 就是显示加载提示弹窗
})
//引入vuex 进行状态传值,这里完成点击这个组件将商品的id传给父级或其他组件
import {
mapMutations
} from "vuex"
import {
priceFormat,
discount
} from "@/utils/tools.js" //导入公共方法,并再方法中声明,就可以直接用了
export default {
name: "productItem",
data() {
return {
};
},
props: {
item: {
type: Object,
// 这里的default,记住不是defaultvalue ,defaultvalue是微信小程序开法工具上使用的。hbuilder上用default!!!!!
// 当然没有默认值 直接删除掉就好了
default: () => {
return {}
//obj 的默认值要用一个方法来获取(){}
}
},
btnState: {
// 选规格按钮的状态,如果是在商品list页面(shop页面)就显示也就是true,
// 如果该 按钮(选规格) 被点击了就改变为不显示,就是false 就是说这个按钮已经被点击过了,也就是点击选规格后 就不显示 该 按钮(选规格)
type: Boolean,
default: true
}
},
methods: {
...mapMutations(["SET_DETAIL_STATE", "SET_DETAIL_DATA", "SET_PRO_SPECS"]),
//声明那个公共方法,变成了this.priceFormat的方法,可以直接用
priceFormat,
discount,
//打开详情,显示商品信息
showDetail() {
// btnState判断就是用来看该组件出现在哪里,是否允许弹出商品详情界面。
// 出现在 选规格 界面就 btnState 就被 pro-select-specs这个组件赋值false ,就不弹出
if (!this.btnState) return;
// 这里可以设计为每次点击 shop页面获取时,不要把什么数据都获取过来浪费流量(那这里就需要改改getlist,使用filed 过滤不需要的字段)
// 这里重新获取商品数据data ,然后异步同步操作后在
// 获取数据就不要加customUI,或者改为false 让他显示加载中
// 然后赋值给SET_DETAIL_DATA
// 最好将这个值保存到缓存,退出登录,就删除这样免得 老是来刷新获取 浪费流量
this.SET_DETAIL_STATE(true)
this.SET_DETAIL_DATA(this.item);
},
//点击选择规格
selectSpecs() {
this.SET_PRO_SPECS(true);
this.SET_DETAIL_DATA(this.item);
console.log(this.item);
}
}
}
</script>
<style lang="scss">
.pro-item {
padding: 25rpx 0;
width: 100%;
//弹性盒
// @include flex-box();
display: flex; //作用,将该区域的内容撑开 占满
.pic {
width: 140rpx;
height: 140rpx;
border-radius: 20rpx;
overflow: hidden;
background: #bbb; //测试用
.img {
//最好给一个属性,不然小程序报错
width: 100%;
height: 100%;
}
}
.text {
position: relative; //它里面有一个绝对的,所以这里的父级就加一个相对得
flex: 1;
padding-left: 25rpx;
// border: 1rpx solid red;
.title {
font-size: 34rpx;
font-weight: bold;
@include ellipsis();
}
.price {
@include flex-box-set(start, end);
font-weight: bold;
padding: 25rpx 0;
.small {
font-size: 34rpx;
color: $brand-theme-color;
}
.big {
font-size: 26rpx;
opacity: 0.4; //透明度
text-decoration: line-through; //中画线
padding-right: 10rpx;
}
}
.discount {
font-size: 20rpx;
color: $brand-theme-color;
padding: 2rpx 10rpx;
border: 1rpx solid $brand-theme-color;
display: inline-block; //行级块元素
border-radius: 7rpx;
}
.numbox {
position: absolute; //这里有一个绝对的,那父级就加一个相对得
right: 0;
bottom: 0;
.skuSelect {
height: 40rpx;
padding: 0 10rpx;
background-color: $brand-theme-color;
color: #fff;
font-size: 28rpx;
}
}
}
}
</style>
html

3、在shop页面我们添加了商品详情的组件pro-detail-popup
该组件通过单击 商品组件 productItem 中的 showDetail 按钮后会打开详情页面 pro-detail-popup
默认状态下,是不弹出,只有点击就弹出
3.1 该组件pro-detail-popup 使用了 uview 的 u-popup 组件
主要原因是该组件提供了配置选项来控制组件的状态。除了自定义配置参数外,并且还自带了一个独立的安全隔离区(iOS设备在 home 区域的安全隔离区)。
弹出窗口展示uni-popup效果时,默认设置为较高z-index值以避免被其他元素覆盖;而uview中的popup组件则提供了更灵活的层级管理功能
注意:
<u-popup :show="detailPopState" @close="onClose" closeable="support" round="10">
3.2 具体代码
<template>
<view class="proDetail">
<!-- 弹窗使用uni-popup可以,但是不能自定义层级,我么这里用uview 中的popup可以默认层级较高 zindex ,不会被其他元素遮挡住-->
<!-- 使用show 来定义是否显示 @close 定义关闭操作-->
<u-popup :show="detailPopState" @close="onClose" closeable round="10">
<!-- show 控制显示
close 控制关闭 点击遮罩改变显示状态
closeable 是在弹窗上显示关闭的 x 号,点击它也可以关闭
round 弹窗的圆角
-->
<view class="detailWrapper" v-if="detailData.name">
<!-- v-if="detailData.name" 保证有数据了才显示,避免报错 -->
<!-- 上中下三部分 top 用来保证 和上面的 closeable 的 x 号 同样高的留白-->
<view class="top"></view>
<view class="body">
<scroll-view class="scrollView" scroll-y>
<!-- y轴滚动 -->
<view class="thumb">
<!-- 缩略图 -->
<image class="img" :src="detailData.thumb[0].url" mode="aspectFill"></image>
</view>
<view class="info">
<view class="title">{{detailData.name}}</view>
<view class="price">
<view class="big">¥{{priceFormat(detailData.price)}}</view>
<view class="small" v-if="detailData.before_price">
¥{{priceFormat(detailData.before_price)}}</view>
<view class="discount"
v-if="detailData.before_price && discount(detailData.price,detailData.before_price)">
{{discount(detailData.price,detailData.before_price)}}折
</view>
</view>
</view>
<view class="detail">
<view class="text">
<view class="title">商品描述</view>
<view class="description">
<rich-text :nodes="detailData.description"></rich-text>
</view>
</view>
<view class="picList">
<view v-for="(item,index) in detailData.thumb" :key="item.url">
<image class="img" :src="item.url" mode="widthFix" v-if="index!=0"></image>
<!-- widthFix 宽度100%,长图自动延撑 -->
</view>
</view>
<view class="intro">以上是全部介绍,欢迎选购</view>
</view>
</scroll-view>
</view>
<view class="footer">
<!-- 下面 有一个按钮 -->
<u-button color="#EC544F" icon="shopping-cart" iconColor="#fff" @click="clickAddCart">加入购物车</u-button>
</view>
</view>
</u-popup>
</view>
</template>
<script>
import {
mapGetters,
mapMutations
} from "vuex"
import {
priceFormat,
discount
} from "@/utils/tools.js"
export default {
name: "pro-detail-popup",
data() {
return {
};
},
computed: {
...mapGetters(["detailPopState", "detailData"])
},
methods: {
...mapMutations(["SET_DETAIL_STATE","SET_PRO_SPECS"]),
priceFormat,
discount,
onClose() {
this.SET_DETAIL_STATE(false)
},
//点击加入购物车
clickAddCart(){
this.SET_PRO_SPECS(true)
}
}
}
</script>
<style lang="scss" scoped>
.detailWrapper {
// height: 400rpx;
height: 85vh; //默认显示全屏(vh)的 85%
.top {
height: 80rpx;
width: 100%;
}
.body {
height: calc(100% - 220rpx);
// 高度计算 就是 全部高度减去80+140
.scrollView {
// 100% 保证内容在这里面显示 就是可以在这里滚动
height: 100%;
padding: 0 30rpx;
.thumb {
width: 690rpx;
height: 690rpx;
.img {
//在内部撑开,小图片如果,不定义100%就是靠左边,未撑开
width: 100%;
height: 100%;
}
}
.info {
padding: 20rpx 0;
border-bottom: 1px solid $border-color-light;
.title {
font-size: 40rpx;
font-weight: bold;
}
.price {
display: flex;
align-items: center;
padding-top: 20rpx;
.big {
font-size: 46rpx;
font-weight: bold;
color: $brand-theme-color;
}
.small {
font-size: 28rpx;
color: $text-font-color-3;
font-weight: bold;
text-decoration: line-through;
margin-left: 10rpx;
}
.discount {
border: 1px solid $brand-theme-color;
color: $brand-theme-color;
font-size: 22rpx;
padding: 2rpx 20rpx;
margin-left: 10rpx;
border-radius: 8rpx;
}
}
}
.detail {
.text {
padding: 20rpx 0;
.title {
font-size: 32rpx;
font-weight: bold;
}
.description {
padding: 20rpx 0;
line-height: 1.7em;
}
}
.picList {
.img {
width: 100%;
margin-bottom: 30rpx;
}
}
.intro {
padding: 40rpx 0;
font-size: 30rpx;
color: $text-font-color-3;
text-align: center;
}
}
}
}
.footer {
// top + body +footer 就三部分 body高度 就是全部(85vh)-220
height: 140rpx;
border-top: 1px solid $border-color-light;
@include flex-box-set(); // 居中显示
padding: 0 200rpx; //这是 上下 0 ,左右边距200rpx
}
}
</style>
html

3.3 如何获取弹出标识?
利用两个 productItem 组件将跳出标记及其所需数据传递给三个 pro-detail-popup组件。
如何传呢,我们就定义一个 公共变量 good ,
productItem在被点击之后会传递相应的值到good。同时, pro-detail-popup也会请求这个标识和数据。
这个公共数据是如何获得的?它来源于组件传递值或者采用Vuex的状态管理机制传入的。我们采用了Vuex的状态管理机制。
3.3.1 第一步:为项目搭建存储库文件夹(无需重复搭建),并创建模块化目录结构(无需重复构建)。

3.3.2 第二步:在modules 中,建立goods.js文件
在文件中,定义state,mutations;
state 定义 弹出状态detailPopState
mutations 定义同步动作
SET_DETAIL_STATE 弹出状态
SET_DETAIL_DATA 商品详情数据
// 操作商品的vuex
//vuex的3步骤: 1 创建good是.js;2 在store的index 引入;3 在getter中暴露
const goods = {
state:{
detailPopState:false, //商品详情页弹窗的状态,false 默认是关闭不弹出
detailData:{}, //商品详情的数据
proSpecsState:false //商品规格选择的状态是否弹出,false 就是不弹出
},
mutations:{
SET_DETAIL_STATE(state,value){
state.detailPopState = value
},
SET_DETAIL_DATA(state,value){
// 使用正则把文本中的换行,替换为<br/>
// 页面就可以用rich-text来显示了
value.description = value.description.replace(/\n/g,"<br/>");
state.detailData=value
},
SET_PRO_SPECS(state,value){
state.proSpecsState = value
}
}
}
export default goods
javascript

3.3.3 第三步:导入store 的 index(以前建过 index.js就不用在创建)
导入 goods;定义modules中的对象 goods
// import createPersistedState from 'vuex-persistedstate' // 引入数据持久化插件
import Vue from "vue"
import Vuex from "vuex"
Vue.use(Vuex) //再vue安装vuex
import system from "@/store/modules/system.js"
//上面三个必须写 ,创建getters.js 并导入 但是getters.js 中是暴露 modules 中js 的state 便于页面使用
import getters from "./getters" //导入getter 然后再去使用的页面vue 导入getters import {mapState,mapMutations,mapGetters} from "vuex"
import cars from "@/store/modules/cars.js"
import brand from "@/store/modules/brand.js"
import goods from "@/store/modules/goods.js"
const store = new Vuex.Store({
getters, //实例化 getters 不然vue页面用不了
modules: {
system,
cars,
brand,
goods
},
plugins: [
createPersistedState({
paths: ['cars'],
storage: { // 存储方式定义
getItem: (key) => uni.getStorageSync(key), // 获取
setItem: (key, value) => uni.setStorageSync(key, value), // 存储
removeItem: (key) => uni.removeStorageSync(key) // 删除
}
})
]
})
export default store;
javascript

3.3.4 第四步:展示 goods 中必须进行数据计算的部分,并非所有变量都需直接暴露
这里暴露了是否需要弹出详情的 变量 : detailPopState
//通过这里将所有store的js文件的state 暴露后转发出去
// 作用:类似于过滤器,数据输出之前可以操作数据。
// getters 属性和 state属性平级,可以过滤state中的数据。
const getters = {
StatusBarHeight: state => state.system.StatusBarHeight,
TitleBarHeight: state => state.system.TitleBarHeight,
foldState: state => state.system.foldState, //是否折叠
bodyBarHeight: state => {
if (state.system.foldState) return state.system.TitleBarHeight;
return 100;
},
totalHeight: state => {
if (state.system.foldState) return state.system.StatusBarHeight + state.system.TitleBarHeight + 10;
return state.system.StatusBarHeight + state.system.TitleBarHeight + 100 + 10;
},
carsList: state => state.cars.carsList,
//获取数组的总价 数组求和用的是reduce [1,2,3] return prev+=next prev是上一个值,next是下一个值
totalPrice: state => {
return state.cars.carsList.reduce((prev, next) => {
return prev += next.price * next.numvalue
}, 0)
//0 是初始值 prev,next对象的price值,一直加到最后
},
totalNumValue: state => {
return state.cars.carsList.reduce((prev, next) => {
return prev += next.numvalue
}, 0)
},
brandData: state => state.brand.brandData, //暴露接口,主要是通过这里做一些计算,然后暴露出去,当然这里没有做计算。
detailPopState: state => state.goods.detailPopState,
detailData: state => state.goods.detailData,
proSpecsState: state => state.goods.proSpecsState
}
export default getters;
javascript

4 商品组件 productItem 中对弹出状态的处理
代码见 2 章节
4.1 引入 vuex的mapmutation
// 导入vuex库以实现状态传输
当组件被点击时会将商品ID传递给父组件或其他相关组件
import { mapMutations } from "vuex"
4.2 导入mapmutation中的方法
methods: {
...mapMutations(["SET_DETAIL_STATE", "SET_DETAIL_DATA", "SET_PRO_SPECS"]),
4.3 点击组件的处理 showDetail
此函数用于打开商品详情页面并展示相关信息。
此函数通过btnState变量来判断当前组件的位置以及是否允许弹出商品详情页面。具体来说:
- 当处于选规格界面时,则btnState会被pro-select-specs组件设置为false从而阻止弹出。
- 如果btnState未被设置则直接返回执行其他操作
// 在每次访问shop页面时,
// 现在应改进getlist,
// 使用field过滤不需要的字段。
// 现在重新读取商品数据data,
// 然后进行异步同步操作后,
// 在完成数据读取后就不再设置customUI,
// 或者将其设置为false以显示加载中状态。
// 最好将这个结果保存到缓存中,
// 用户退出登录后删除该缓存项,
// 从而避免频繁刷新获取数据以节省流量。
this.SET_DETAIL_STATE(true)
this.SET_DETAIL_DATA(this.item);
},
5、商品详情 pro-detail-popup 组件的处理
详细代码见 3.2
5.1 导入vuex,获取状态
import {
mapGetters,
mapMutations
} from "vuex"
5.2 解构vuex 的数据和方法

5.3 是否弹出,由于点击了 productItem 组件的处理 showDetail
通过vuex 获取的 detailPopState 是true,需要弹窗

5.4 其中数据都来至于 vuex 的 detailData,
vuex 的 detailData又来自于4.3 的处理。
5.5 商品描述 rich-text 使用
需要在goods.js中做一个\n 替换为
,保证前端页面显示的正确
后端在添加商品时间时,在存在换行符的情况下也会一并被保存下来;而前端展示则需要重新调整格式。
我们这里使用了 rich-text ,所以在vuex中就做了一个正则替换。

