写在前面

学习uni-app,根据uni-app黑马优购项目,结合自己所遇到的问题和解决方案,写此篇博文为后来回头复盘

Headline | uniapp - 黑马优购

打算学习的小伙伴也可以照着此篇博文和视频把项目做出来,上面 uni-app黑马优购 有原文档,项目资料在下面,希望对学习uni-app的你有所帮助!

学习资料链接:百度网盘 请输入提取码
提取码:s9by

目录

HBuilderX的uni-app学习笔记

写在前面

在微信开发者工具引用接口调试时报错和解决方法:

1.起步

1.1 uni-app简介

1.2 开发工具

1.3 创建 uni-app 项目

1.4 目录结构

1.5 项目运行到微信开发者工具

1.6 配置checkSiteMap选项

1.7 使用Git本地管理uni-app项目

项目托管到码云

2. tabBar

2.0 创建新的分支

2.1 创建tabBar页面

2.2 配置tabBar效果

2.3 删除默认的 index 首页

2.4 修改导航条的样式效果

2.5 分支的提交与合并

3. 首页

3.0 创建home分支

3.1 配置网络请求

3.2 轮播图区域

3.3 分类导航区域

3.4楼层区域

3.5 分支的合并与提交

4 分类

4.0 创建cate分支

4.1渲染分类页面的基本结构

4.2 获取分类数据

4.3 动态渲染左侧的一级分类列表

4.4 动态渲染右侧的二级分类列表

4.5 动态渲染右侧的三级分类列表

4.6 切换一级分类后重置滚动条的位置

4.7 点击三级分类跳转到商品列表页面

4.8 分支的合并与提交

5.搜索

5.0 创建 search 分支

5.1 自定义搜索组件

5.2 搜索建议

5.3 搜索历史

5.4 分支的合并与提交

6.商品列表

6.0 创建 goodslist 分支

6.1 定义请求参数对象

6.2 获取商品列表数据

6.3 渲染商品列表结构

6.4 把商品 item 项封装为自定义组件

6.5 使用过滤器处理价格

6.6 上拉加载更多

6.7 下拉刷新

6.8 点击商品 item 项跳转到详情页面

6.9 分支的合并与提交

7. 商品详情

7.0 创建 goodsdetail 分支

7.1 添加商品详情页的编译模式

7.2 获取商品详情数据

7.3 渲染商品详情页的 UI 结构

7.4 渲染详情页底部的商品导航区域

7.5 分支的合并与提交


在微信开发者工具引用接口调试时报错和解决方法:

在微信开发者工具引用接口调试时报错 

 解决: 

1.起步

1.1 uni-app简介

uni-app是一个使用vue.js开发所有前端应用的框架

详情可见 uni-app官网

1.2 开发工具

  1. 下载 HBuilderX

    • 选择Windows或Mac - 选择下载正式版
  2. 安装

    • 将下载的 zip包 进行解压缩

    • 将解压之后的文件夹,存放到纯英文的目录中(且不能包含括号等特殊字符)

    • 双击 HBuilderX.exe 即可启动 HBuilderX

  3. 安装 scss/sass 编译

    为了方便编写样式(例如:),建议安装 scss/sass 编译 插件。插件下载地址:

    scss/sass编译 - DCloud 插件市场

    进入插件下载页面之后,点击右上角的 使用 HBuilderX 导入插件 按钮进行自动安装,截图如下: 

  4. 快捷键方案切换

    操作步骤:工具 -> 预设快捷键方案切换 -> VS Code

  5. 修改编辑器的基本设置

    操作步骤:工具 -> 设置 -> 打开 Settings.json 按需进行配置

1.3 创建 uni-app 项目

  1. 文件 -> 新建 -> 项目
  2. 选择uni-app项目,命名,选择存放路径
  3. 选择uni-ui模板,创建

1.4 目录结构

1.5 项目运行到微信开发者工具

  1. mainfest.json - 微信小程序配置 -- 填写微信小程序AppID(APPID在微信开发者工具中申请获取) 

  2. 工具 -> 设置 -> 运行配置 -> 小程序运行配置 输入微信开发者工具的路径
  3. 微信开发者工具中 -> 设置 -> 安全设置 -> 打开服务端口

    目的:能够运行到开发者工具看效果

  4. Hx菜单栏 运行 -> 运行到小程序模拟器 -> 微信开发者工具

1.6 配置checkSiteMap选项

mainfest.json -> 源码视图 -> setting中 加入 "checkSiteMap" : false

1.7 使用Git本地管理uni-app项目

  1. 在项目根目录中新建 .gitignore 忽略文件,并配置
    /*忽略 node_modules 目录 */ 
    /node_modules
    因为安装的第三方包都会存储在这目录,第三方包没有必要进行Git管理,因此需要忽略
    /unpackage/dist
    运行到小程序模拟器,编译生成到的小程序项目,存放到unpackage/dist目录里mp-weixin,因此需要忽略
    

注意:由于忽略了unpackage目录中仅有的dist目录,因此默认情况下,unpackage目录不会被Git追踪

此时,为了让Git正常追踪unpackage目录,按照惯例,可以在unpackage目录下创建一个 .gitkeep 的文件进行占位

  1. 打开项目所在文件夹(右键项目根目录,右键,在外部资源管理器打开),打开PowerShell窗口
    git init   //初始化本地Git仓库
    git add .  //将所有文件都加入到暂存区
    git commit -m "init project"  //本地提交更新 可以status查看分支 
    

项目托管到码云

  1. 进入码云
  2. 生成配置SSH公钥 -> 在设置里添加已有公钥 C:\Users\DELL.ssh .pub格式是公钥
    // 添加完 ,在终端
     ssh -T git@gitee.com
    // 检测是否配置成功
    
  3. 创建空白仓库,本地项目上传到空白仓库
  • 选择SSH
  • 已有仓库 在文件所在位置打开终端输入代码 //关联仓库

2. tabBar

2.0 创建新的分支

  • 创建分支 git checkout -b newbranch
  • 查看当前项目所有分支 git branch

2.1 创建tabBar页面

  • pages目录上鼠标右键, 选择新建页面
  1. 选择所需模板
  2. 项目名称命名
  3. 在pages.json中注册
  4. 创建同名目录
  5. 完成创建

shift+alt+f 整理pages.json里的代码格式

2.2 配置tabBar效果

  1. 先将资料目录下的 static 文件夹替换到项目目录里。所需照片导入
  2. 修改项目根目录中的 pages.json 配置文件,新增 tabBar 的配置节点如下:
    {
    "tabBar": {
     "selectedColor": "#C00000", //设置选中项文本的颜色
     "list": [
       {
         "pagePath": "pages/home/home",
         "text": "首页",
         "iconPath": "static/tab_icons/home.png",
         "selectedIconPath": "static/tab_icons/home-active.png"
       },
       {
         "pagePath": "pages/cate/cate",
         "text": "分类",
         "iconPath": "static/tab_icons/cate.png",
         "selectedIconPath": "static/tab_icons/cate-active.png"
       },
       {
         "pagePath": "pages/cart/cart",
         "text": "购物车",
         "iconPath": "static/tab_icons/cart.png",
         "selectedIconPath": "static/tab_icons/cart-active.png"
       },
       {
         "pagePath": "pages/my/my",
         "text": "我的",
         "iconPath": "static/tab_icons/my.png",
         "selectedIconPath": "static/tab_icons/my-active.png"
       }
     ]
    }
    }
    

2.3 删除默认的 index 首页

配置完看不到最终的tabbar效果,需要把pages数组第一个index删除 找到 page 数组,把第一项默认的数组删除,再去pages文件夹删除index文件,即可正常显示

  1. 在 HBuilderX 中,把 pages 目录下的 index首页文件夹 删除掉
  2. 同时,把 page.json 中记录的 index 首页 路径删除掉
  3. 为了防止小程序运行失败,在微信开发者工具中,手动删除 pages 目录下的 index 首页文件夹
  4. 同时,把 components 目录下的 uni-link 组件文件夹 删除掉

