前两篇咱们把环境跑通了,也画了一个能看的购物首页——轮播图、分类、商品列表,该有的皮相都有了。但那个页面有个致命问题:它是个“死”的。你点啥都没反应,加购按钮是摆设,搜索框是个装饰品。今天就来让这个页面真正动起来。
这篇的核心只有两个:计算属性怎么让代码变聪明,以及组件拆了之后父子怎么沟通。全程用购物首页当试验田,边改边讲。
01 先加个搜索框,顺便把事件绑定捡回来
购物页没个搜索框,就跟方便面没调料包一样,缺少灵魂。咱们在轮播图上面加一个,顺带复习事件处理。
在 scroll-view 里,轮播图前面塞个搜索框:
<!-- 搜索栏 --><view class="search-bar"> <input class="search-input" type="text" placeholder="搜你爱吃的零食" v-model="searchText" @confirm="onSearch" /></view>样式随手写一下,别太丑:
.search-bar { padding: 20rpx; background: #fff;}.search-input { width: 100%; height: 70rpx; background: #f5f5f5; border-radius: 35rpx; padding: 0 30rpx; font-size: 28rpx;}JS 部分:
data() { return { searchText: '', // ...之前的数据 }},methods: { onSearch() { if (!this.searchText.trim()) { uni.showToast({ title: '输入点啥吧', icon: 'none' }) return } // 假装在搜,后面接真实接口 uni.showToast({ title: '搜一下:' + this.searchText, icon: 'none' }) }}几个你可能没注意到的小点:
• v-model双向绑定输入框的值,searchText会随用户输入实时更新,但别用它做实时搜索(太耗性能),一般用@confirm点键盘完成键再触发。• @confirm是input的一个事件,用户点键盘上的“完成”或“搜索”按钮时触发,比@input聪明。• uni.showToast出个提示,图标设为'none'就是纯文字,没有勾勾叉叉,适合这类轻提示。
现在你在手机或浏览器里敲个词点搜索,能看到弹出来的土司提示,死页开始有反应了。
02 给商品加个“加入购物车”按钮,顺便学会阻止冒泡
列表里每个商品卡片右下角加个按钮,点击后把商品加入购物车。这个需求看似简单,但有个经典坑:点击按钮时,页面跳转到详情的事件也触发了,因为按钮在卡片内部,事件会冒泡。
先给模板加上按钮和点击事件:
<view class="goods-item" v-for="item in goods" :key="item.id" @click="goDetail(item.id)"> <image :src="item.pic" mode="aspectFill" class="goods-pic"></image> <view class="goods-info"> <text class="goods-title">{{ item.title }}</text> <view class="goods-bottom"> <text class="goods-price">¥{{ item.price }}</text> <button class="add-btn" @click.stop="addToCart(item)">+</button> </view> </view></view>注意 .stop 修饰符,它能阻止事件冒泡。没有它,你点加购按钮,父级 @click 也会触发,结果就是加购的同时跳转了详情,体验稀碎。
样式微调一下,让价格和按钮并排:
.goods-bottom { display: flex; justify-content: space-between; align-items: center;}.add-btn { width: 60rpx; height: 60rpx; border-radius: 50%; background: #e6533a; color: #fff; font-size: 36rpx; line-height: 60rpx; text-align: center; padding: 0; margin: 0;}JS 里加上两个方法:
methods: { goDetail(id) { uni.showToast({ title: '去详情:' + id, icon: 'none' }) }, addToCart(item) { uni.showToast({ title: '加了' + item.title, icon: 'success' }) // 待会这里会升级,先放个钩子 }}这时候你点击“+”按钮,只会弹出“加了xxx”,不会触发详情跳转,.stop 功不可没。
03 搞个购物车数组,计算属性算出总数和总价
购物车可不是简单弹个提示就完事了,得有真实数据。我们在 data 里加个 cart 数组,每个元素存商品信息和数量。然后把 addToCart 方法完善:
data() { return { // ...之前的一堆 cart: [] }},methods: { addToCart(item) { // 先看看购物车里有没有这个商品 const existed = this.cart.find(c => c.id === item.id) if (existed) { existed.count++ } else { // 没找到就推进去,数量初始为1 this.cart.push({ id: item.id, title: item.title, price: item.price, pic: item.pic, count: 1 }) } uni.showToast({ title: '已加入购物车', icon: 'success' }) }}现在每次点“+”,购物车数组就会变化。但是页面上看不到任何反馈。咱们在页面底部加一个购物车角标,显示总数量,用计算属性来做。
在 scroll-view 末尾,商品列表下面加个浮动的购物车图标(简单写个条):
<!-- 底部购物车条 --><view class="cart-bar" v-if="cartTotal.count > 0"> <text>🛒 {{ cartTotal.count }} 件商品</text> <text class="cart-total-price">¥{{ cartTotal.price }}</text></view>计算属性 cartTotal :
computed: { cartTotal() { let count = 0 let price = 0 this.cart.forEach(item => { count += item.count price += item.count * item.price }) return { count, price: price.toFixed(2) } }}computed 的好处是什么?只要 cart 数组变了,cartTotal 就会自动重新计算,页面上的数量和总价都会实时更新。你不用手动写一堆刷新逻辑,Vue 帮你盯梢。
样式让购物车条浮在底部:
.cart-bar { position: sticky; bottom: 0; left: 0; width: 100%; height: 100rpx; background: #fff; display: flex; justify-content: space-between; align-items: center; padding: 0 30rpx; box-shadow: 0 -2rpx 10rpx rgba(0,0,0,0.05);}.cart-total-price { color: #e6533a; font-weight: bold; font-size: 36rpx;}注意 v-if="cartTotal.count > 0",购物车为空时这条条自动隐藏,避免占地方。
现在你点加购,底部立马出数字和总价,再点同一个商品数量往上蹦,总价跟着涨。这个“活”的感觉是不是立刻出来了?
04 把商品卡片拆成子组件,父子通信练手
首页的 goods-item 写了一大坨,如果后面详情页、搜索结果页也要复用这个卡片,你难道每个页面拷一遍代码?太傻了。正确的做法是拆成一个独立组件。
在 components 文件夹下新建 GoodsCard.vue,把商品卡片的模板、样式、部分逻辑搬过去:
components/GoodsCard.vue
<template> <view class="goods-item" @click="goDetail"> <image :src="item.pic" mode="aspectFill" class="goods-pic"></image> <view class="goods-info"> <text class="goods-title">{{ item.title }}</text> <view class="goods-bottom"> <text class="goods-price">¥{{ item.price }}</text> <button class="add-btn" @click.stop="handleAdd">+</button> </view> </view> </view></template><script>export default { // 从父组件接收商品数据 props: { item: { type: Object, required: true } }, methods: { goDetail() { // 告诉父组件:我被点击了,带上id this.$emit('go-detail', this.item.id) }, handleAdd() { // 告诉父组件:有人要加购 this.$emit('add-cart', this.item) } }}</script><style scoped>/* 样式原封不动搬过来,加个 scoped 避免污染 */</style>这里两个关键的通信方式:
• props父传子:父组件通过:item="goodsItem"把数据丢进来。• $emit子传父:子组件发生事件时,发一个信号出去,父组件用@go-detail="xxx"接住。
回到 home.vue,用组件替换原来的代码:
<!-- 商品列表 --><view class="goods-list"> <GoodsCard v-for="item in goods" :key="item.id" :item="item" @go-detail="goDetail" @add-cart="addToCart" /></view>记得在 script 里引入:
import GoodsCard from '@/components/GoodsCard.vue'export default { components: { GoodsCard }, // ...}父组件原来的 goDetail 和 addToCart 方法都不用动,因为子组件发出来的事件,父组件用同样名字的方法接住了。goDetail 方法里我目前还用 uni.showToast 占位,后面接上 uni.navigateTo 就能实现真正的页面跳转。
这个拆分的最大好处是:以后在别的页面,一个 <GoodsCard> 就能复用全部的卡片样式和交互,改一个地方全站生效。组件化开发不是唬人的概念,是实打实的偷懒技巧。
05 再逼真一点:计算属性过滤“热销”列表
购物首页常有个“热销推荐”模块,只显示价格大于 10 块的商品。我们可以用计算属性来过滤,而不用写额外的方法。
在 data 里给商品加个标识也可以,不过这里咱们直接用价格硬筛,演示计算属性的过滤能力:
computed: { cartTotal() { ... }, hotGoods() { return this.goods.filter(item => item.price > 10) }}然后在模板里新增一个区块,放在普通商品列表上方:
<!-- 热销推荐 --><view class="section-title">🔥 热销推荐</view><view class="goods-list"> <GoodsCard v-for="item in hotGoods" :key="'hot-' + item.id" :item="item" @go-detail="goDetail" @add-cart="addToCart" /></view>计算属性 hotGoods 会随 goods 数组自动更新。以后你从接口拿到完整商品列表,不需要手动维护热销数组,这层筛选逻辑干净利落。
06 知识点打包:今天被你收入囊中的技能
今天没讲什么新组件,但把 Vue 的内功又夯实了一层。这些内功是 uni-app 的核心发动机,总结一下:
① 事件绑定与修饰符
• @click绑定点击事件,@confirm处理键盘完成键。• .stop阻止事件冒泡(加购按钮的坑全靠它)。• v-model双向绑定输入框,但要慎用实时触发。
② 计算属性 computed
• 依赖的数据变了,计算属性会自动重新求值,不用手动调用。 • 适合做购物车统计、列表过滤、价格格式化等一切“由现有数据派生”的逻辑。 • 和方法的区别:计算属性有缓存,依赖不变不会重复执行;方法每次调用都跑。能用计算属性就用它,性能更优。
③ 组件通信:props 和 $emit
• props:父传子,父组件用:propName="data"传递,子组件用props接收,可以限定类型、默认值。• $emit:子传父,子组件里this.$emit('事件名', 参数)发信号,父组件用@事件名="处理函数"接收。• 这俩是父子组件对话的基石,务必练熟,后面全局状态管理(Vuex)只是锦上添花。
④ 组件化思维
• 拆出可复用的组件,减少重复代码。 • 组件负责 UI 和交互,具体业务逻辑仍由父组件控制,分工清晰。 • scoped属性让组件样式局部生效,避免污染其他组件。
⑤ 数组操作
• push添加元素,find查找元素,filter过滤数组,forEach遍历。这几个方法用得最多,熟练之后数据处理如切瓜砍菜。
今天页面已经“活”了:搜索能打字,加购有反馈,购物车栏动态算钱,组件也拆得清清爽爽。静态页面变成了一个能交互的小 Demo。这一套东西在真实项目里就是反复用、反复改,后面做登录、做支付、做复杂页面对话都不怕。
老规矩,把代码敲一遍,改一改参数,看看效果。
夜雨聆风