乐于分享
好东西不私藏

深入 uni-app:给首页加点“活气”,计算属性和组件通信原来这么好用

深入 uni-app:给首页加点“活气”,计算属性和组件通信原来这么好用

前两篇咱们把环境跑通了,也画了一个能看的购物首页——轮播图、分类、商品列表,该有的皮相都有了。但那个页面有个致命问题:它是个“死”的。你点啥都没反应,加购按钮是摆设,搜索框是个装饰品。今天就来让这个页面真正动起来。

这篇的核心只有两个:计算属性怎么让代码变聪明,以及组件拆了之后父子怎么沟通。全程用购物首页当试验田,边改边讲。


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。这一套东西在真实项目里就是反复用、反复改,后面做登录、做支付、做复杂页面对话都不怕。

老规矩,把代码敲一遍,改一改参数,看看效果。