2.4 修改导航条的样式效果

打开pages.json -> 修改globalStyle节点

{
  "globalStyle": {
    "navigationBarTextStyle": "white",    //导航条文本颜色
    "navigationBarTitleText": "黑马优购",   //导航条文本内容
    "navigationBarBackgroundColor": "#C00000",   //导航条背景颜色
    "backgroundColor": "#FFFFFF"         
  }
}

2.5 分支的提交与合并

  1. 将本地的 tabbar 分支进行本地的 commit 提交:
    git add .    // 将修改的文件加入暂存区
    git commit -m "完成了 tabBar 的开发"   // 提交信息
    
  2. 将本地的 tabbar 分支推送到远程仓库进行保存:
    git push -u origin tabbar      //分支推送到码云保存
    
  3. 将本地的 tabbar 分支合并到本地的 master 分支:
    git checkout master    //首先切换到主分支
    git merge tabbar    //合并tabbar代码
    

    到这一步后不能看源文档里的直接删除分支!!!因为还没有推送到master分支

    git push
    
  4. 最后删除本地的 tabbar 分支:
    git branch -d tabbar //想删除当前分支,先跳转到其他分支
    

3. 首页

微信开发者工具出现 TypeError: Cannot read property 'invoke' of undefined

解决办法: 

3.0 创建home分支

打开终端,基于master分支在本地创建home子分支,用来开发首页相关功能

git checkout -b home

3.1 配置网络请求

由于平台的限制,小程序项目中不支持 axios,而且原生的 wx.request() API 功能较为简单, 不支持拦截器等全局定制的功能。因此,建议在 uni-app 项目中使用 @escook/request-miniprogram 第三方包发起网络数据请求。

@escook/request-miniprogram官方文档

安装

  1. 右键根目录 ->
  2. 在外部资源管理器打开 ->
  3. 进入powershell ->
  4. 初始化一个npm包管理文件 即npm init -y生成默认的package.json文件 ->
  5. 安装 npm install @escook/request-miniprogram

导入
在项目的 main.js 入口文件中,通过如下的方式进行配置:

//导入网络请求的包
import { $http } from '@escook/request-miniprogram'

//挂载
uni.$http = $http

//请求拦截器   请求之前展示loading效果
$http.beforeRequest = function(options){
    uni.showLoading({
        title: '数据加载中...'
    })
}
//响应拦截器   请求后隐藏loading效果
$http.afterRequest = function(){
    uni.hideLoading()
}

如果当前接口失效,可以:

  1. 登录你的微信公众平台 https://mp.weixin.qq.com
  2. 打开服务器域名
  3. 设置request域名
  4. 微信小程序工具 -> 详情 -> 项目配置 -> 域名信息 (在此添加修改)
  5. 重启微信开发工具 (必须要重启)

3.2 轮播图区域

3.2.1请求轮播图数据

实现步骤:

  1. 在main.js中挂载一下根路径
    //请求根路径
    $http.baseUrl = 'https://www.uinav.com'
    

    如果在微信开发者工具 报错对应的服务器证书无效。控制台输入 showRequestInfo() 可以获取更详细信息。

    解决办法 

     勾选不校验合法域名

  2. 在 data 中定义轮播图的数组
  3. 在 onLoad 生命周期函数中调用获取轮播图数据的方法
  4. 在 methods 中定义获取轮播图数据的方法

just like

