data() {
return {
// 请求参数对象
queryObj: {
// 查询关键词
query: '',
// 商品分类Id
cid: '',
// 页码值
pagenum: 1,
// 每页显示多少条数据
pagesize: 10
}
}
}
onLoad(options) {
// 将页面参数转存到 this.queryObj 对象中
this.queryObj.query = options.query || ''
this.queryObj.cid = options.cid || ''
}
data() {
return {
// 商品列表的数据
goodsList: [],
// 总数量,用来实现分页
total: 0
}
}
onLoad(options) {
// 调用获取商品列表数据的方法
this.getGoodsList()
}
methods: {
// 获取商品列表数据的方法
async getGoodsList() {
// 发起请求
const { data: res } = await uni.$http.get('/api/public/v1/goods/search', this.queryObj)
if (res.meta.status !== 200) return uni.$showMsg()
// 为数据赋值
this.goodsList = res.message.goods
this.total = res.message.total
}
}
<template>
<view>
<view class="goods-list">
<block v-for="(goods, i) in goodsList" :key="i">
<view class="goods-item">
<!-- 商品左侧图片区域 -->
<view class="goods-item-left"> // 在这里使用两个图片做保险
<image :src="goods.goods_small_logo || defaultPic" class="goods-pic"></image>
</view>
<!-- 商品右侧信息区域 -->
<view class="goods-item-right">
<!-- 商品标题 -->
<view class="goods-name">{{goods.goods_name}}</view>
<view class="goods-info-box">
<!-- 商品价格 -->
<view class="goods-price">¥{{goods.goods_price}}</view>
</view>
</view>
</view>
</block>
</view>
</view>
</template>
data() {
return {
// 默认的空图片
defaultPic: 'https://img3.doubanio.com/f/movie/8dd0c794499fe925ae2ae89ee30cd225750457b4/pics/movie/celebrity-default-medium.png'
}
}
.goods-item {
display: flex;
padding: 10px 5px;
border-bottom: 1px solid #f0f0f0;
.goods-item-left {
margin-right: 5px;
.goods-pic {
width: 100px;
height: 100px;
display: block;
}
}
.goods-item-right {
display: flex;
flex-direction: column;
justify-content: space-between;
.goods-name {
font-size: 13px;
}
.goods-price {
font-size: 16px;
color: #c00000;
}
}
}
<template>
<view class="goods-item">
<!-- 商品左侧图片区域 -->
<view class="goods-item-left">
<image :src="goods.goods_small_logo || defaultPic" class="goods-pic"></image>
</view>
<!-- 商品右侧信息区域 -->
<view class="goods-item-right">
<!-- 商品标题 -->
<view class="goods-name">{{goods.goods_name}}</view>
<view class="goods-info-box">
<!-- 商品价格 -->
<view class="goods-price">¥{{goods.goods_price}}</view>
</view>
</view>
</view>
</template>
<script>
export default {
// 定义 props 属性,用来接收外界传递到当前组件的数据
props: {
// 商品的信息对象
goods: {
type: Object,
defaul: {},
},
},
data() {
return {
// 默认的空图片
defaultPic: 'https://img3.doubanio.com/f/movie/8dd0c794499fe925ae2ae89ee30cd225750457b4/pics/movie/celebrity-default-medium.png',
}
},
}
</script>
<style lang="scss">
.goods-item {
display: flex;
padding: 10px 5px;
border-bottom: 1px solid #f0f0f0;
.goods-item-left {
margin-right: 5px;
.goods-pic {
width: 100px;
height: 100px;
display: block;
}
}
.goods-item-right {
display: flex;
flex-direction: column;
justify-content: space-between;
.goods-name {
font-size: 13px;
}
.goods-price {
font-size: 16px;
color: #c00000;
}
}
}
</style>
<view class="goods-list">
<block v-for="(item, i) in goodsList" :key="i">
<!-- 为 my-goods 组件动态绑定 goods 属性的值 -->
<my-goods :goods="item"></my-goods>
</block>
</view>
filters: {
// 把数字处理为带两位小数点的数字
tofixed(num) {
return Number(num).toFixed(2)
}
}
<!-- 商品价格 -->
<view class="goods-price">¥{{goods.goods_price | tofixed}}</view>
"subPackages": [
{
"root": "subpkg",
"pages": [
{
"path": "goods_detail/goods_detail",
"style": {}
},
{
"path": "goods_list/goods_list",
"style": {
"onReachBottomDistance": 150 // 就这一行代码
}
},
{
"path": "search/search",
"style": {}
}
]
}
]
// 触底的事件
onReachBottom() {
// 让页码值自增 +1
this.queryObj.pagenum += 1
// 重新获取列表数据
this.getGoodsList()
}
// 获取商品列表数据的方法
async getGoodsList() {
// 发起请求
const { data: res } = await uni.$http.get('/api/public/v1/goods/search', this.queryObj)
if (res.meta.status !== 200) return uni.$showMsg()
// 为数据赋值:通过展开运算符的形式,进行新旧数据的拼接
this.goodsList = [...this.goodsList, ...res.message.goods]
this.total = res.message.total
}
data() {
return {
// 是否正在请求数据
isloading: false
}
}
// 获取商品列表数据的方法
async getGoodsList() {
// ** 打开节流阀
this.isloading = true
// 发起请求
const { data: res } = await uni.$http.get('/api/public/v1/goods/search', this.queryObj)
// ** 关闭节流阀 这个 就放在 请求数据的 正 下面 ,
this.isloading = false
// 省略其它代码...
}
// 触底的事件
onReachBottom() {
// 判断是否正在请求其它数据,如果是,则不发起额外的请求
if (this.isloading) return
this.queryObj.pagenum += 1
this.getGoodsList()
}
当前的页码值 * 每页显示多少条数据 >= 总数条数
pagenum * pagesize >= total
// 触底的事件
onReachBottom() {
// 判断是否还有下一页数据 就这一行代码
if (this.queryObj.pagenum * this.queryObj.pagesize >= this.total) return uni.$showMsg('数据加载完毕!')
// 判断是否正在请求其它数据,如果是,则不发起额外的请求
if (this.isloading) return
this.queryObj.pagenum += 1
this.getGoodsList()
}
"subPackages": [{
"root": "subpkg",
"pages": [{
"path": "goods_detail/goods_detail",
"style": {}
}, {
"path": "goods_list/goods_list",
"style": {
"onReachBottomDistance": 150,
"enablePullDownRefresh": true, // 就这 两行
"backgroundColor": "#F8F8F8" // 代码
}
}, {
"path": "search/search",
"style": {}
}]
}]
// 下拉刷新的事件
onPullDownRefresh() {
// 1. 重置关键数据
this.queryObj.pagenum = 1
this.total = 0
this.isloading = false
this.goodsList = []
// 2. 重新发起请求 后面的回调函数, 是关闭下路刷新的弹窗(这个只能手动关闭)
this.getGoodsList(() => uni.stopPullDownRefresh())
}
// 获取商品列表数据的方法
async getGoodsList(cb) {
this.isloading = true
const { data: res } = await uni.$http.get('/api/public/v1/goods/search', this.queryObj)
this.isloading = false
// 只要数据请求完毕,就立即按需调用 cb 回调函数 (这个代码是表示, 必须要有 cb 才会去执行 cb() 这个回调函数)
cb && cb()
if (res.meta.status !== 200) return uni.$showMsg()
this.goodsList = [...this.goodsList, ...res.message.goods]
this.total = res.message.total
}
<view class="goods-list">
<view v-for="(item, i) in goodsList" :key="i" @click="gotoDetail(item)">
<!-- 为 my-goods 组件动态绑定 goods 属性的值 -->
<my-goods :goods="item"></my-goods>
</view>
</view>
// 点击跳转到商品详情页面
gotoDetail(item) {
uni.navigateTo({
url: '/subpkg/goods_detail/goods_detail?goods_id=' + item.goods_id
})
}
data() {
return {
// 商品详情对象
goods_info: {}
}
}
onLoad(options) {
// 获取商品 Id
const goods_id = options.goods_id
// 调用请求商品详情数据的方法
this.getGoodsDetail(goods_id)
}
methods: {
// 定义请求商品详情数据的方法
async getGoodsDetail(goods_id) {
const { data: res } = await uni.$http.get('/api/public/v1/goods/detail', { goods_id })
if (res.meta.status !== 200) return uni.$showMsg()
// 为 data 中的数据赋值
this.goods_info = res.message
}
}
<!-- 轮播图区域 -->
<swiper :indicator-dots="true" :autoplay="true" :interval="3000" :duration="1000" :circular="true">
<swiper-item v-for="(item, i) in goods_info.pics" :key="i">
<image :src="item.pics_big"></image>
</swiper-item>
</swiper>
swiper {
height: 750rpx;
image {
width: 100%;
height: 100%;
}
}
<swiper-item v-for="(item, i) in goods_info.pics" :key="i">
<!-- 把当前点击的图片的索引,传递到 preview() 处理函数中 -->
<image :src="item.pics_big" @click="preview(i)"></image>
</swiper-item>
// 实现轮播图的预览效果
preview(i) {
// 调用 uni.previewImage() 方法预览图片
uni.previewImage({
// 预览时,默认显示图片的索引
current: i,
// 所有图片 url 地址的数组
urls: this.goods_info.pics.map(x => x.pics_big)
})
}
<!-- 商品信息区域 -->
<view class="goods-info-box">
<!-- 商品价格 -->
<view class="price">¥{{goods_info.goods_price}}</view>
<!-- 信息主体区域 -->
<view class="goods-info-body">
<!-- 商品名称 -->
<view class="goods-name">{{goods_info.goods_name}}</view>
<!-- 收藏 -->
<view class="favi">
<uni-icons type="star" size="18" color="gray"></uni-icons>
<text>收藏</text>
</view>
</view>
<!-- 运费 -->
<view class="yf">快递:免运费</view>
</view>
// 商品信息区域的样式
.goods-info-box {
padding: 10px;
padding-right: 0;
.price {
color: #c00000;
font-size: 18px;
margin: 10px 0;
}
.goods-info-body {
display: flex;
justify-content: space-between;
.goods-name {
font-size: 13px;
padding-right: 10px;
}
// 收藏区域
.favi {
width: 120px;
font-size: 12px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border-left: 1px solid #efefef;
color: gray;
}
}
// 运费
.yf {
margin: 10px 0;
font-size: 12px;
color: gray;
}
}
<!-- 商品详情信息 -->
<rich-text :nodes="goods_info.goods_introduce"></rich-text>
// 定义请求商品详情数据的方法
async getGoodsDetail(goods_id) {
const { data: res } = await uni.$http.get('/api/public/v1/goods/detail', { goods_id })
if (res.meta.status !== 200) return uni.$showMsg()
// 使用字符串的 replace() 方法,为 img 标签添加行内的 style 样式,从而解决图片底部空白间隙的问题
res.message.goods_introduce = res.message.goods_introduce.replace(//g, ')
this.goods_info = res.message
}
// 定义请求商品详情数据的方法
async getGoodsDetail(goods_id) {
const { data: res } = await uni.$http.get('/api/public/v1/goods/detail', { goods_id })
if (res.meta.status !== 200) return uni.$showMsg()
// 使用字符串的 replace() 方法,将 webp 的后缀名替换为 jpg 的后缀名
res.message.goods_introduce = res.message.goods_introduce.replace(//g, ').replace(/webp/g, 'jpg')
this.goods_info = res.message
}
导致问题的原因:在商品详情数据请求回来之前,data 中 goods_info 的值为 { },因此初次渲染页面时,会导致 商品价格、商品名称 等闪烁的问题。
解决方案:判断 goods_info.goods_name 属性的值是否存在,从而使用 v-if 指令控制页面的显示与隐藏:
<template>
<view v-if="goods_info.goods_name">
<!-- 省略其它代码 -->
</view>
</template>
基于 uni-ui 提供的 GoodsNav 组件来实现商品导航区域的效果
data() {
return {
// 商品详情对象
goods_info: {},
// 左侧按钮组的配置对象
options: [{
icon: 'shop',
text: '店铺'
}, {
icon: 'cart',
text: '购物车',
info: 2
}],
// 右侧按钮组的配置对象
buttonGroup: [{
text: '加入购物车',
backgroundColor: '#ff0000',
color: '#fff'
},
{
text: '立即购买',
backgroundColor: '#ffa200',
color: '#fff'
}
]
}
}
<!-- 商品导航组件 -->
<view class="goods_nav">
<!-- fill 控制右侧按钮的样式 -->
<!-- options 左侧按钮的配置项 -->
<!-- buttonGroup 右侧按钮的配置项 -->
<!-- click 左侧按钮的点击事件处理函数 -->
<!-- buttonClick 右侧按钮的点击事件处理函数 -->
<uni-goods-nav :fill="true" :options="options" :buttonGroup="buttonGroup" @click="onClick" @buttonClick="buttonClick" />
</view>
.goods-detail-container {
// 给页面外层的容器,添加 50px 的内padding,
// 防止页面内容被底部的商品导航组件遮盖
padding-bottom: 50px;
}
.goods_nav {
// 为商品导航组件添加固定定位
position: fixed;
bottom: 0;
left: 0;
width: 100%;
}
// 左侧按钮的点击事件处理函数
onClick(e) {
console.log(e)
}
// 左侧按钮的点击事件处理函数
onClick(e) {
if (e.content.text === '购物车') {
// 切换到购物车页面
uni.switchTab({
url: '/pages/cart/cart'
})
}
}
// 1. 导入 Vue 和 Vuex
import Vue from 'vue'
import Vuex from 'vuex'
// 2. 将 Vuex 安装为 Vue 的插件
Vue.use(Vuex)
// 3. 创建 Store 的实例对象
const store = new Vuex.Store({
// TODO:挂载 store 模块
modules: {},
})
// 4. 向外共享 Store 的实例对象
export default store
// 1. 导入 store 的实例对象
import store from './store/store.js'
// 省略其它代码...
const app = new Vue({
...App,
// 2. 将 store 挂载到 Vue 实例上
store,
})
app.$mount()
export default {
// 为当前模块开启命名空间
namespaced: true,
// 模块的 state 数据
state: () => ({
// 购物车的数组,用来存储购物车中每个商品的信息对象
// 每个商品的信息对象,都包含如下 6 个属性:
// { goods_id, goods_name, goods_price, goods_count, goods_small_logo, goods_state }
cart: [],
}),
// 模块的 mutations 方法
mutations: {},
// 模块的 getters 属性
getters: {},
}
import Vue from 'vue'
import Vuex from 'vuex'
// 1. 导入购物车的 vuex 模块
import moduleCart from './cart.js'
Vue.use(Vuex)
const store = new Vuex.Store({
// TODO:挂载 store 模块
modules: {
// 2. 挂载购物车的 vuex 模块,模块内成员的访问路径被调整为 m_cart,例如:
// 购物车模块中 cart 数组的访问路径是 m_cart/cart
m_cart: moduleCart,
},
})
export default store
// 从 vuex 中按需导出 mapState 辅助方法
import { mapState } from 'vuex'
export default {
computed: {
// 调用 mapState 方法,把 m_cart 模块中的 cart 数组映射到当前页面中,作为计算属性来使用
// ...mapState('模块的名称', ['要映射的数据名称1', '要映射的数据名称2'])
...mapState('m_cart', ['cart']),
},
// 省略其它代码...
}
注意:今后无论映射 mutations 方法,还是 getters 属性,还是 state 中的数据,都需要指定模块的名称,才能进行映射。
<!-- 运费 -->
<view class="yf">快递:免运费 -- {{cart.length}}</view>
export default {
// 为当前模块开启命名空间
namespaced: true,
// 模块的 state 数据
state: () => ({
// 购物车的数组,用来存储购物车中每个商品的信息对象
// 每个商品的信息对象,都包含如下 6 个属性:
// { goods_id, goods_name, goods_price, goods_count, goods_small_logo, goods_state }
cart: [],
}),
// 模块的 mutations 方法
mutations: {
addToCart(state, goods) {
// 根据提交的商品的Id,查询购物车中是否存在这件商品
// 如果不存在,则 findResult 为 undefined;否则,为查找到的商品信息对象
const findResult = state.cart.find((x) => x.goods_id === goods.goods_id)
if (!findResult) {
// 如果购物车中没有这件商品,则直接 push
state.cart.push(goods)
} else {
// 如果购物车中有这件商品,则只更新数量即可
findResult.goods_count++
}
},
},
// 模块的 getters 属性
getters: {},
}
// 按需导入 mapMutations 这个辅助方法
import { mapMutations } from 'vuex'
export default {
methods: {
// 把 m_cart 模块中的 addToCart 方法映射到当前页面使用
...mapMutations('m_cart', ['addToCart']),
},
}
// 右侧按钮的点击事件处理函数
buttonClick(e) {
// 1. 判断是否点击了 加入购物车 按钮
if (e.content.text === '加入购物车') {
// 2. 组织一个商品的信息对象
const goods = {
goods_id: this.goods_info.goods_id, // 商品的Id
goods_name: this.goods_info.goods_name, // 商品的名称
goods_price: this.goods_info.goods_price, // 商品的价格
goods_count: 1, // 商品的数量
goods_small_logo: this.goods_info.goods_small_logo, // 商品的图片
goods_state: true // 商品的勾选状态
}
// 3. 通过 this 调用映射过来的 addToCart 方法,把商品信息对象存储到购物车中
this.addToCart(goods)
}
}
// 模块的 getters 属性
getters: {
// 统计购物车中商品的总数量
total(state) {
let c = 0
// 循环统计商品的数量,累加到变量 c 中
state.cart.forEach(goods => c += goods.goods_count)
return c
}
}
// 按需导入 mapGetters 这个辅助方法
import { mapGetters } from 'vuex'
export default {
computed: {
// 把 m_cart 模块中名称为 total 的 getter 映射到当前页面中使用
...mapGetters('m_cart', ['total']),
},
}
export default {
watch: {
// 1. 监听 total 值的变化,通过第一个形参得到变化后的新值
total(newVal) {
// 2. 通过数组的 find() 方法,找到购物车按钮的配置对象
const findResult = this.options.find((x) => x.text === '购物车')
if (findResult) {
// 3. 动态为购物车按钮的 info 属性赋值
findResult.info = newVal
}
},
},
}
// 将购物车中的数据持久化存储到本地
saveToStorage(state) {
uni.setStorageSync('cart', JSON.stringify(state.cart))
}
addToCart(state, goods) {
// 根据提交的商品的Id,查询购物车中是否存在这件商品
// 如果不存在,则 findResult 为 undefined;否则,为查找到的商品信息对象
const findResult = state.cart.find(x => x.goods_id === goods.goods_id)
if (!findResult) {
// 如果购物车中没有这件商品,则直接 push
state.cart.push(goods)
} else {
// 如果购物车中有这件商品,则只更新数量即可
findResult.goods_count++
}
// 通过 commit 方法,调用 m_cart 命名空间下的 saveToStorage 方法
this.commit('m_cart/saveToStorage')
}
// 模块的 state 数据
state: () => ({
// 购物车的数组,用来存储购物车中每个商品的信息对象
// 每个商品的信息对象,都包含如下 6 个属性:
// { goods_id, goods_name, goods_price, goods_count, goods_small_logo, goods_state }
cart: JSON.parse(uni.getStorageSync('cart') || '[]')
}),
watch: {
// 页面首次加载完毕后,不会调用这个侦听器
total(newVal) {
const findResult = this.options.find(x => x.text === '购物车')
if (findResult) {
findResult.info = newVal
}
}
}
watch: {
// 定义 total 侦听器,指向一个配置对象
total: {
// handler 属性用来定义侦听器的 function 处理函数
handler(newVal) {
const findResult = this.options.find(x => x.text === '购物车')
if (findResult) {
findResult.info = newVal
}
},
// immediate 属性用来声明此侦听器,是否在页面初次加载完毕后立即调用
immediate: true
}
}
需求描述:从商品详情页面导航到购物车页面之后,需要为 tabBar 中的购物车动态设置数字徽标。
// 按需导入 mapGetters 这个辅助方法
import { mapGetters } from 'vuex'
export default {
data() {
return {}
},
computed: {
// 将 m_cart 模块中的 total 映射为当前页面的计算属性
...mapGetters('m_cart', ['total']),
},
}
onShow() {
// 在页面刚展示的时候,设置数字徽标
this.setBadge()
}
methods: {
setBadge() {
// 调用 uni.setTabBarBadge() 方法,为购物车设置右上角的徽标
uni.setTabBarBadge({
index: 2, // 索引
text: this.total + '' // 注意:text 的值必须是字符串,不能是数字
})
}
}
注意:除了要在 cart.vue 页面中设置购物车的数字徽标,还需要在其它 3 个 tabBar 页面中,为购物车设置数字徽标。
此时可以使用 Vue 提供的 mixins 特性,提高代码的可维护性。
import { mapGetters } from 'vuex'
// 导出一个 mixin 对象
export default {
computed: {
...mapGetters('m_cart', ['total']),
},
onShow() {
// 在页面刚展示的时候,设置数字徽标
this.setBadge()
},
methods: {
setBadge() {
// 调用 uni.setTabBarBadge() 方法,为购物车设置右上角的徽标
uni.setTabBarBadge({
index: 2,
text: this.total + '', // 注意:text 的值必须是字符串,不能是数字
})
},
},
}
// 导入自己封装的 mixin 模块
import badgeMix from '@/mixins/tabbar-badge.js'
export default {
// 将 badgeMix 混入到当前的页面中进行使用
mixins: [badgeMix],
// 省略其它代码...
}