export default {
        data() {
            return {
                // 轮播图的数据列表,默认为空列表
                swiperList: []
            };
        },
        onLoad() {
            // 在小程序页面刚加载时,调用获取轮播图数据方法
            this.getSwiperList()
        },
        methods: {
            async getSwiperList() {
                //发起网络数据请求,获取轮播图数据的方法
                const {data: res} = await uni.$http.get('/api/public/v1/home/swiperdata')
                //请求失败
                if (res.meta.status !== 200) {
                    return uni.showToast({
                        title: '数据请求失败!',
                        duration: 1500,
                        icon: 'none'
                    })
                }

                //请求成功 -> 挂载到data的swpierList里,为data中的数据赋值
                this.swiperList = res.message
            }
        }

轮播图的借口 '/api/public/v1/home/swiperdata'

3.2.2 渲染轮播图的 UI 结构

  1. 渲染 UI 结构:
    <template>
    <view>
     <!-- 轮播图区域 -->
     <swiper :indicator-dots="true" :autoplay="true" :interval="3000" :duration="1000" :circular="true">
       <!-- 循环渲染轮播图的 item 项 -->
       <swiper-item v-for="(item, i) in swiperList" :key="i">
         <view class="swiper-item">
           <!-- 动态绑定图片的 src 属性 -->
           <image :src="item.image_src"></image>
         </view>
       </swiper-item>
     </swiper>
    </view>
    </template>
    解释:
    :indicator-dots="true"  //小圆点
    :autoplay="true"  //自动轮播
    :interval="3000"  //轮播间隔 3s
    :duration="1000"  //从开始轮播到结束总共耗时 1s
    :circular="true"  //衔接轮播
    
  2. 美化 UI 结构:
<style lang="scss">
swiper {
 height: 330rpx;

 .swiper-item,
 image {
   width: 100%;
   height: 100%;
 }
}
</style>

有可能会报错TypeError: Cannot read property ‘$$‘ of undefined

出现以上警告是因为使用 swiper 的 current属性 没有重置,将其重置为0即可

在data加入current: 0 即可

data: {
    current: 0
}

3.2.3 配置小程序分包

分包可以减少小程序首次启动时的加载时间

为此,我们在项目中,把 tabBar 相关的 4 个页面放到主包中,

其它页面(例如:商品详情页、商品列表页)放到分包中。在 uni-app 项目中,配置分包的步骤如下:

  1. 在项目根目录中,创建分包的根目录,命名为 subpkg
  2. 在 pages.json 中,和 pages 节点平级的位置声明 subPackages 节点,用来定义分包相关的结构:
    {
    "pages": [
     {
       "path": "pages/home/home",
       "style": {}
     },
     {
       "path": "pages/cate/cate",
       "style": {}
     },
     {
       "path": "pages/cart/cart",
       "style": {}
     },
     {
       "path": "pages/my/my",
       "style": {}
     }
    ],
    "subPackages": [
     {
       "root": "subpkg",
       "pages": []
     }
    ]
    }
    
  3. 在 subpkg 目录上鼠标右键,点击 新建页面 选项,并填写页面的相关信息:

3.2.4 点击轮播图跳转到商品详情页面

将 节点内的 view 组件,改造为 navigator 导航组件,并动态绑定 url 属性的值。 改造之前的 UI 结构:

<swiper-item v-for="(item, i) in swiperList" :key="i">
  <view class="swiper-item">
    <!-- 动态绑定图片的 src 属性 -->
    <image :src="item.image_src"></image>
  </view>
</swiper-item>

改造之后的 UI 结构:

<swiper-item v-for="(item, i) in swiperList" :key="i">
    <navigator class="swiper-item" :url="'/subpkg/goods_detail/goods_detail?goods_id=' + item.goods_id">
      <!-- 动态绑定图片的 src 属性 -->
      <image :src="item.image_src"></image>
    </navigator>
</swiper-item>

url动态绑定 商品路径和商品id

3.2.5封装 uni.$showMsg() 方法

当数据请求失败之后,经常需要调用 uni.showToast({ /* 配置对象 */ }) 方法来提示用户。 此时,可以在全局封装一个 uni.$showMsg() 方法,来简化 uni.showToast() 方法的调用。具体的改造步骤如下:

  1. 在 main.js 中,为 uni 对象挂载自定义的 $showMsg() 方法:
    // 封装弹框消息提示的方法
    uni.$showMsg = function (title ='数据请求失败!', duration = 1500) {
     uni.showToast({
         title,
         duration,
         icon: 'none',
     })
    }
    
  2. 今后,在需要提示消息的时候,直接调用 uni.$showMsg() 方法即可:
    async getSwiperList() {
             <!-- 发起网络数据请求,获取轮播图数据的方法 -->
         const { data: res } = await uni.$http.get('/api/public/v1/home/swiperdata')
             <!-- 请求失败 -->
         if (res.meta.status !== 200) return uni.$showMsg()
             <!-- 请求成功 -> 挂载到data的swpierList里,为data中数据赋值 -->
         this.swiperList = res.message
    }
    

    首页分类导航接口 '/api/public/v1/home/swiperdata'

3.3 分类导航区域

3.3.1 获取分类导航的数据

实现思路:

  1. 定义 data 数据
  2. 在 onLoad 中调用获取数据的方法
  3. 在 methods 中定义获取数据的方法

示例代码如下:

export default {
  data() {
    return {
      // 1. 分类导航的数据列表
      navList: [],
    }
  },
  onLoad() {
    // 2. 在 onLoad 中调用获取数据的方法
    this.getNavList()
  },
  methods: {
    // 3. 在 methods 中定义获取数据的方法
    async getNavList() {
      const { data: res } = await uni.$http.get('/api/public/v1/home/catitems')
      if (res.meta.status !== 200) return uni.$showMsg()
      this.navList = res.message
    },
  },
}

3.3.2 渲染分类导航的 UI 结构

  1. 定义如下的 UI 结构:

    <!-- 分类导航区域 -->
    <view class="nav-list">
    <view class="nav-item" v-for="(item, i) in navList" :key="i">
      <image :src="item.image_src" class="nav-img"></image>
    </view>
    </view>
    
  2. 通过如下的样式美化页面结构:

    .nav-list {
    display: flex;
    justify-content: space-around;
    margin: 15px 0;
    
    .nav-img {
     width: 128rpx;
     height: 140rpx;
    }
    }
    

3.3.3 点击第一项,切换到分类页面

  1. 为 nav-item 绑定点击事件处理函数:
    <!-- 分类导航区域 -->
    <view class="nav-list">
    <view class="nav-item" v-for="(item, i) in navList" :key="i" @click="navClickHandler(item)">
     <image :src="item.image_src" class="nav-img"></image>
    </view>
    </view>
    
  2. 定义 navClickHandler 事件处理函数:
    // nav-item 项被点击时候的事件处理函数
    navClickHandler(item) {
     if (item.name === '分类') {
         uni.swichTab({
             url:'/pages/cate/cate'
         })
     }
    }
    

    注意:分类页面属于tabbar对应页面,要切换tabbar页面,只能调用switchTab

3.4楼层区域

实现思路:

  1. 定义 data 数据
  2. 在 onLoad 中调用获取数据的方法
  3. 在 methods 中定义获取数据的方法 示例代码如下:
    export default {
    data() {
     return {
       // 1. 楼层的数据列表
       floorList: [],
     }
    },
    onLoad() {
     // 2. 在 onLoad 中调用获取楼层数据的方法
     this.getFloorList()
    },
    methods: {
     // 3. 定义获取楼层列表数据的方法
     async getFloorList() {
       const { data: res } = await uni.$http.get('/api/public/v1/home/floordata')
       if (res.meta.status !== 200) return uni.$showMsg()
       this.floorList = res.message
     },
    },
    }
    

3.4.1 获取楼层数据

实现思路:

  1. 定义 data 数据
  2. 在 onLoad 中调用获取数据的方法
  3. 在 methods 中定义获取数据的方法 示例代码如下:
    export default {
    data() {
     return {
       // 1. 楼层的数据列表
       floorList: [],
     }
    },
    onLoad() {
     // 2. 在 onLoad 中调用获取楼层数据的方法
     this.getFloorList()
    },
    methods: {
     // 3. 定义获取楼层列表数据的方法
     async getFloorList() {
       const { data: res } = await uni.$http.get('/api/public/v1/home/floordata')
       if (res.meta.status !== 200) return uni.$showMsg()
       this.floorList = res.message
     },
    },
    }
    

    楼层数据接口 '/api/public/v1/home/floordata'

3.4.2 渲染楼层的标题

  1. 定义如下的 UI 结构:
    <!-- 楼层区域 -->
    <view class="floor-list">
    <!-- 楼层 item 项 -->
    <view class="floor-item" v-for="(item, i) in floorList" :key="i">
     <!-- 楼层标题 -->
     <image :src="item.floor_title.image_src" class="floor-title"></image>
    </view>
    </view>
    
  2. 美化楼层标题的样式:
    .floor-title {
    height: 60rpx;
    width: 100%;
    display: flex;
    }
    

3.4.3 渲染楼层里的图片

  1. 定义楼层图片区域的 UI 结构:

    <!-- 楼层图片区域 -->
    <view class="floor-img-box">
    <!-- 左侧大图片的盒子 -->
    <view class="left-img-box">
     <image :src="item.product_list[0].image_src" :style="{width: item.product_list[0].image_width + 'rpx'}" mode="widthFix"></image>
    </view>
    <!-- 右侧 4 个小图片的盒子 -->
    <view class="right-img-box">
     <view class="right-img-item" v-for="(item2, i2) in item.product_list" :key="i2" v-if="i2 !== 0">
       <image :src="item2.image_src" mode="widthFix" :style="{width: item2.image_width + 'rpx'}"></image>
     </view>
    </view>
    </view>
    

    图片高度需要自适应 mode="widthFix"

  2. 美化楼层图片区域的样式:

    .right-img-box {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-around;
    }
    .floor-img-box {
    display: flex;
    padding-left: 10rpx;
    }
    

3.4.4 点击楼层图片跳转到商品列表页

  1. 在 subpkg 分包中,新建 goods_list 页面 

  2. 楼层数据请求成功之后,通过双层 forEach 循环,处理 URL 地址:

    // 获取楼层列表数据
    async getFloorList() {
    const { data: res } = await uni.$http.get('/api/public/v1/home/floordata')
    if (res.meta.status !== 200) return uni.$showMsg()
    
    // 通过双层 forEach 循环,处理 URL 地址
    res.message.forEach(floor => {
     floor.product_list.forEach(prod => {
       prod.url = '/subpkg/goods_list/goods_list?' + prod.navigator_url.split('?')[1]
     })
    })
    
    this.floorList = res.message
    }
    

    split(“?“)分割字符串

  3. 把图片外层的 view 组件,改造为 navigator 组件,并动态绑定 url 属性 的值:

    <!-- 楼层图片区域 -->
    <view class="floor-img-box">
    <!-- 左侧大图片的盒子 -->
    <navigator class="left-img-box" :url="item.product_list[0].url">
     <image :src="item.product_list[0].image_src" :style="{width: item.product_list[0].image_width + 'rpx'}" mode="widthFix"></image>
    </navigator>
    <!-- 右侧 4 个小图片的盒子 -->
    <view class="right-img-box">
     <navigator class="right-img-item" v-for="(item2, i2) in item.product_list" :key="i2" v-if="i2 !== 0" :url="item2.url">
       <image :src="item2.image_src" mode="widthFix" :style="{width: item2.image_width + 'rpx'}"></image>
     </navigator>
    </view>
    </view>
    

3.5 分支的合并与提交

  1. 将本地的 home 分支进行本地的 commit 提交:
    git add .
    git commit -m "完成了 home 首页的开发"
    
  2. 将本地的 home 分支推送到远程仓库进行保存:
    git push -u origin home
    
  3. 将本地的 home 分支合并到本地的 master 分支,再推送一次:
    git checkout master
    git merge home
    git push
    
  4. 删除本地的 home 分支:
    git branch -d home
    

4 分类

4.0 创建cate分支

git checkout -b cate

可以新建分类页面编译模式,刷新便是分类页面方便查看 

4.1渲染分类页面的基本结构

  1. 定义页面结构如下:

    <template>
    <view>
     <view class="scroll-view-container">
       <!-- 左侧的滚动视图区域 -->
       <scroll-view class="left-scroll-view" scroll-y :style="{height: wh + 'px'}">
         <view class="left-scroll-view-item active">xxx</view>
         <view class="left-scroll-view-item">xxx</view>
         <view class="left-scroll-view-item">xxx</view>
         <view class="left-scroll-view-item">xxx</view>
         <view class="left-scroll-view-item">xxx</view>
         <view class="left-scroll-view-item">多复制一些节点,演示纵向滚动效果...</view>
       </scroll-view>
       <!-- 右侧的滚动视图区域 -->
       <scroll-view class="right-scroll-view" scroll-y :style="{height: wh + 'px'}">
         <view class="left-scroll-view-item">zzz</view>
         <view class="left-scroll-view-item">zzz</view>
         <view class="left-scroll-view-item">zzz</view>
         <view class="left-scroll-view-item">zzz</view>
         <view class="left-scroll-view-item">多复制一些节点,演示纵向滚动效果</view>
       </scroll-view>
     </view>
    </view>
    </template>
    
  2. 动态计算窗口的剩余高度:

    <script>
    export default {
     data() {
       return {
         // 窗口的可用高度 = 屏幕高度 - navigationBar高度 - tabBar 高度
         wh: 0
       };
     },
     onLoad() {
       // 获取当前系统的信息
       const sysInfo = uni.getSystemInfoSync()
       // 为 wh 窗口可用高度动态赋值
       this.wh = sysInfo.windowHeight
     }
    }
    </script>
    

    调用uni.getSystemInfoSync()获取设备信息

  3. 美化页面结构:

    .scroll-view-container {
    display: flex;
    
    .left-scroll-view {
     width: 120px;
    
     .left-scroll-view-item {
       line-height: 60px;
       background-color: #f7f7f7;
       text-align: center;
       font-size: 12px;
    
       // 激活项的样式
       &.active {
         background-color: #ffffff;
         position: relative;
    
         // 渲染激活项左侧的红色指示边线 伪元素
         &::before {
           content: ' ';
           display: block;
           width: 3px;
           height: 30px;
           background-color: #c00000;
           position: absolute;
           left: 0;
           top: 50%;
           transform: translateY(-50%);
         }
       }
     }
    }
    }
    

4.2 获取分类数据

  1. 在 data 中定义分类数据节点:
    data() {
    return {
     // 分类数据列表
     cateList: []
    }
    }
    
  2. 调用获取分类列表数据的方法:
    onLoad() {
    // 调用获取分类列表数据的方法
    this.getCateList()
    }
    
  3. 定义获取分类列表数据的方法:
    methods: {
    async getCateList() {
     // 发起请求
     const { data: res } = await uni.$http.get('/api/public/v1/categories')
     // 判断是否获取失败
     if (res.meta.status !== 200) return uni.$showMsg()
     // 转存数据
     this.cateList = res.message
    }
    }
    

4.3 动态渲染左侧的一级分类列表

  1. 循环渲染列表结构:
    <!-- 左侧的滚动视图区域 -->
    <scroll-view class="left-scroll-view" scroll-y :style="{height: wh + 'px'}">
    <block v-for="(item,i) in cateList" :key="i">
         <view class="left-scroll-view-item">{{item.cat_name}}</view>
     </block>
    </scroll-view>
    
  2. 在data中定义默认选中项的索引:
    data() {
     return {
         active: 0
     }
    }
    
  3. 循环渲染结构时,为选中项动态添加 .active 类名
    <block v-for="(item, i) in cateList" :key="i">
    <view :class="['left-scroll-view-item', i === active ? 'active' : '']">{{item.cat_name}}</view>
    </block>
    
  4. 为一级分类的item项绑定点击事件处理函数activeChanged:
    <block v-for="(item, i) in cateList" :key="i">
    <view :class="['left-scroll-view-item', i === active ? 'active' : '']" @click="activeChanged(i)">{{item.cat_name}}</view>
    </block>
    
  5. 定义activeChanged事件处理函数,动态修改选中项的索引:
    methods: {
     //选中项 改变的事件处理函数
     activeChanged(i) {
         this.active = i
     }
    }
    

4.4 动态渲染右侧的二级分类列表

  1. 在 data 中定义二级分类列表的数据节点:
    data() {
    return {
     // 二级分类列表
     cateLevel2: []
    }
    }
    
  2. 修改 getCateList 方法,在请求到数据之后,为二级分类列表数据赋值:
    async getCateList() {
    const { data: res } = await uni.$http.get('/api/public/v1/categories')
    if (res.meta.status !== 200) return uni.$showMsg()
    this.cateList = res.message
    // 为二级分类赋值
    this.cateLevel2 = res.message[0].children
    }
    
  3. 修改 activeChanged 方法,在一级分类选中项改变之后,为二级分类列表数据重新赋值:
    activeChanged(i) {
    this.active = i
    // 为二级分类列表重新赋值
    this.cateLevel2 = this.cateList[i].children
    }
    
  4. 循环渲染右侧二级分类列表的 UI 结构:
    <!-- 右侧的滚动视图区域 -->
    <scroll-view class="right-scroll-view" scroll-y :style="{height: wh + 'px'}">
    <view class="cate-lv2" v-for="(item2, i2) in cateLevel2" :key="i2">
     <view class="cate-lv2-title">/ {{item2.cat_name}} /</view>
    </view>
    </scroll-view>
    
  5. 美化二级分类的标题样式:
    .cate-lv2-title {
    font-size: 12px;
    font-weight: bold;
    text-align: center;
    padding: 15px 0;
    }
    

4.5 动态渲染右侧的三级分类列表

  1. 在二级分类的 组件中,循环渲染三级分类的列表结构:

    <!-- 右侧的滚动视图区域 -->
    <scroll-view class="right-scroll-view" scroll-y :style="{height: wh + 'px'}">
    <view class="cate-lv2" v-for="(item2, i2) in cateLevel2" :key="i2">
     <view class="cate-lv2-title">/ {{item2.cat_name}} /</view>
     <!-- 动态渲染三级分类的列表数据 -->
     <view class="cate-lv3-list">
       <!-- 三级分类 Item 项 -->
       <view class="cate-lv3-item" v-for="(item3, i3) in item2.children" :key="i3">
         <!-- 图片 -->
         <image :src="item3.cat_icon"></image>
         <!-- 文本 -->
         <text>{{item3.cat_name}}</text>
       </view>
     </view>
    </view>
    </scroll-view>
    
  2. 美化三级分类的样式:

    .cate-lv3-list {
    display: flex;
    flex-wrap: wrap;
    
    .cate-lv3-item {
     width: 33.33%;
     margin-bottom: 10px;
     display: flex;
     flex-direction: column;
     align-items: center;
    
     image {
       width: 60px;
       height: 60px;
     }
    
     text {
       font-size: 12px;
     }
    }
    }
    

4.6 切换一级分类后重置滚动条的位置

  1. 在 data 中定义 滚动条距离顶部的距离:

    data() {
    return {
     // 滚动条距离顶部的距离
     scrollTop: 0
    }
    }
    
  2. 动态为右侧的 组件绑定 :scroll-top 属性

    <!-- 右侧的滚动视图区域 -->
    <scroll-view class="right-scroll-view" scroll-y :style="{height: wh + 'px'}" :scroll-top="scrollTop"></scroll-view>
    
  3. 切换一级分类时,动态设置 scrollTop 的值

    // 选中项改变的事件处理函数
    activeChanged(i) {
    this.active = i
    this.cateLevel2 = this.cateList[i].children
    
    // 让 scrollTop 的值在 0 与 1 之间切换
    this.scrollTop = this.scrollTop === 0 ? 1 : 0
    
    // 可以简化为如下的代码:
    // this.scrollTop = this.scrollTop ? 0 : 1
    }
    

4.7 点击三级分类跳转到商品列表页面

  1. 为三级分类的item项绑定点击事件
    <view class="cate-lv3-item" v-for="(item3, i3) in item2.children" :key="i3" @click="gotoGoodsList(item3)">
    <image :src="item3.cat_icon"></image>
    <text>{{item3.cat_name}}</text>
    </view>
    
  2. 定义事件处理函数如下
    // 点击三级分类项跳转到商品列表页面
    gotoGoodsList(item3) {
    uni.navigateTo({
     url: '/subpkg/goods_list/goods_list?cid=' + item3.cat_id
    })
    }
    

4.8 分支的合并与提交

  1. 将 cate 分支进行本地提交:
    git add .
    git commit -m "完成了分类页面的开发"
    
  2. 将本地的 cate 分支推送到码云:
    git push -u origin cate
    
  3. 将本地 cate 分支中的代码合并到 master 分支:
    git checkout master
    git merge cate
    git push
    
  4. 删除本地的 cate 分支:
    git branch -d cate
    

5.搜索

5.0 创建 search 分支

git checkout -b search

5.1 自定义搜索组件

5.1.1 自定义 my-search 组件

  1. 在项目根目录的 components 目录上,鼠标右键,选择 新建组件,填写组件信息后,最后点击 创建 按钮: 

  2. 在分类页面的 UI 结构中,直接以标签的形式使用 my-search 自定义组件:

    <!-- 使用自定义的搜索组件 -->
    <my-search></my-search>
    

    定义好的组件,可在 template 下直接使用

  3. 定义 my-search 组件的 UI 结构如下:

    <template>
    <view class="my-search-container">
     <!-- 使用 view 组件模拟 input 输入框的样式 -->
     <view class="my-search-box">
       <uni-icons type="search" size="17"></uni-icons>
       <text class="placeholder">搜索</text>
     </view>
    </view>
    </template>
    

    注意:在当前组件中,我们使用 view 组件模拟 input 输入框的效果;并不会在页面上渲染真正的 input 输入框

  4. 美化自定义 search 组件的样式:

    .my-search-container {
    background-color: #c00000;
    height: 50px;
    padding: 0 10px;
    display: flex;
    align-items: center;
    }
    .my-search-box {
    height: 36px;
    background-color: #ffffff;
    border-radius: 15px;
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    
    .placeholder {
     font-size: 15px;
     margin-left: 5px;
    }
    }
    
  5. 由于自定义的 my-search 组件高度为 50px,因此,需要重新计算分类页面窗口的可用高度:

    onLoad() {
    const sysInfo = uni.getSystemInfoSync()
    // 可用高度 = 屏幕高度 - navigationBar高度 - tabBar高度 - 自定义的search组件高度
    this.wh = sysInfo.windowHeight - 50
    }
    

    加入搜索框的scroll滑动距离需重新计算,于是在cate目录中修改

5.1.2 通过自定义属性增强组件的通用性

为了增强组件的通用性,我们允许使用者自定义搜索组件的 背景颜色 和 圆角尺寸

  1. 通过 props 定义 bgcolor 和 radius 两个属性,并指定值类型和属性默认值:

    props: {
    // 背景颜色
    bgcolor: {
     type: String,
     default: '#C00000'
    },
    // 圆角尺寸
    radius: {
     type: Number,
     // 单位是 px
     default: 18
    }
    }
    
  2. 通过属性绑定的形式,为 .my-search-container 盒子和 .my-search-box 盒子动态绑定 style 属性:

    <view class="my-search-container" :style="{'background-color': bgcolor}">
    <view class="my-search-box" :style="{'border-radius': radius + 'px'}">
     <uni-icons type="search" size="17"></uni-icons>
     <text class="placeholder">搜索</text>
    </view>
    </view>
    
  3. 移除对应 scss 样式中的 背景颜色 和 圆角尺寸:

    .my-search-container {
    // 移除背景颜色,改由 props 属性控制
    // background-color: #C00000;
    height: 50px;
    padding: 0 10px;
    display: flex;
    align-items: center;
    }
    .my-search-box {
    height: 36px;
    background-color: #ffffff;
    // 移除圆角尺寸,改由 props 属性控制
    // border-radius: 15px;
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    
    .placeholder {
     font-size: 15px;
     margin-left: 5px;
    }
    }
    

5.1.3 为自定义组件封装 click 事件

  1. 在 my-search 自定义组件内部,给类名为 .my-search-box 的 view 绑定 click 事件处理函数:
    <view class="my-search-box" :style="{'border-radius': radius + 'px'}" @click="searchBoxHandler">
    <uni-icons type="search" size="17"></uni-icons>
    <text class="placeholder">搜索</text>
    </view>
    
  2. 在 my-search 自定义组件的 methods 节点中,声明事件处理函数如下:
    methods: {
    // 点击了模拟的 input 输入框
    searchBoxHandler() {
     // 触发外界通过 @click 绑定的 click 事件处理函数
     this.$emit('click')
    }
    }
    
  3. 在分类页面中使用 my-search 自定义组件时,即可通过 @click 为其绑定点击事件处理函数:
    <!-- 使用自定义的搜索组件 -->
    <my-search @click="gotoSearch"></my-search>
    
    同时在分类页面中,定义 gotoSearch 事件处理函数如下:
    methods: {
    // 跳转到分包中的搜索页面
    gotoSearch() {
      uni.navigateTo({
        url: '/subpkg/search/search'
      })
    }
    }
    

    为组件封装 click 事件,以后在哪里调用组件直接 @调用 ,方便

  4. 此时可能还没有创建 subpkg/search 因此报错,自己右键subpkg根目录创建一下 

5.1.4 实现首页搜索组件的吸顶效果

  1. 在 home 首页定义如下的 UI 结构:
    <!-- 使用自定义的搜索组件 -->
    <view class="search-box">
    <my-search @click="gotoSearch"></my-search>
    </view>
    
  2. 在 home 首页定义如下的事件处理函数:
    gotoSearch() {
    uni.navigateTo({
     url: '/subpkg/search/search'
    })
    }
    
  3. 通过如下的样式实现吸顶的效果:
    .search-box {
    // 设置定位效果为“吸顶”
    position: sticky;
    // 吸顶的“位置”
    top: 0;
    // 提高层级,防止被轮播图覆盖
    z-index: 999;
    }
    

5.2 搜索建议

5.2.1 渲染搜索页面的基本结构

  1. 定义如下的 UI 结构:
    <view class="search-box">
    <!-- 使用 uni-ui 提供的搜索组件 -->
    <uni-search-bar @input="input" :radius="100" cancelButton="none"></uni-search-bar>
    </view>
    
  2. 修改 uni_modules -> uni-search-bar -> uni-search-bar.vue 组件,将默认的白色搜索背景改为 #C00000 的红色背景:
    .uni-searchbar {
    /* #ifndef APP-NVUE */
    display: flex;
    /* #endif */
    flex-direction: row;
    position: relative;
    padding: 16rpx;
    /* 将默认的 #FFFFFF 改为 #C00000 */
    background-color: #c00000;
    }
    

    教程提到的是uni-search-barcomponent目录下,我这里不同,可能是导入插件时一并导入了,根据uni-search-bar的位置调整就行

  3. 实现搜索框的吸顶效果:
    .search-box {
    position: sticky;
    top: 0;
    z-index: 999;
    }
    
  4. 定义如下的 input 事件处理函数:
    methods: {
     input(e) {
         // e 是最新的搜索内容
         console.log(e)
     }
    }
    

    这里注意,和原文档里的不同,可能因为uni-search-bar的更新,这里无需加 value,否则报 undefined

5.2.2 实现搜索框自动获取焦点

  1. 修改 uni_modules -> uni-search-bar -> uni-search-bar.vue 组件,把 data 数据中的 show 和 showSync 的值,从默认的 false 改为 true 即可:
    data() {
    return {
     show: true,
     showSync: true,
     searchVal: ""
    }
    }
    
  2. 使用手机扫码预览,即可在真机上查看效果。

5.2.3 实现搜索框的防抖处理

  1. 在 data 中定义防抖的延时器 timerId 如下:
    data() {
    return {
     // 延时器的 timerId
     timer: null,
     // 搜索关键词
     kw: ''
    }
    }
    
  2. 修改 input 事件处理函数如下:
    input(e) {
     //用户在500ms内连续输入内容,应该清除之前的
     clearTimeout(this.timer)
     //开一个延时器就会有一个返回值 timer
     this.timer = setTimeout(() => {
         this.kw = e
         console.log(e)
     },500)
    },
    

5.2.4 根据关键词查询搜索建议列表

  1. 在 data 中定义如下的数据节点,用来存放搜索建议的列表数据:
    data() {
    return {
     // 搜索结果列表
     searchResults: []
    }
    }
    
  2. 在防抖的 setTimeout 中,调用 getSearchList 方法获取搜索建议列表:
    this.timer = setTimeout(() => {
     this.kw = e
     this.getSearchList()
    },500)
    
  3. 在 methods 中定义 getSearchList 方法如下:
    // 根据搜索关键词,搜索商品建议列表
    async getSearchList() {
    // 判断关键词是否为空
    if (this.kw === '') {
     this.searchResults = []
     return
    }
    // 发起请求,获取搜索建议列表
    const { data: res } = await uni.$http.get('/api/public/v1/goods/qsearch', { query: this.kw })
    if (res.meta.status !== 200) return uni.$showMsg()
    this.searchResults = res.message
    }
    

5.2.5 渲染搜索建议列表

  1. 定义如下的 UI 结构:

    <!-- 搜索建议列表 -->
    <view class="sugg-list">
    <view class="sugg-item" v-for="(item, i) in searchResults" :key="i" @click="gotoDetail(item.goods_id)">
     <view class="goods-name">{{item.goods_name}}</view>
     <uni-icons type="arrowright" size="16"></uni-icons>
    </view>
    </view>
    
  2. 美化搜索建议列表:

    .sugg-list {
    padding: 0 5px;
    
    .sugg-item {
     font-size: 12px;
     padding: 13px 0;
     border-bottom: 1px solid #efefef;
     display: flex;
     align-items: center;
     justify-content: space-between;
    
     .goods-name {
       // 文字不允许换行(单行文本)
       white-space: nowrap;
       // 溢出部分隐藏
       overflow: hidden;
       // 文本溢出后,使用 ... 代替
       text-overflow: ellipsis;
       margin-right: 3px;
     }
    }
    }
    

    样式要是报错就不嵌套,拿出来写

  3. 点击搜索建议的 Item 项,跳转到商品详情页面:

    gotoDetail(goods_id) {
    uni.navigateTo({
     // 指定详情页面的 URL 地址,并传递 goods_id 参数
     url: '/subpkg/goods_detail/goods_detail?goods_id=' + goods_id
    })
    }
    

5.3 搜索历史

5.3.1 渲染搜索历史记录的基本结构

  1. 在 data 中定义搜索历史的假数据:

    data() {
    return {
     // 搜索关键词的历史记录
     historyList: ['a', 'app', 'apple']
    }
    }
    
  2. 渲染搜索历史区域的 UI 结构:

    <!-- 搜索历史 -->
    <view class="history-box">
    <!-- 标题区域 -->
    <view class="history-title">
     <text>搜索历史</text>
     <uni-icons type="trash" size="17"></uni-icons>
    </view>
    <!-- 列表区域 -->
    <view class="history-list">
     <uni-tag :text="item" v-for="(item, i) in historyList" :key="i"></uni-tag>
    </view>
    </view>
    
  3. 美化搜索历史区域的样式:

    .history-box {
    padding: 0 5px;
    
    .history-title {
     display: flex;
     justify-content: space-between;
     align-items: center;
     height: 40px;
     font-size: 13px;
     border-bottom: 1px solid #efefef;
    }
    
    .history-list {
     display: flex;
     flex-wrap: wrap;
    
     .uni-tag {
       margin-top: 5px;
       margin-right: 5px;
     }
    }
    }
    

5.3.2 实现搜索建议和搜索历史的按需展示

  1. 当搜索结果列表的长度不为 0的时候(searchResults.length !== 0),需要展示搜索建议区域,隐藏搜索历史区域
  2. 当搜索结果列表的长度等于 0的时候(searchResults.length === 0),需要隐藏搜索建议区域,展示搜索历史区域
  3. 使用 v-if 和 v-else 控制这两个区域的显示和隐藏,示例代码如下:
    <!-- 搜索建议列表 -->
    <view class="sugg-list" v-if="searchResults.length !== 0">
    <!-- 省略其它代码... -->
    </view>
    

```

5.3.3 将搜索关键词存入 historyList

  1. 直接将搜索关键词 push 到 historyList 数组中即可

    methods: {
    // 根据搜索关键词,搜索商品建议列表
    async getSearchList() {
     // 省略其它不必要的代码...
    
     // 1. 查询到搜索建议之后,调用 saveSearchHistory() 方法保存搜索关键词
     this.saveSearchHistory()
    },
    // 2. 保存搜索关键词的方法
    saveSearchHistory() {
     // 2.1 直接把搜索关键词 push 到 historyList 数组中
     this.historyList.push(this.kw)
    }
    }
    
  2. 上述实现思路存在的问题:

    • 关键词前后顺序的问题(可以调用数组的 reverse() 方法对数组进行反转)
    • 关键词重复的问题(可以使用 Set 对象进行去重操作)

5.3.4 解决关键字前后顺序的问题

  1. data中的 historyList 不做任何修改,依然使用push进行末尾追加

  2. 定义一个计算属性 histories ,将historyList数组reverse反转之后,就是此计算属性的值:

    computed: {
     histories() {
         return[...this.historyList].reverse()
     }
    }
    

    注意:由于数组是引用类型,所以不要直接基于原数组调用 reverse 方法,以免修改原数组中元素的顺序 而是应该新建一个内存无关的数组,再进行 reverse 反转

  3. 页面中渲染搜索关键词的时候,不再使用data中的historyList ,而是使用计算属性histories:

    <view>
     <uni-tag :text="item" v-for="(item,i) in histories" :key="i"></uni-tag>
    </view>
    

5.3.5 解决关键词重复的问题

修改 saveSearchHistory 方法如下:

// 保存搜索关键词为历史记录
saveSearchHistory() {
  // this.historyList.push(this.kw)

  // 1. 将 Array 数组转化为 Set 对象
  const set = new Set(this.historyList)
  // 2. 调用 Set 对象的 delete 方法,移除对应的元素
  set.delete(this.kw)
  // 3. 调用 Set 对象的 add 方法,向 Set 中添加元素
  set.add(this.kw)
  // 4. 将 Set 对象转化为 Array 数组
  this.historyList = Array.from(set)
}

5.3.6 将搜索历史记录持久化存储到本地

  1. 修改 saveSearchHistory 方法如下:
    //保存搜索关键词
    saveSearchHistory() {
     const set = new Set(this.historyList)
     set.delete(this.kw)
     set.add(this.kw)
     this.historyList = Array.from(set)
     //调用uni.setStorageSync(key,value) 将搜索记录持久化存储到本地
     uni.setStorageSync('kw',JSON.stringify(this.historyList))
    }
    
  2. onLoad生命周期函数中,加载本地存储的搜索历史记录:
    onLoad() {
     this.historyList = JSON.parse(uni.getStorageSync('kw') || '[]')
    }
    

    5.3.7 清空搜索历史记录

  3. 为清空的图标按钮绑定 click 事件:
    <uni-icons type="trash" size="17" @click="cleanHistory"></uni-icons>
    
  4. 在 methods 中定义 cleanHistory 处理函数:
    //清空搜索历史记录
    cleanHistory() {
     //清空 data 保存的搜索历史
     this.historyList = []
     //清空本地存储中的搜索历史
     uni.setStorageSync('kw','[]')
    }
    

5.3.8 点击搜索历史跳转到商品列表页面

  1. 为搜索历史的 item 绑定 click 事件处理函数:
    <uni-tag :text="item" v-for="(item, i) in historys" :key="i" @click="gotoGoodsList(item)"></uni-tag>
    

    item传进去,是因为要根据关键词来进行商品的展示

  2. 在 methods 中定义 gotoGoodsList 处理函数:
    gotoGoodsList(kw) {
     uni.navigateTo({
         url:'subpkg/goods_list/goods_list?query=' + kw
     })
    }
    

5.4 分支的合并与提交

  1. 将 search 分支进行本地提交:

    git add .
    git commit -m "完成了搜索功能的开发"
    
  2. 将本地的 search 分支推送到码云:

    git push -u origin search
    
  3. 将本地 search 分支中的代码合并到 master 分支:

    git checkout master
    git merge search
    git push
    
  4. 删除本地的 search 分支:

    git branch -d search
    

6.商品列表

6.0 创建 goodslist 分支

git checkout -b goodslist

6.1 定义请求参数对象

  1. 为了方便发起请求获取商品列表的数据,根据接口的要求, 事先定义一个请求参数对象
    data() {
    return {
     // 请求参数对象
     queryObj: {
       // 查询关键词
       query: '',
       // 商品分类Id
       cid: '',
       // 页码值
       pagenum: 1,
       // 每页显示多少条数据
       pagesize: 10
     }
    }
    }
    
  2. 将页面跳转时携带的参数,转存到 queryObj 对象中
    onLoad(options) {
    // 将页面参数转存到 this.queryObj 对象中
    this.queryObj.query = options.query || ''
    this.queryObj.cid = options.cid || ''
    }
    

6.2 获取商品列表数据

  1. 在data中新增如下的数据节点:

    data() {
    return {
     // 商品列表的数据
     goodsList: [],
     // 总数量,用来实现分页
     total: 0
    }
    }
    
  2. onLoad生命周期函数中,调用getGoodsList方法获取商品列表数据:

    onLoad(options) {
    // 调用获取商品列表数据的方法
    this.getGoodsList()
    }
    
  3. methods节点中,声明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
    }
    }
    

6.3 渲染商品列表结构

  1. 在页面中,通过 v-for 指令,循环渲染出商品的 UI 结构:

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

    使用block包裹:循环时不会被渲染为任何实际的元素,起到包裹性质的作用,结构更加清晰

  2. 为了防止某些商品的图片不存在,需要在 data 中定义一个默认的图片:

    data() {
    return {
     // 默认的空图片
     defaultPic: 'https://img3.doubanio.com/f/movie/8dd0c794499fe925ae2ae89ee30cd225750457b4/pics/movie/celebrity-default-medium.png'
    }
    }
    

    并在页面渲染时按需使用:

    <image :src="goods.goods_small_logo || defaultPic" class="goods-pic"></image>
    
  3. 美化商品列表的 UI 结构:

    .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;
     }
    }
    }
    

6.4 把商品 item 项封装为自定义组件

  1. 在 components 目录上鼠标右键,选择 新建组件: 

  2. 将 goods_list 页面中,关于商品 item 项相关的 UI 结构、样式、data 数据,封装到 my-goods 组件中:

    <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>
    
  3. 在 goods_list 组件中,循环渲染 my-goods 组件即可:

    <view class="goods-list">
    <block v-for="(item, i) in goodsList" :key="i">
     <!-- 为 my-goods 组件动态绑定 goods 属性的值 -->
     <my-goods :goods="item"></my-goods>
    </block>
    </view>
    

6.5 使用过滤器处理价格

  1. 在 my-goods 组件中,和 data 节点平级,声明 filters 过滤器节点如下:
    filters: {
    // 把数字处理为带两位小数点的数字
    tofixed(num) {
     return Number(num).toFixed(2)
    }
    }
    
  2. 在渲染商品价格的时候,通过管道符 | 调用过滤器:
    <!-- 商品价格 -->
    <view class="goods-price">¥{{goods.goods_price | tofixed}}</view>
    

6.6 上拉加载更多

6.6.1 初步实现上拉加载更多

  1. 打开项目根目录中的 pages.json 配置文件,为 subPackages 分包中的 goods_list 页面配置上拉触底的距离:

    "subPackages": [
    {
      "root": "subpkg",
      "pages": [
        {
          "path": "goods_detail/goods_detail",
          "style": {}
        },
        {
          "path": "goods_list/goods_list",
          "style": {
            "onReachBottomDistance": 150
          }
        },
        {
          "path": "search/search",
          "style": {}
        }
      ]
    }
    ]
    
  2. 在 goods_list 页面中,和 methods 节点平级,声明 onReachBottom 事件处理函数,用来监听页面的上拉触底行为:

    // 触底的事件
    onReachBottom() {
    // 让页码值自增 +1
    this.queryObj.pagenum += 1
    // 重新获取列表数据
    this.getGoodsList()
    }
    
  3. 改造 methods 中的 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
    }
    

6.6.2 通过节流阀防止发起额外的请求

  1. 在 data 中定义 isloading 节流阀如下:

    data() {
    return {
     // 是否正在请求数据
     isloading: false
    }
    }
    
  2. 修改 getGoodsList 方法,在请求数据前后,分别打开和关闭节流阀:

    // 获取商品列表数据的方法
    async getGoodsList() {
    // ** 打开节流阀
    this.isloading = true
    // 发起请求
    const { data: res } = await uni.$http.get('/api/public/v1/goods/search', this.queryObj)
    // ** 关闭节流阀
    this.isloading = false
    
    // 省略其它代码...
    }
    
  3. 在 onReachBottom 触底事件处理函数中,根据节流阀的状态,来决定是否发起请求:

    // 触底的事件
    onReachBottom() {
    // 判断是否正在请求其它数据,如果是,则不发起额外的请求
    if (this.isloading) return
    
    this.queryObj.pagenum += 1
    this.getGoodsList()
    }
    

6.6.3 判断数据是否加载完毕

  1. 如果下面的公式成立,则证明没有下一页数据了:

    当前的页码值 * 每页显示多少条数据 >= 总数条数
    pagenum * pagesize >= total
    

    记一下公式

  2. 修改 onReachBottom 事件处理函数如下:

    // 触底的事件
    onReachBottom() {
    // 判断是否还有下一页数据
    if (this.queryObj.pagenum * this.queryObj.pagesize >= this.total) return uni.$showMsg('数据加载完毕!')
    
    // 判断是否正在请求其它数据,如果是,则不发起额外的请求
    if (this.isloading) return
    
    this.queryObj.pagenum += 1
    this.getGoodsList()
    }
    

6.7 下拉刷新

  1. 在 pages.json 配置文件中,为当前的 goods_list 页面单独开启下拉刷新效果:

    "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": {}
    }]
    }]
    
  2. 监听页面的 onPullDownRefresh 事件处理函数:

    // 下拉刷新的事件
    onPullDownRefresh() {
    // 1. 重置关键数据
    this.queryObj.pagenum = 1
    this.total = 0
    this.isloading = false
    this.goodsList = []
    
    // 2. 重新发起请求
    this.getGoodsList(() => uni.stopPullDownRefresh())
    }
    
  3. 修改 getGoodsList 函数,接收 cb 回调函数并按需进行调用:

    // 获取商品列表数据的方法
    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()
    
    if (res.meta.status !== 200) return uni.$showMsg()
    this.goodsList = [...this.goodsList, ...res.message.goods]
    this.total = res.message.total
    }
    

6.8 点击商品 item 项跳转到详情页面

  1. 将循环时的 block 组件修改为 view 组件,并绑定 click 点击事件处理函数:
    <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>
    
  2. 在 methods 节点中,定义 gotoDetail 事件处理函数:
    // 点击跳转到商品详情页面
    gotoDetail(item) {
    uni.navigateTo({
     url: '/subpkg/goods_detail/goods_detail?goods_id=' + item.goods_id
    })
    }
    

6.9 分支的合并与提交

  1. 将 goodslist 分支进行本地提交:
    git add .
    git commit -m "完成了商品列表页面的开发"
    
  2. 将本地的 goodslist 分支推送到码云:
    git push -u origin goodslist
    
  3. 将本地 goodslist 分支中的代码合并到 master 分支:
    git checkout master
    git merge goodslist
    git push
    
  4. 删除本地的 goodslist 分支:
    git branch -d goodslist
    

7. 商品详情

7.0 创建 goodsdetail 分支

运行如下的命令,基于 master 分支在本地创建 goodsdetail 子分支,用来开发商品详情页相关的功能:

git checkout -b goodsdetail

7.1 添加商品详情页的编译模式

  1. 在微信开发者工具中,点击工具栏上的编译模式下拉菜单,选择 添加编译模式 选项: 

  2. 勾选 启动页面 的路径,并填写了 启动参数 之后,点击 确定 按钮,添加详情页面的编译模式: 

7.2 获取商品详情数据

  1. 在 data 中定义商品详情的数据节点:
    data() {
     return {
         // 商品详情对象
         goods_info: {}
     }
    }
    
  2. 在 onLoad 中获取商品的 Id,并调用请求商品详情的方法:
    onLoad(options) {
    // 获取商品 Id
    const goods_id = options.goods_id
    // 调用请求商品详情数据的方法
    this.getGoodsDetail(goods_id)
    }
    
  3. 在 methods 中声明 getGoodsDetail 方法:
    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
    }
    }
    

7.3 渲染商品详情页的 UI 结构

7.3.1 渲染轮播图区域

  1. 使用 v-for 指令,循环渲染如下的轮播图 UI 结构:

    <!-- 轮播图区域 -->
    <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>
    
  2. 美化轮播图的样式:

    swiper {
    height: 750rpx;
    
    image {
     width: 100%;
     height: 100%;
    }
    }
    

7.3.2 实现轮播图预览效果

  1. 为轮播图中的 image 图片绑定 click 事件处理函数:
    <swiper-item v-for="(item, i) in goods_info.pics" :key="i">
    <!-- 把当前点击的图片的索引,传递到 preview() 处理函数中 -->
    <image :src="item.pics_big" @click="preview(i)"></image>
    </swiper-item>
    
  2. 在 methods 中定义 preview 事件处理函数:
    // 实现轮播图的预览效果
    preview(i) {
    // 调用 uni.previewImage() 方法预览图片
    uni.previewImage({
     // 预览时,默认显示图片的索引
     current: i,
     // 所有图片 url 地址的数组
     urls: this.goods_info.pics.map(x => x.pics_big)
    })
    }
    

7.3.3 渲染商品信息区域

  1. 定义商品信息区域的 UI 结构如下:

    <!-- 商品信息区域 -->
    <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>
    
  2. 美化商品信息区域的样式:

    // 商品信息区域的样式
    .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;
    }
    }
    

7.3.4 渲染商品详情信息

  1. 在页面结构中,使用 rich-text 组件,将带有 HTML 标签的内容,渲染为小程序的页面结构:

    <!-- 商品详情信息 -->
    <rich-text :nodes="goods_info.goods_introduce"></rich-text>
    

    nodes属性可以渲染html对应标签

  2. 修改 getGoodsDetail 方法,从而解决图片底部 空白间隙 的问题:

    // 定义请求商品详情数据的方法
    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(/<img /g, '<img style="display:block;" ')
    this.goods_info = res.message
    }
    
  3. 解决 .webp 格式图片在 ios 设备上无法正常显示的问题:

    // 定义请求商品详情数据的方法
    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(/<img /g, '<img style="display:block;" ').replace(/webp/g, 'jpg')
    this.goods_info = res.message
    }
    

7.3.5 解决商品价格闪烁的问题

  1. 导致问题的原因:在商品详情数据请求回来之前,data 中 goods_info 的值为 {},因此初次渲染页面时,会导致 商品价格、商品名称 等闪烁的问题。
  2. 解决方案:判断 goods_info.goods_name 属性的值是否存在,存在才执行下面代码,从而使用 v-if 指令控制页面的显示与隐藏:
    <template>
    <view v-if="goods_info.goods_name">
    <!-- 省略其它代码 -->
    </view>
    </template>
    

7.4 渲染详情页底部的商品导航区域

7.4.1 渲染商品导航区域的 UI 结构

基于 uni-ui 提供的 GoodsNav 组件来实现商品导航区域的效果

  1. 在 data 中,通过 options 和 buttonGroup 两个数组,来声明商品导航组件的按钮配置对象:
    data() {
    return {
     // 商品详情对象
     goods_info: {},
     // 左侧按钮组的配置对象
     options: [{
       icon: 'shop',
       text: '店铺'
     }, {
       icon: 'cart',
       text: '购物车',
       info: 2
     }],
     // 右侧按钮组的配置对象
     buttonGroup: [{
         text: '加入购物车',
         backgroundColor: '#ff0000',
         color: '#fff'
       },
       {
         text: '立即购买',
         backgroundColor: '#ffa200',
         color: '#fff'
       }
     ]
    }
    }
    
  2. 在页面中使用 uni-goods-nav 商品导航组件:
    <!-- 商品导航组件 -->
    <view class="goods_nav">
    <!-- fill 控制右侧按钮的样式 -->
    <!-- options 左侧按钮的配置项 -->
    <!-- buttonGroup 右侧按钮的配置项 -->
    <!-- click 左侧按钮的点击事件处理函数 -->
    <!-- buttonClick 右侧按钮的点击事件处理函数 -->
    <uni-goods-nav :fill="true" :options="options" :buttonGroup="buttonGroup" @click="onClick" @buttonClick="buttonClick" />
    </view>
    
  3. 美化商品导航组件,使之固定在页面最底部:
    .goods-detail-container {
    // 给页面外层的容器,添加 50px 的内padding,
    // 防止页面内容被底部的商品导航组件遮盖
    padding-bottom: 50px;
    }
    .goods_nav {
    // 为商品导航组件添加固定定位
    position: fixed;
    bottom: 0;
    left: 0;
    width: 100%;
    }
    

    因为加入底部 nav功能的原因,原本信息会被挡住,所以在最外层容器赋予类名 添加 padding

7.4.2 点击跳转到购物车页面

  1. 点击商品导航组件左侧的按钮,会触发 uni-goods-nav 的 @click 事件处理函数,事件对象 e 中会包含当前点击的按钮相关的信息:
    // 左侧按钮的点击事件处理函数
    onClick(e) {
    console.log(e)
    }
    

    onClick()放在 methods 里 打印的按钮信息对象如下: 

  2. 根据 e.content.text 的值,来决定进一步的操作:
    // 左侧按钮的点击事件处理函数
    onClick(e) {
    if (e.content.text === '购物车') {
     // 切换到购物车页面
     uni.switchTab({
       url: '/pages/cart/cart'
     })
    }
    }
    

7.5 分支的合并与提交

  1. 将 goodsdetail 分支进行本地提交:
    git add .
    git commit -m "完成了商品详情页面goods_detail的开发"
    
  2. 将本地的 goodsdetail 分支推送到码云:
    git push -u origin goodsdetail
    
  3. 将本地 goodsdetail 分支中的代码合并到 master 分支:
    git checkout master
    git merge goodsdetail
    git push
    
  4. 删除本地的 goodsdetail 分支:
    git branch -d goodsdetail
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