119.【Uniapp】

news2025/3/12 9:55:35

uni-app

  • (一)、uni-app 起步
    • 1.Uniapp简介
    • 2.Uniapp开发工具
        • (1).下载HbuilderX
        • (2).安装scss/sass编译
        • (3).快捷键方案切换
        • (4).修改编辑器的基本设置
    • 3.新建 uni-app项目
    • 4.uniapp 的目录结构
    • 5.把项目运行到微信开发者工具中
        • (1).填写自己的微信小程序的AppID:
        • (2).在HBuilderX中,配置微信开发者工具的安装路径
        • (3).在微信开发者工具中, 通过 设置-> 安全设置-面板,开启微信开发者工具的服务端口
        • (4).在Hbuilder中,点击菜单栏中的 运行->运行到小程序模拟器->微信开发者工具,将当前uni-app项目编译之后,自动运行到开发者工具中,从而方便查看项目效果与测试。
    • 6.使用Gite管理项目
        • (1).本地管理
        • (2).远程管理
  • (二)、配置页面
    • 1.配置页面 - tabBar
        • (1).创建tabBar分支
        • (2).创建tabBar页面
        • (3).配置tabBar效果
        • (4).修改导航条的样式效果
        • (5).分支的提交与合并
    • 2.配置页面 - 首页
        • (1).创建 home 分支
        • (2).配置网络请求
        • (3).请求轮播图的数据
        • (4).配置小程序分包
        • (5).点击轮播图跳转到商品的详情页面
        • (6).分装uni.$showMsg()方法
        • (7).分类导航区域 💱
        • (8).点击第一项跳转到分类页面
        • (9).楼层区域 💱
        • (10).点击楼层图片跳转到商品列表页
        • (11).分支的合并与提交
    • 3.配置页面 - 分类
        • (1).创建cate分支
        • (2).渲染分裂页面的基本结构
        • (3).获取分类数据
        • (4).动态渲染左侧的一级分类列表
        • (5).动态渲染右侧的二级分类列表
        • (6).动态渲染右侧的三级分类列表
        • (7).修改Bug-(我们点击一级分类不回顶部)
        • (8).点击三级分类跳转到商品列表页面
        • (9).提交合并页面
    • 4.配置页面 - 搜索
        • (1).创建search分支
        • (2).自定义搜索组件
        • (3).搜索建议
        • (4).搜索历史
        • (5).提交合并页面
    • 5.配置页面 - 商品列表
        • (1).创建goodslist分支
        • (2).定义请求参数对象
        • (3).获取商品列表数据
        • (4).渲染商品列表的数据结构
        • (5).把商品 item 项封装为自定义组件 (提升复用性)
        • (6).使用过滤器处理价格
        • (7).下拉加载更多
        • (8).商品点击item项目进入商品详情页
        • (9).分支的合并与提交
    • 6.配置页面 - 商品详情页
        • (1).创建goodsdetail分支
        • (2).获取商品详情数据
        • (3).渲染商品详情页的UI结构
        • (4).渲染详情页底部的商品导航区域
        • (5).分支的合并与提交
    • 7.配置页面 - 加入购物车
        • (1).创建 cart 分支
        • (2). 配置 vuex (uniapp封装了Vue不用安装vuex)
        • (3). 创建购物车的 store 模块
        • (4).在商品详情页中使用 Store 中的数据
        • (5).实现加入购物车的功能
        • (6). 动态统计购物车中商品的总数量
        • (7).持久化存储购物车的功能
        • (8).优化商品详情页的监听器
        • (9).动态为 tabBar 页面设置数字徽标
        • (10).将设置 tabBar 徽标的代码抽离为 mixins
    • 8.Vuex 数据共享步骤 ⭐
        • (1).创建store.js并共享
        • (2).挂载到主入口文件上
        • (3).创建共享的数据.js
        • (4).引入要共享的数据.js
        • (5).引入/使用共享的数据
    • 9.配置页面 - 商品列表区域
        • (1).渲染购物车商品列表的标题区域
        • (2).渲染商品列表区域的基本结构
        • (3).my-goods 组件封装 radio 勾选状态
        • (4). 为 my-goods 组件封装 radio-change 事件
        • (5).修改购物车中商品的勾选状态
        • (6). 为 my-goods 组件封装 NumberBox
        • (7).为 my-goods 组件封装 num-change 事件
        • (8).渲染滑动删除的 UI 效果
        • (9).实现滑动删除的功能
    • 10.配置页面 - 收货地址区域
        • (1). 创建收货地址组件
        • (2).实现收货地址区域的按需展示
        • (3).现选择收货地址的功能
        • (4).将 address 信息存储到 vuex 中
        • (5).将 Store 中的 address 持久化存储到本地
        • (6).将 addstr 抽离为 getters
        • (7).重新选择收货地址
    • 11.配置页面 - 结算区域
        • (1).把结算区域封装为组件
        • (2).渲染结算区域的结构和样式
        • (3). 动态渲染已勾选商品的总数量
        • (5).动态渲染全选按钮的选中状态
        • (6). 实现商品的全选/反选功能
        • (7). 动态渲染已勾选商品的总价格
        • (8).渲染购物车为空时的页面结构
        • (9).分支的合并与提交
    • 12.配置页面 - 登入与支付
        • (1).创建settle分支
        • (2). 点击结算按钮进行条件判断
        • (3).登入页面
        • (4). 实现登录和用户信息组件的按需展示
        • (5).实现登入组件的基本布局
        • (6).点击登录按钮获取微信用户的基本信息

(一)、uni-app 起步

1.Uniapp简介

uni-app 是一个使用 Vue.js 开发所有前端应用的框架,开发者编写一套代码,可发布到iOS、Android、Web(响应式)、以及各种小程序(微信/支付宝/百度/头条/飞书/QQ/快手/钉钉/淘宝)、快应用等多个平台。

uni-app在手,做啥都不愁。即使不跨端,uni-app也是更好的小程序开发框架(详见)、更好的App跨平台框架、更方便的H5开发框架。不管领导安排什么样的项目,你都可以快速交付,不需要转换开发思维、不需要更改开发习惯。

2.Uniapp开发工具

可视化的方式比较简单,HBuilderX内置相关环境,开箱即用,无需配置nodejs。

  • 模板丰富
  • 完善的只能提示
  • 一键运行

(1).下载HbuilderX

官网链接 : https://www.dcloud.io/hbuilderx.html
在这里插入图片描述
第一步:
在这里插入图片描述

(2).安装scss/sass编译

为了方便编写样式,建议安装 scss/sass编译插件。

插件地址: https://ext.dcloud.net.cn/plugin?name=compile-node-sass

进入插件下载页面之后,点击右上角的 使用 HbuilderX 导入插件按钮进行自动安装。

在这里插入图片描述
在这里插入图片描述

(3).快捷键方案切换

操作步骤: 工具 -> 预设快捷方式切换 -> Vscode

在这里插入图片描述

(4).修改编辑器的基本设置

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

{
  "editor.colorScheme": "Default",
  "editor.fontSize": 12,
  "editor.fontFamily": "Consolas",
  "editor.fontFmyCHS": "微软雅黑 Light",
  "editor.insertSpaces": true,
  "editor.lineHeight": "1.5",
  "editor.minimap.enabled": false,
  "editor.mouseWheelZoom": true,
  "editor.onlyHighlightWord": false,
  "editor.tabSize": 2,
  "editor.wordWrap": true,
  "explorer.iconTheme": "vs-seti",
  "editor.codeassist.px2rem.enabel": false,
  "editor.codeassist.px2upx.enabel": false
}

3.新建 uni-app项目

1.文件->新建->项目

在这里插入图片描述
2.填写项目基本信息
在这里插入图片描述

4.uniapp 的目录结构

┌─components            uni-app组件目录
│  └─comp-a.vue         可复用的a组件
├─pages                 业务页面文件存放的目录
│  ├─index
│  │  └─index.vue       index页面
│  └─list
│     └─list.vue        list页面
├─static                存放应用引用静态资源(如图片、视频等)的目录,注意:静态资源只能存放于此
├─main.js               Vue初始化入口文件
├─App.vue               应用配置,用来配置小程序的全局样式、生命周期函数等
├─manifest.json         配置应用名称、appid、logo、版本等打包信息
└─pages.json            配置页面路径、页面窗口样式、tabBar、navigationBar 等页面类信息

5.把项目运行到微信开发者工具中

(1).填写自己的微信小程序的AppID:

在这里插入图片描述

(2).在HBuilderX中,配置微信开发者工具的安装路径

在这里插入图片描述

(3).在微信开发者工具中, 通过 设置-> 安全设置-面板,开启微信开发者工具的服务端口

在这里插入图片描述

(4).在Hbuilder中,点击菜单栏中的 运行->运行到小程序模拟器->微信开发者工具,将当前uni-app项目编译之后,自动运行到开发者工具中,从而方便查看项目效果与测试。

在这里插入图片描述
取消黄色警告!
在这里插入图片描述

6.使用Gite管理项目

(1).本地管理

1.在项目目录中创建 .gitgnore 葫芦娃文件,并配置如下:

/node_modules
/unpackage/dist

在这里插入图片描述

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

此时,为了让Git能够正常最总unpackage目录,按照管理额,我们可以在unpackage目录下创建一个教 .gitkeep的文件进行占位置

在这里插入图片描述
2.打开终端,切换到项目根目录中,运行如下的命令,初始化本地Git仓库

在这里插入图片描述
shift+鼠标右键
在这里插入图片描述

git init

3.将所有的文件都加入到暂存区

git add .

4.本地提交更新

git commit -m "第一次初始化项目"

(2).远程管理

1.新建一个仓库
在这里插入图片描述

将本地的仓库和远程的仓库链接一下
git remote add origin git@gitee.com:lwt121788/uniapp-shop2.git
将本地仓库推送上远程仓库
git push -u origin "master"

在这里插入图片描述

(二)、配置页面

1.配置页面 - tabBar

(1).创建tabBar分支

运行如下的命令,基于master分支在本地创建tabBar分支,用来开发和tabBar相关的功能。
1.创建新分支 tabbar

在git中,可利用checkout命令转换分支,该命令的作用就是切换分支或恢复工作树文件,语法为“git checkout 分支名”;当参数设置为“-b”时,可以在新分支创建的同时切换分支,语法为“git checkout -b 分支名”。

在这里插入代码片
git checkout -b tabbar

2.查看分支

git branch

(2).创建tabBar页面

page 目录中,创建首页(home)、分类(sort)、购物车(cart)、我的(my)这四个tabBar页面。在HBuilderX中,可以通过如下的两个步骤,快速新建页面:

  1. Page目录上鼠标右键,选择新建页面
  2. 在弹出的窗口中,填写页面的名称、勾选scss模板之后,点击创建按钮。

在这里插入图片描述创建完毕代码之后:

在这里插入图片描述

(3).配置tabBar效果

1.将姊俩目录下的 static 文件夹 拷贝一份,替换掉原来根目录中的 static 文件夹

就是拷贝静态资源-图片的

2.修改项目根目录中的 pages.json 配置文件,新增 tabBar的配置节点如下:

{
  "tabBar": {
    // 1.设置选中文本的颜色
    "selectedColor": "#C00000",
    // 2.设置tabBar的页面
    "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"
      }
    ]
  },

  "pages": [{
    "path": "pages/home/home",
    "style": {
      "navigationBarTitleText": "",
      "enablePullDownRefresh": false
    }
  }, {
    "path": "pages/cate/cate",
    "style": {
      "navigationBarTitleText": "",
      "enablePullDownRefresh": false
    }

  }, {
    "path": "pages/cart/cart",
    "style": {
      "navigationBarTitleText": "",
      "enablePullDownRefresh": false
    }

  }, {
    "path": "pages/my/my",
    "style": {
      "navigationBarTitleText": "",
      "enablePullDownRefresh": false
    }

  }],
  "globalStyle": {
    "navigationBarTextStyle": "black",
    "navigationBarTitleText": "uni-app",
    "navigationBarBackgroundColor": "#F8F8F8",
    "backgroundColor": "#F8F8F8",
    "app-plus": {
      "background": "#efeff4"
    }
  }
}

3.我们运行的时候会发现 不会跳转到有tabBar的页面。原因是我们需要删除默认的index页面即可
在这里插入图片描述
3.Hbuilder启动一次之后就处于热重载效果,每次更新数据之后。我们只需要在微信小程序里面重新更新即可刷新数据

在这里插入图片描述

(4).修改导航条的样式效果

1.打开 page.json 这个全局的配置文件
在这里插入图片描述

2.修改 globstyle 节点如下

  "globalStyle": {
    "navigationBarTextStyle": "white",
    // 文本
    "navigationBarTitleText": "掌上周口",
    // 导航栏背景
    "navigationBarBackgroundColor": "#C00000",
    "backgroundColor": "#FFFFFF",
    "app-plus": {
      "background": "#efeff4"
    }
  }

在这里插入图片描述

(5).分支的提交与合并

1.将本地的tabbar分支进行本地的commiit提交:(当前在分支)

git checkout 分支名
git add .
git commit -m "完成了tabBar开发"

2.将本地的tabBar分支推送到远程仓库进行保存(当前在分支)

git push -u origin tabbar

在这里插入图片描述

3.将本地的tabbar分支合并到本地的master分支;(切换到主分支)

选择主分支
git checkout master

获取tabbar分支的内容
git merge tabbar 

提交主分支
git push

4.删除本地的tabbar分支(删除某一个分支的时候,我们的位置不能站在这个分支上)

git branch -d tabbar

在这里插入图片描述
结果:
在这里插入图片描述

2.配置页面 - 首页

(1).创建 home 分支

运行如下的命令,基于master分支在本地创建home子分支,用来开发和home相关首页相关的功能:

git checkout -b home

在这里插入图片描述

(2).配置网络请求

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

官方文档: https://www.npmjs.com/package/@escook/request-miniprogram
在这里插入图片描述

  1. 安装包

1.打开终端并初始化npm

npm init -y

在这里插入图片描述

2.安装

npm install @escook/request-miniprogram

在这里插入图片描述
3.导入文件
uni是顶级对象,不是作者规定的而是 uni-app规定的

// 按需导入 $http 对象
import { $http } from '@escook/request-miniprogram'

// 将按需导入的 $http 挂载到 wx 顶级对象之上,方便全局调用
wx.$http = $http

// 在 uni-app 项目中,可以把 $http 挂载到 uni 顶级对象之上,方便全局调用
uni.$http = $http

main.js Vue的入口文件

// #ifndef VUE3
import Vue from 'vue'
import App from './App'
// 1.导入网络请求的包 ⭐
import {
  $http
} from '@escook/request-miniprogram'
// 2.在 uni-app 项目中,可以把 $http 挂载到 uni 顶级对象之上,方便全局调用⭐⭐
uni.$http = $http

// 3.请求开始之前做一些事情  (请求拦截器)⭐⭐⭐
$http.beforeRequest = function(options) {
  // do somethimg...
  uni.showLoading({
    title: '数据加载中...'
  })
}
// 4.请求完成之后做一些事情 (响应拦截器)⭐⭐⭐⭐
$http.afterRequest = function () {
  // do something...
  uni.hideLoading()
}

Vue.config.productionTip = false

App.mpType = 'app'

const app = new Vue({
  ...App
})
app.$mount()
// #endif

// #ifdef VUE3
import {
  createSSRApp
} from 'vue'
import App from './App.vue'
export function createApp() {
  const app = createSSRApp(App)
  return {
    app
  }
}
// #endif

(3).请求轮播图的数据

  1. 实现步骤
  1. 在data中定义轮播图的数组。
  2. 在onload生命周期函数中调用获取轮播图数据的方法。
  3. 在methods中定义获取轮播图数据的方法。
  1. 绑定跟URL

主要是为了防止域名混乱的情况-域名地狱

main.js

// ⭐ 设置根路径目的是为了防止 网址混乱
$http.baseUrl = 'https://api-hmugo-web.itheima.net'
// #ifndef VUE3
import Vue from 'vue'
import App from './App'
// 1.导入网络请求的包 ⭐
import {
  $http
} from '@escook/request-miniprogram'
// 2.在 uni-app 项目中,可以把 $http 挂载到 uni 顶级对象之上,方便全局调用 ⭐⭐
uni.$http = $http

// ⭐⭐⭐ 设置根路径目的是为了防止 网址混乱
$http.baseUrl = 'https://api-hmugo-web.itheima.net'

// 3.请求开始之前做一些事情  (请求拦截器) ⭐⭐⭐⭐
$http.beforeRequest = function(options) {
  // do somethimg...
  uni.showLoading({
    title: '数据加载中...'
  })
}
// 4.请求完成之后做一些事情 (响应拦截器)⭐⭐⭐⭐⭐
$http.afterRequest = function() {
  // do something...
  uni.hideLoading()
}

Vue.config.productionTip = false

App.mpType = 'app'

const app = new Vue({
  ...App
})
app.$mount()
// #endif

// #ifdef VUE3
import {
  createSSRApp
} from 'vue'
import App from './App.vue'
export function createApp() {
  const app = createSSRApp(App)
  return {
    app
  }
}
// #endif
  1. 实现网络请求

home.vue

  export default {
    data() {
      return {
        // 轮播图的数据节点
        swiperList: []
      };
    },
    // ⭐ 挂载事件
    onLoad() {
      this.getSwiperList()
    },
    methods: {
    // ⭐⭐ 请求网络数据
      async getSwiperList() {
        // 通过花括号结构对象,以key:value的形式赋值。引用数据类型赋值穿的是对象的内存地址,该变量可以指向目标对象 {data :res} 将 响应结果的data赋值给res
        const {
          data: res
        } = await uni.$http.get('/api/public/v1/home/swiperdata')
        console.log(res)
        if (res.meta.status == 200) { // 假如说返回的状态码为 200 说明成功
          // ⭐⭐⭐ 赋值数据
          this.swiperList = res.message
          // 正常调用弹窗应该用的是wx 但是在uniapp看i面我们推荐使用 uni
          return uni.showToast({
            title: '数据请求成功',
            duration: 1500,
            icon: 'success'
          })
        } else {
          return uni.showToast({
            title: '数据请求失败',
            duration: 1500,
            icon: 'error'
          })
        }
      }
    }
  }
<template>
  <view>
    <h1>Home</h1>
  </view>
</template>

<script>
  export default {
    data() {
      return {
        // 轮播图的数据节点
        swiperList: []
      };
    },
    // ⭐ 挂载事件
    onLoad() {
      this.getSwiperList()
    },
    methods: {
    // ⭐⭐ 请求网络数据
      async getSwiperList() {
        // 通过花括号结构对象,以key:value的形式赋值。引用数据类型赋值穿的是对象的内存地址,该变量可以指向目标对象 {data :res} 将 响应结果的data赋值给res
        const {
          data: res
        } = await uni.$http.get('/api/public/v1/home/swiperdata')
        console.log(res)
        if (res.meta.status == 200) { // 假如说返回的状态码为 200 说明成功
          // ⭐⭐⭐ 赋值数据
          this.swiperList = res.message
          // 正常调用弹窗应该用的是wx 但是在uniapp看i面我们推荐使用 uni
          return uni.showToast({
            title: '数据请求成功',
            duration: 1500,
            icon: 'success'
          })
        } else {
          return uni.showToast({
            title: '数据请求失败',
            duration: 1500,
            icon: 'error'
          })
        }
      }
    }
  }
</script>

<style lang="scss">

</style>

使用花括号赋值之前
在这里插入图片描述
使用花括号赋值之后
在这里插入图片描述
运行结果:
在这里插入图片描述

  1. 遍历轮播图的数据并渲染数据

home.vue

<template>
  <view>
    <!-- 轮播图的区域  快捷键 usw -->
    <swiper :indicator-dots="true" :autoplay="true" :interval="3000" :duration="1000" :circular="true">
      <swiper-item v-for="(item,index) in  swiperList" :key="index">
        <!-- 利用Vue的语法操作, v-bind:src='xx' :src='xx' -->
        <image :src="item.image_src" mode="widthFix"></image>
      </swiper-item>
    </swiper>
  </view>
</template>

<style lang="scss">
  swiper {
    height: 330rpx;
// 这个意思就是在 swiper里面有一个类名叫做 swiper-item和一个image标签进行操作
    .swiper-item,
    image {
      width: 100%;
      height: 100%;
    }
  }
</style>

在这里插入图片描述

(4).配置小程序分包

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

为此,我们在项目中,把 tabBar 相关的 4 个页面放到主包中,其它页面(例如:商品详情页、商品列表页)放到分包中。在 uni-app 项目中,配置分包的步骤如下:

  1. 在项目根目录中,创建分包的根目录,命名为 subpkg
    在这里插入图片描述

  2. pages.json 中,和 pages 节点平级的位置声明subPackages节点,用来定义分包相关的结构:

  // 配置分包 ⭐
  "subPackages": [{
    "root": "subpkg", //指定分包的根路径在那
    "pages": [ //配置在这个根路径下 有几个分包..
			xxxx
    ]
  }],

3.在 subpkg 目录上鼠标右键,点击 新建页面 选项,并填写页面的相关信息:
在这里插入图片描述
在这里插入图片描述

(5).点击轮播图跳转到商品的详情页面

<swiper-item></swiper-item>节点内的view组件,改造为 navigator导航组件,并动态绑定 url属性的值。

1. 改造之前的UI界面
home.vue

<template>
  <view>
    <!-- 轮播图的区域  快捷键 usw -->
    <swiper :indicator-dots="true" :autoplay="true" :interval="3000" :duration="1000" :circular="true">
      <swiper-item v-for="(item,index) in  swiperList" :key="index">
        <!-- 利用Vue的语法操作, v-bind:src='xx' :src='xx' -->
        <image :src="item.image_src" mode="widthFix"></image>
      </swiper-item>
    </swiper>
  </view>
</template>

2.改造之后的UI界面

<template>
  <view>
    <!-- 轮播图的区域  快捷键 usw -->
    <swiper :indicator-dots="true" :autoplay="true" :interval="3000" :duration="1000" :circular="true">
      <swiper-item v-for="(item,index) in  swiperList" :key="index">
        <!-- 利用Vue的语法操作, v-bind:src='xx' :src='xx' -->
        <navigator class="swiper-item" :url="'/subpkg/goods_detail/goods_detail?goods_id='+item.goods_id">
          <image :src="item.image_src" mode="widthFix"></image>
        </navigator>
      </swiper-item>
    </swiper>
  </view>
</template>

在这里插入图片描述
运行之后的结果是:
在这里插入图片描述

(6).分装uni.$showMsg()方法

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

1.在main.js中,为uni对象挂载自定义的 $showMsg()方法:

为uni挂载一个方法。调用的方式是 : uni.$showMsg

原有的方法是:

uni.showToast({
            title: '数据请求失败',
            duration: 1500,
            icon: 'error'
          })

改造之后的方法

// 封装的展示消息提示的方法
uni.$showMsg = function (title = '数据加载失败!', duration = 1500) {
  uni.showToast({
    title,
    duration,
    icon: 'none',
  })
}

在这里插入图片描述

(7).分类导航区域 💱

  1. 获取分类导航的区域

实现思路:

  1. 定义data数据。
  2. 在onLoad区域挂载调用获取的方法。
  3. 在methods区域获取数据的方法。

home.vue

      // 分类导航区
      async getNavList() {
        const {
          data: res
        } = await uni.$http.get('/api/public/v1/home/catitems')
        console.log('分类导航区->', res)
        if (res.meta.status == 200) {
          this.navList = res.message
        } else {
          uni.$showMsg()
        }
      }
<template>
  <view>
    <!-- 轮播图的区域  快捷键 usw -->
    <swiper :indicator-dots="true" :autoplay="true" :interval="3000" :duration="1000" :circular="true">
      <swiper-item v-for="(item,index) in  swiperList" :key="index">
        <!-- 利用Vue的语法操作, v-bind:src='xx' :src='xx' -->
        <navigator class="swiper-item" :url="'/subpkg/goods_detail/goods_detail?goods_id='+item.goods_id">
          <image :src="item.image_src" mode="widthFix"></image>
        </navigator>
      </swiper-item>
    </swiper>
  </view>
</template>

<script>
  export default {
    data() {
      return {
        // 轮播图的数据节点
        swiperList: [],
        // 分类导航栏的数据列表
        navList: []
      };
    },
    onLoad() {
      this.getSwiperList()
      this.getNavList()
    },
    methods: {
      // 首页轮播图***
      async getSwiperList() {
        // 通过花括号结构对象,以key:value的形式赋值。引用数据类型赋值穿的是对象的内存地址,该变量可以指向目标对象 {data :res} 将 响应结果的data赋值给res
        const {
          data: res
        } = await uni.$http.get('/api/public/v1/home/swiperdata')
        console.log('首页轮播图->', res)
        if (res.meta.status == 200) { // 假如说返回的状态码为 200 说明成功
          this.swiperList = res.message
        } else {
          // 正常调用弹窗应该用的是wx 但是在uniapp看i面我们推荐使用 uni
          uni.$showMsg()
        }
      },
      // 分类导航区
      async getNavList() {
        const {
          data: res
        } = await uni.$http.get('/api/public/v1/home/catitems')
        console.log('分类导航区->', res)
        if (res.meta.status == 200) {
          this.navList = res.message
        } else {
          uni.$showMsg()
        }
      }
    }
  }
</script>

<style lang="scss">
  swiper {
    height: 330rpx;

    // 这个意思就是在 swiper里面有一个类名叫做 swiper-item和一个image标签进行操作
    .swiper-item,
    image {
      width: 100%;
      height: 100%;
    }
  }
</style>
  1. 渲染分类的导航区域

home.vue

  // 分类导航的样式
  .nav-list {
    display: flex; // 分布在一行
    justify-content: space-around;
    margin: 30rpx 0;

    .nav-img {
      width: 120rpx;
      height: 140rpx;
    }
  }
    <!-- 分类的导航区域 -->
    <view class="nav-list">
      <view v-for="(item,index) in navList" :key="index" class="nav-item">
        <image :src="item.image_src" class="nav-img"></image>
      </view>
    </view>

在这里插入图片描述

(8).点击第一项跳转到分类页面

  1. nav-item绑定点击事件处理函数
    <view class="nav-list">
      <view v-for="(item,index) in navList" :key="index" class="nav-item">
        <!-- 编写点击事件 并->传递每行的item这个项 -->
        <image :src="item.image_src" class="nav-img" @click="navClickHandler(item)"></image>
      </view>
    </view>

2.定义事件处理函数
switchTab 这个函数是切换到 tabbar标签页的

      // 分类点击图片事件
      navClickHandler(item) {
        if (item.name === '分类') {
          uni.switchTab({
            url: '/pages/cate/cate'
          })
        }
      }

在这里插入图片描述

(9).楼层区域 💱

  1. 获取楼层数据

实现思路:

  1. 定义data数据
  2. 在onLoad中调用获取数据的方法
  3. 在methods中定义获取数据的方法
<template>
  <view>
    <!-- 轮播图的区域  快捷键 usw -->
    <swiper :indicator-dots="true" :autoplay="true" :interval="3000" :duration="1000" :circular="true">
      <swiper-item v-for="(item,index) in  swiperList" :key="index">
        <!-- 利用Vue的语法操作, v-bind:src='xx' :src='xx' -->
        <navigator class="swiper-item" :url="'/subpkg/goods_detail/goods_detail?goods_id='+item.goods_id">
          <image :src="item.image_src" mode="widthFix"></image>
        </navigator>
      </swiper-item>
    </swiper>
    <!-- 分类的导航区域 -->
    <view class="nav-list">
      <view v-for="(item,index) in navList" :key="index" class="nav-item">
        <!-- 编写点击事件 并->传递每行的item这个项 -->
        <image :src="item.image_src" class="nav-img" @click="navClickHandler(item)"></image>
      </view>
    </view>

  </view>
</template>

<script>
  export default {
    data() {
      return {
        // 轮播图的数据节点
        swiperList: [],
        // 分类导航栏的数据列表
        navList: [],
        // 楼层的数据列表
        floorList: []
      };
    },
    onLoad() {
      this.getSwiperList(),
        this.getNavList(),
        this.getFloorList()
    },
    methods: {
      // 首页轮播图***
      async getSwiperList() {
        // 通过花括号结构对象,以key:value的形式赋值。引用数据类型赋值穿的是对象的内存地址,该变量可以指向目标对象 {data :res} 将 响应结果的data赋值给res
        const {
          data: res
        } = await uni.$http.get('/api/public/v1/home/swiperdata')
        console.log('home页轮播图->', res)
        if (res.meta.status == 200) { // 假如说返回的状态码为 200 说明成功
          this.swiperList = res.message
        } else {
          // 正常调用弹窗应该用的是wx 但是在uniapp看i面我们推荐使用 uni
          uni.$showMsg()
        }
      },
      // 分类导航区
      async getNavList() {
        const {
          data: res
        } = await uni.$http.get('/api/public/v1/home/catitems')
        console.log('home页分类导航区->', res)
        if (res.meta.status == 200) {
          this.navList = res.message
        } else {
          uni.$showMsg()
        }
      },
      // 分类点击图片事件
      navClickHandler(item) {
        if (item.name === '分类') {
          uni.switchTab({
            url: '/pages/cate/cate'
          })
        }
      },
      async getFloorList() {
        const {
          data: res
        } = await uni.$http.get('/api/public/v1/home/floordata')
        console.log('home页楼层的数据->', res)
        if (res.meta.status === 200) {
          this.floorList = res.message
        } else {
          uni.$showMsg()
        }
      }
    }
  }
</script>

<style lang="scss">
  // 轮播图的美化样式
  swiper {
    height: 330rpx;

    // 这个意思就是在 swiper里面有一个类名叫做 swiper-item和一个image标签进行操作
    .swiper-item,
    image {
      width: 100%;
      height: 100%;
    }
  }

  // 分类导航的样式
  .nav-list {
    display: flex; // 分布在一行
    justify-content: space-around;
    margin: 30rpx 0;

    .nav-img {
      width: 120rpx;
      height: 140rpx;
    }
  }
</style>
  1. 渲染图层的标题
  // 楼层标题
  .floor-title {
    width: 100%;
    height: 60rpx;
  }
    <!-- 楼层包裹性容器 -->
    <view class="floor-list">
      <!-- 每一个口曾的item项 -->
      <view v-for="(item,index) in floorList" :key="index">
        <image :src="item.floor_title.image_src" mode="" class="floor-title"></image>
      </view>
    </view>

在这里插入图片描述

在这里插入图片描述

  1. 渲染楼层中的图片

home.vue

    <!-- 楼层包裹性容器 -->
    <view class="floor-list">
      <!-- 每一个口曾的item项 -->
      <view v-for="(item,index) in floorList" :key="index" class="floor-item">
        <image :src="item.floor_title.image_src" mode="" class="floor-title"></image>
        <!-- 楼层的图片区域 -->
        <view class="floor-img-box">
          <!-- 左侧大图片的盒子 -->
          <view class="left-img-box">
            <!-- 获取索引为0的这个大图片  动态绑定src 和 style  -->
            <image :src="item.product_list[0].image_src" :style="{width:item.product_list[0].image_width+'rpx'}"
              mode="widthFix">
            </image>
          </view>
          <!-- 右侧四个小盒子   动态绑定src。但因为我们左侧的图片是索引第一张也会遍历出来,所以我们通过判断索引进行判断的操作-->
          <view class="right-img-box">
            <view class="right-img-item" v-for=" (item2,index2) in item.product_list" :key="index2" v-show="index2!=0">
              <image :src="item2.image_src" mode="widthFix" :style="{width:item2.image_width+'rpx'}"></image>
            </view>
          </view>
        </view>
      </view>
    </view>
// 楼层标题
  .floor-title {
    width: 100%;
    height: 60rpx;
    display: flex;
  }

  .right-img-box {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-around;
  }

  .floor-img-box {
    display: flex;
    padding-left: 10rpx;
  }

在这里插入图片描述

(10).点击楼层图片跳转到商品列表页

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

在这里插入图片描述
2.楼层请求成功之后,通过双层forEac循环,处理URL地址
home.vue
为什么需要进行处理URL地址呢? 因为后端传递过来的数据页面是:/pages/goods_list?query=x而我们需要的是/subpkg/goods_list/goods_list?query=x所以我们要处理地址并将参数通过split()分割出来。

      // 获取图层的数据
      async getFloorList() {
        const {
          data: res
        } = await uni.$http.get('/api/public/v1/home/floordata')
        console.log('home页楼层的数据->', res)
        if (res.meta.status === 200) {
          // 对数据进行处理⭐⭐⭐
          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
        } else {
          uni.$showMsg()
        }
      }

3.将我们的左侧大图片和右侧四个小图片的view修改成navigator

    <!-- 楼层包裹性容器 -->
    <view class="floor-list">
      <!-- 每一个口曾的item项 -->
      <view v-for="(item,index) in floorList" :key="index" class="floor-item">
        <image :src="item.floor_title.image_src" mode="" class="floor-title"></image>
        <!-- 楼层的图片区域 -->
        <view class="floor-img-box">
          <!-- 左侧大图片的盒子 --> ⭐⭐
          <navigator class="left-img-box" :url="item.product_list[0].url">
            <!-- 获取索引为0的这个大图片  动态绑定src 和 style  -->
            <image :src="item.product_list[0].image_src" :style="{width:item.product_list[0].image_width+'rpx'}"
              mode="widthFix">
            </image>
          </navigator>
          <!-- 右侧四个小盒子   动态绑定src。但因为我们左侧的图片是索引第一张也会遍历出来,所以我们通过判断索引进行判断的操作-->
          <view class="right-img-box">⭐⭐⭐
            <navigator class="right-img-item" v-for=" (item2,index2) in item.product_list" :key="index2"
              v-show="index2!=0" :url="item2.url">
              <image :src="item2.image_src" mode="widthFix" :style="{width:item2.image_width+'rpx'}"></image>
            </navigator>
          </view>
        </view>
      </view>
    </view>

在这里插入图片描述

(11).分支的合并与提交

  1. 将本地的 home 分支进行本地的 commit 提交:
查看是否在分支目录上
git branch
查看哪些数据可以添加(红色)
git status
添加的操作
git add .
查看是否添加成功(绿色成功)
git status
提交本地仓库
git commit -m "完成了 home 首页的开发"

在这里插入图片描述

  1. 将本地的 home 分支推送到远程仓库进行保存:
git push -u origin home

在这里插入图片描述
在这里插入图片描述

  1. 将本地的 home 分支合并到本地的 master 分支:
git checkout master
git add .
git commit -m "完成了home开发"
git merge home
强制推送到远程仓库
git push -f origin master 
  1. 删除本地的 home 分支:
git branch -d home

在这里插入图片描述

3.配置页面 - 分类

(1).创建cate分支

运行下面的命令,基于master分支在本地创建cate分支,用来开发分类页面相关的功能:

git checkout -b cate

在这里插入图片描述
修改编译模式:
在这里插入图片描述

(2).渲染分裂页面的基本结构

  1. 定义页面结构如下:

cate.vue

<template>
  <view>
    <!-- 滑动区域 -->
    <view class="scroll-view-container">
      <!-- 左侧的滑动区域 -->
      <scroll-view scroll-y="true" style="height: 300px;" class="left-scroll-view">
        <view>xxxx</view>
        <view>xxxx</view>
        <view>xxxxx</view>
      </scroll-view>
      <!-- 右侧的滑动区域 -->
      <scroll-view scroll-y="true" style="height: 300px;">
        <view>xxxx</view>
        <view>xxxx</view>
        <view>xxxx</view>
        <view>xxxx</view>
        <view>xxxx</view>
      </scroll-view>
    </view>
  </view>
</template>

<script>
  export default {
    data() {
      return {

      };
    }
  }
</script>

<style lang="scss">
  .scroll-view-container {
    display: flex;

    .left-scroll-view {
      width: 120px;
    }
  }
</style>

在这里插入图片描述

  1. 动态计算窗口的剩余高度:

Uniapp提供了 getsysteminfosync()获取屏幕宽度的方法

在这里插入图片描述
cate.vue

<script>
  export default {
    data() {
      return {
        // 当前设备可用的高度
        wh: 0
      };
    },
    onLoad() {
      const systemInfo = uni.getSystemInfoSync()
      console.log('手机设备信息->', systemInfo)
      this.wh = systemInfo.windowHeight
    }
  }
</script>
<template>
  <view>
    <!-- 滑动区域 -->
    <view class="scroll-view-container">
      <!-- 左侧的滑动区域 -->
      <scroll-view scroll-y="true" class="left-scroll-view" :style="{height:wh+'px'}">
        <view>xxxx</view>

        <view>xxxxx</view>
        <view>xxxxx</view>
      </scroll-view>
      <!-- 右侧的滑动区域 -->
      <scroll-view scroll-y="true" :style="{height:wh+'px'}">
        <view>xxxx</view>
        <view>xxxx</view>
        <view>xxxx</view>
        <view>xxxx</view>
      </scroll-view>
    </view>
  </view>
</template>

<script>
  export default {
    data() {
      return {
        // 当前设备可用的高度
        wh: 0
      };
    },
    onLoad() {
      const systemInfo = uni.getSystemInfoSync()
      console.log('手机设备信息->', systemInfo)
      this.wh = systemInfo.windowHeight
    }
  }
</script>

<style lang="scss">
  .scroll-view-container {
    display: flex;

    .left-scroll-view {
      width: 120px;
    }
  }
</style>

在这里插入图片描述

  1. 美化页面结构

cate.vue

 // 左侧选中区域  $.xxx 就是说如果说 即包括.left-scroll-view-item又包括这个 active类名的话,我们就对其设置样式
<style lang="scss">
  // 滑动块布局
  .scroll-view-container {
    display: flex;

    // 左侧滑总区域
    .left-scroll-view {
      width: 120px;

      // 左侧滑动区域项目 
      .left-scroll-view-item {
        background-color: #f7f7f7;
        line-height: 60px;
        text-align: center;
        font-size: 12px;

        // 左侧选中区域  $.xxx 就是说如果说 即包括.left-scroll-view-item又包括这个 active类名的话,我们就对其设置样式
        &.active {
          background-color: #FFFFFF;
          position: relative;

          &::before {
            content: '';
            display: block;
            width: 3px;
            height: 30px;
            background-color: #C00000;
            position: absolute;
            top: 50%;
            left: 0;
            transform: translateY(-50%);
          }
        }
      }
    }
  }
</style>
<template>
  <view>
    <!-- 滑动区域 -->
    <view class="scroll-view-container">
      <!-- 左侧的滑动区域 -->
      <scroll-view scroll-y="true" class="left-scroll-view" :style="{height:wh+'px'}">
        <view class="left-scroll-view-item active">xxxx</view>
        <view class="left-scroll-view-item">xxxx</view>
      </scroll-view>
      <!-- 右侧的滑动区域 -->
      <scroll-view scroll-y="true" :style="{height:wh+'px'}">
        <view class="right-scroll-view-item">xxxx</view>
      </scroll-view>
    </view>
  </view>
</template>

在这里插入图片描述

(3).获取分类数据

  1. 在 data 中定义分类数据节点:

cate.vue

data() {
  return {
    // 分类数据列表
    cateList: []
  }
}
  1. 调用获取分类列表数据的方法:
onLoad() {
  // 调用获取分类列表数据的方法
  this.getCateList()
}
  1. 定义获取分类列表数据的方法:
    methods: {
      async getCateList() {
        const {
          data: res
        } = await uni.$http.get('/api/public/v1/categories')
        if (res.meta.status === 200) {
          console.log('cate页面分类数据->', res.message)
          this.cateList = res.message
        } else {
          return uni.$showMsg()
        }
      }
    }

(4).动态渲染左侧的一级分类列表

  1. 循环渲染列表结构并绑定选中项:

cate.vue

<view :class="['left-scroll-view-item',index===active ? 'active' : '']" @click="selectedHand(index)">
      <!-- 左侧的滑动区域 -->
      <scroll-view scroll-y="true" class="left-scroll-view" :style="{height:wh+'px'}">
        <block v-for="(item,index) in cateList" :key="index">
          <view :class="['left-scroll-view-item',index===active ? 'active' : '']" @click="selectedHand(index)">
            {{item.cat_name}}
          </view>
        </block>
      </scroll-view>
      selectedHand(index) {
        this.active = index
      }

在这里插入图片描述

(5).动态渲染右侧的二级分类列表

cate.vue

    methods: {
      // 获取分类的数据
      async getCateList() {
        const {
          data: res
        } = await uni.$http.get('/api/public/v1/categories')
        if (res.meta.status === 200) {
          console.log('cate页面分类数据->', res.message)
          this.cateList = res.message
          // 赋值二级分类
          this.cateList2 = res.message[0].children
        } else {
          return uni.$showMsg()
        }
      },
      // 选择一级分类处理事件
      selectedHand(index) {
        this.active = index
        // 赋值二级分类
        this.cateList2 = this.cateList[index].children
      }
    }

在这里插入图片描述

(6).动态渲染右侧的三级分类列表

      <!-- 右侧的滑动区域 -->
      <scroll-view scroll-y="true" :style="{height:wh+'px'}">
        <view class="cate-lv2" v-for="(item2,index2) in cateList2" :key="index2">
          <!-- 二级区域的标题 -->
          <view class="cate-lv2-title">
            /{{item2.cat_name}}/
          </view>
          <!-- 动态渲染三级分类的列表 -->
          <view class="cate-lv3-list">
            <!-- 三级item -->
            <view class="cate-lv3-item" v-for="(item3,index3) in item2.children" :key="index3">
              <!-- 三级分类图片 -->
              <image :src="item3.cat_icon"></image>
              <!-- 三级分类名字 -->
              <text>{{item3.cat_name}}</text>
            </view>
          </view>
        </view>
      </scroll-view>
 .cate-lv3-item {
      // 让每一个子项目占据33.33
      width: 33.33%;
      // 钟祥布局
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      // 下滑间隔为10px
      margin-bottom: 10px;

      image {
        width: 60px;
        height: 60px;
      }

      text {
        font-size: 12px;
      }
    }
  }

在这里插入图片描述

(7).修改Bug-(我们点击一级分类不回顶部)

cate.vue

scroll-top 属性
      <!-- 右侧的滑动区域 -->
      <scroll-view scroll-y="true" :style="{height:wh+'px'}" :scroll-top="scrollTop">
        <view class="cate-lv2" v-for="(item2,index2) in cateList2" :key="index2">
          <!-- 二级区域的标题 -->
          <view class="cate-lv2-title">
            /{{item2.cat_name}}/
          </view>
          <!-- 动态渲染三级分类的列表 -->
          <view class="cate-lv3-list">
            <!-- 三级item -->
            <view class="cate-lv3-item" v-for="(item3,index3) in item2.children" :key="index3">
              <!-- 三级分类图片 -->
              <image :src="item3.cat_icon"></image>
              <!-- 三级分类名字 -->
              <text>{{item3.cat_name}}</text>
            </view>
          </view>
        </view>
      </scroll-view>

这里我们需要对scrollTop进行动态转换,因为我们初始化的时候就是0像素,如果一直不变的话,也就是不会触发scrool-top的,所以我们用1像素和0像素进行点击时切换。

 data() {
      return {
        // 当前设备可用的高度
        wh: 0,
        // 分类数据列表
        cateList: [],
        // 动态选中的数据
        active: 0,
        // 二级分类的列表
        cateList2: [],
        // 当我们点击一级分类的时候,滑动条滚动到顶部 (这里的数据是距离顶部 x 像素)
        scrollTop: 0
      };
    },

      // 选择一级分类处理事件
      selectedHand(index) {
        this.active = index
        // 赋值二级分类
        this.cateList2 = this.cateList[index].children
        // 
        this.scrollTop = this.scrollTop === 0 ? 1 : 0
      }

在这里插入图片描述

(8).点击三级分类跳转到商品列表页面

  1. 为三级分类的item绑定点击事件出粗粒函数
            <!-- 三级item -->
            <view class="cate-lv3-item" v-for="(item3,index3) in item2.children" :key="index3" @click="getGoodsList(item)">
              <!-- 三级分类图片 -->
              <image :src="item3.cat_icon"></image>
              <!-- 三级分类名字 -->
              <text>{{item3.cat_name}}</text>
            </view>
  1. 定义事件处理函数
            <!-- 三级item -->
            <view class="cate-lv3-item" v-for="(item3,index3) in item2.children" :key="index3"
              @click="gettoGoodsList(item3)">
              <!-- 三级分类图片 -->
              <image :src="item3.cat_icon"></image>
              <!-- 三级分类名字 -->
              <text>{{item3.cat_name}}</text>
            </view>
      // 三级分类详情页处理函数 跳转到商品处理函数
      gettoGoodsList(item3) {
        uni.navigateTo({
          url: '/subpkg/goods_list/goods_list?cid=' + item3.cat_id
        })
      }

在这里插入图片描述

(9).提交合并页面

1.将cate分支提交到本地仓库

git branch
git add .
git commit -m "完成了开发页面的提交"

在这里插入图片描述
2.将cate分支提交到码云仓库

git push -u origin cate

在这里插入图片描述
3.将本地的cate分支合并到master分支

git checkout master
git add .
git commit -m "完成了home开发"
git merge cate
强制推送到远程仓库
git push -f origin master 

在这里插入图片描述
4.删除分支

git branch -d cate

在这里插入图片描述

4.配置页面 - 搜索

(1).创建search分支

运行如下的命令,基于master分支在本地创建search子分子,用来开发搜索相关的功能。

git checkout -b search

在这里插入图片描述

(2).自定义搜索组件

  1. 自定义my-search组件

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

在这里插入图片描述
2.在分类页面的 UI 结构中,直接以标签的形式使用 my-search 自定义组件:
cate.vue

  <my-search></my-search>

注意事项:

  1. Vue: 在Vue中使用组件,我们需要先创建一个组件component。然后在使用这个组件的地方 import引入的操作,然后进行components{}进行注册,然后我们再< xxx></xxx>
  2. Uniapp: 创建components/xxx,直接自动就默认进行了全局的注册,最后我们使用 < xxxx></xxxx>

在这里插入图片描述
3.定义 my-search 组件的 UI 结构如下:

<template>
  <view>
    <!-- 搜索框 -->
    <view class="my-search-container">
      <view class="my-search-box">
        <!-- uniapp提供的图标 type指的是类型、size指的是大小-->
        <uni-icons type="search" size="17"></uni-icons>
        <text class="pleacholder">搜索</text>
      </view>
    </view>
  </view>
</template>

<style lang="scss">
  // 搜索框整个view
  .my-search-container {
    // 搜索框的高度是 50px
    height: 50px;
    background-color: #C00000;
    // 将搜索框进行居中对其盒子
    display: flex;
    align-items: center;
    padding: 0 20rpx;

    // 搜索框里面
    .my-search-box {
      height: 36px;
      background-color: #FFFFFF;
      // 圆角
      border-radius: 18px;
      width: 100%;
      // 文本横向布局、左右居中、垂直居中、
      display: flex;
      justify-content: center;
      align-items: center;

      .pleacholder {
        font-size: 15px;
        margin-left: 5px;
      }
    }
  }
</style>

在这里插入图片描述
修改可用屏幕高度解决小bug

this.wh = systemInfo.windowHeight - 50

在这里插入图片描述

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

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

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

<template>
  <view>
    <!-- 搜索框 -->
    <view class="my-search-container" :style="{'background-color' : bgcolor}">
      <view class="my-search-box" :style="{'border-radius' : radius + 'px'}">
        <!-- uniapp提供的图标 type指的是类型、size指的是大小-->
        <uni-icons type="search" size="17"></uni-icons>
        <text class="pleacholder">搜索</text>
      </view>
    </view>
  </view>
</template>

<script>
  export default {
    name: "my-search",
    props: {
      // 背景颜色
      bgcolor: {
        type: String,
        default: '#C00000'
      },
      //圆角尺寸
      radius: {
        type: Number,
        default: 18 // 单位是px
      }
    },
    data() {
      return {

      };
    }
  }
</script>


在这里插入图片描述
重写背景颜色:
在这里插入图片描述
在这里插入图片描述

  1. 为自定义组件封装 click 事件 (子传父)

1.在 my-search 自定义组件内部,给类名为 .my-search-box 的 view 绑定 click 事件处理函数:

cate.vue 父组件

    <!-- 添加上自定义搜索的组件 -->
    <my-search :bgcolor="'pink'" @Myclick="gotoSearch()"></my-search>

js
      // 点击搜索页
      gotoSearch() {
        console.log('xxxx')
      }

my-search.vue 子组件

<template>
  <view>
    <!-- 搜索框 -->
    <view class="my-search-container" :style="{'background-color' : bgcolor}" @click="searchBoxHandler()">
      <view class="my-search-box" :style="{'border-radius' : radius + 'px'}">
        <!-- uniapp提供的图标 type指的是类型、size指的是大小-->
        <uni-icons type="search" size="17"></uni-icons>
        <text class="pleacholder">搜索</text>
      </view>
    </view>
  </view>
</template>

    methods: {
      searchBoxHandler() {
        console.log('ssss')
        // 利用绑定事件
        this.$emit('Myclick')
      }
    }

因为父组件不存在没有@AAA事件的,所以我们需要给父组件自定义触发事件,然后在子组件的位置进行@Click事件。在触发的函数中进行 this.$emit(‘AAA’)即可。

在这里插入图片描述

  1. 导航跳转

在subpkg目录下创建分包

在这里插入图片描述
cate.vue

      // 点击跳转到搜索页
      gotoSearch() {
        uni.navigateTo({
          url: '/subpkg/search/search'
        })
      }

在这里插入图片描述

  1. 实现首页的吸顶效果 (就是搜素狂固定位置不动)

home.vue

<!-- 使用自定义的搜索组件 -->
<view class="search-box">
  <my-search @click="gotoSearch"></my-search>
</view>

home.vue: 跳转的路径

gotoSearch() {
  uni.navigateTo({
    url: '/subpkg/search/search'
  })
}

home.vue 样式的修改

.search-box {
  // 设置定位效果为“吸顶”
  position: sticky;
  // 吸顶的“位置”
  top: 0;
  // 提高层级,防止被轮播图覆盖
  z-index: 999;
}

在这里插入图片描述

(3).搜索建议

  1. 渲染搜索页面的基本结构

1.渲染基本UI结构
search.vue

<template>
  <view>
    <view class="search-box">
      <!-- 使用 uni-ui 提供的搜索组件  cancelButton:none 一直不显示取消按钮-->
      <uni-search-bar @input="input" :radius="100" cancelButton="none"></uni-search-bar>
    </view>
  </view>
</template>

2.修改源代码的搜索框的背景色
在这里插入图片描述

  .uni-searchbar {
    /* #ifndef APP-NVUE */
    display: flex;
    /* #endif */
    flex-direction: row;
    position: relative;
    padding: 10px;
    background-color: #C00000;
  }

在这里插入图片描述
3.定义input的事件处理函数

    <view class="search-box">
      <!-- 使用 uni-ui 提供的搜索组件  1. cancelButton:none 一直不显示取消按钮 2.绑定input事件-->
      <uni-search-bar @input="input" :radius="100" cancelButton="none"></uni-search-bar>
    </view>
  
    methods: {
      // input事件的处理函数
      input(e) {
        console.log('搜索页面search文本框->', e)
      }
    }

在这里插入图片描述

  1. 自动获取搜索框的焦点 (当我们进入这个页面的时候自动锁定搜索框)

在搜索框上使用: focuse节点即可

<template>
  <view>
    <view class="search-box">
      <!-- 使用 uni-ui 提供的搜索组件  1. cancelButton:none 一直不显示取消按钮 2.绑定input事件-->
      <uni-search-bar @input="input" :radius="100" cancelButton="none" :focus="true"></uni-search-bar>
    </view>
  </view>
</template>

在这里插入图片描述

  1. 防抖处理

正常情况下:我们每次输入文本框的时候,都会进行请求一次。我们不能这样做(因为会给网络造成拥堵的操作)

在这里插入图片描述

<template>
  <view>
    <view class="search-box">
      <!-- 使用 uni-ui 提供的搜索组件  1. cancelButton:none 一直不显示取消按钮 2.绑定input事件-->
      <uni-search-bar @input="input" :radius="100" cancelButton="none" :focus="true"></uni-search-bar>
    </view>
  </view>
</template>

<script>
  export default {
    data() {
      return {
        //延时器的timerId
        timer: null,
      };
    },
    methods: {
      // input事件的处理函数
      input(e) {
        // 清除对应的延时器-> 目的是假如说在500毫秒内用户仍然在输入那么就重新记时
        clearTimeout(this.timer)
        // 重新启动一个演示器
        setTimeout(() => {
          // 假如说500毫秒时间到了,那么我们就赋值给数据
          console.log('搜索页面search文本框->', e)
        }, 500)
      }
    }
  }
</script>

在这里插入图片描述

  1. 实现搜索建议列表

1.在 data 中定义如下的数据节点,用来存放搜索建议的列表数据:

data() {
  return {
    // 搜索结果列表
    searchResults: []
  }
}

      async getSearchList() {
        if (this.kw === '') {
          this.searchResults = []
        } else {
          const {
            data: res
          } = await uni.$http.get('/api/public/v1/goods/qsearch', {
            query: this.kw
          })
          if (res.meta.status === 200) {
            this.searchResults = res.message
          }
        }
      }
  1. 渲染搜素建议列表
    <!-- 搜索建议列表 -->
    <view class="sugg-list">
      <view class="sugg-item" v-for="(item,index) in searchResults" :key="index">
        <view class="goods-name">
          {{item.goods_name}}
        </view>
        <uni-icons type="arrowright" size="16"></uni-icons>
      </view>
    </view>
 .sugg-list {
    // 内间距
    padding: 0 5px;

    .sugg-item {
      // 左右布局
      display: flex;
      // 纵向居中
      align-items: center;
      // 横向两侧对其
      justify-content: space-between;

      font-size: 12px;
      padding: 13px 0;
      border-bottom: 1px solid #efefef;

      .goods-name {
        // 不允许换行
        white-space: nowrap;
        // 超出的内容隐藏
        overflow: hidden;
        // 超出的文本进行替换
        text-overflow: ellipsis;
      }
    }

在这里插入图片描述

  1. 点击搜索建议的 Item 项,跳转到商品详情页面:
    <!-- 搜索建议列表 -->
    <view class="sugg-list">
      <view class="sugg-item" v-for="(item,index) in searchResults" :key="index" @click="gotoDetail(item.goods_id)">
        <view class="goods-name">
          {{item.goods_name}}
        </view>
        <uni-icons type="arrowright" size="16"></uni-icons>
      </view>
    </view>

     gotoDetail(goods_id) {
        uni.navigateTo({
          url: '/subpkg/goods_detail/goods_detail?goods_id=' + goods_id
        })

在这里插入图片描述

(4).搜索历史

  1. 渲染搜索历史的基本结构

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

uni-tag -> 是一个tag标签
        // 搜索历史
        historyList: ['a', 'b', 'jsxs']

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

在这里插入图片描述

  1. 美化搜索历史的选项
// 搜索历史
  .history-box {
    padding: 0 5px;

    .history-title {
      display: flex;
      align-items: center;
      justify-content: space-between;
      height: 40px;
      font-size: 13px;
      border-bottom: 1px solid #efefef;
    }

    .history-item {
      display: flex;
      flex-wrap: wrap;

      uni-tag {
        margin-top: 5px;
        margin-right: 5px;
      }
    }
  }

在这里插入图片描述

  1. 实现搜索建议和搜索历史的按需展示
  1. 当搜索结果列表的长度不为 0的时候(searchResults.length !== 0),需要展示搜索建议区域,隐藏搜索历史区域

  2. 当搜索结果列表的长度等于 0的时候(searchResults.length === 0),需要隐藏搜索建议区域,展示搜索历史区域

  3. 使用 v-ifv-else 控制这两个区域的显示和隐藏,示例代码如下:

1.假如搜索的数组存在值那么就显示
v-show="searchResults.length!==0"
2.假如搜索的数组不存在值那么就不显示
v-show="searchResults.length===0"

在这里插入图片描述

  1. 将搜索关键词存入 historyList

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

在java代码中的新增是add在js的代码中使用的是push这两个都是在尾部进行追加的操作,unshift是在头部进行插补

1.设置关键字 kw1用于暂时存放这次搜索的关键字

// 存储关键字
kw1: ''

2. 在处理建议页列表的时候传递一个关键字
      // 建议页列表
      async getSearchList() {
        if (this.kw.trim() === '') {
          this.searchResults = []
        } else {
          const {
            data: res
          } = await uni.$http.get('/api/public/v1/goods/qsearch', {
            query: this.kw
          })
          if (res.meta.status === 200) {
            this.searchResults = res.message
            this.saveSearchHistory(this.kw.trim())
          }

        }
      },
3. 存储关键字的信息
      // 保存搜索历史
      saveSearchHistory(it) {
        this.kw1 = it
      },
4.假如说用户点击到了推荐的详情页数据我们就进行保存,否则不保存
      // 跳转到详情页面
      gotoDetail(goods_id) {
        uni.navigateTo({
          url: '/subpkg/goods_detail/goods_detail?goods_id=' + goods_id
        })
        this.historyList.unshift(this.kw1)
      }

上面这样做的好处:

  1. unshift:保证最近搜索的放在第一个。
  2. kw1:让其放在详情页的时候才进行存储的目的是,防止随意存储
  1. 存在的Bug

1 .假如说有两个重复的选项,我们没有去重的操作Set

2 . 调用reverse()进行反转多数组的时候,我们前提是进行原数组遍历这样可以避免破坏原素组 […arr].reverse()

我们只需要在跳转到详情页面的时候,进行数组转换为set,set再转换为数组数组再反向遍历即可

    data() {
      return {
        //延时器的timerId
        timer: null,
        // 搜索关键词
        kw: '',
        // 列表的查询
        searchResults: [],
        // 搜索历史
        historyList: ['a', 'b', 'jsxs'],
        // 存储关键字
        kw1: ''
      };
    },
    computed: {
      historys() {
        // 注意:由于数组是引用类型,所以不要直接基于原数组调用 reverse 方法,以免修改原数组中元素的顺序
        // 而是应该新建一个内存无关的数组,再进行 reverse 反转
        return [...this.historyList].reverse()
      }
    },
      // 跳转到详情页面
      gotoDetail(goods_id) {
        uni.navigateTo({
          url: '/subpkg/goods_detail/goods_detail?goods_id=' + goods_id
        })

        // 1. 将 Array 数组转化为 Set 对象
        const set = new Set(this.historyList)
        // 2. 调用 Set 对象的 delete 方法,移除对应的元素
        set.delete(this.kw)
        // 3. 调用 Set 对象的 add 方法,向 Set 中添加元素
        if (item.goods_name.length >= 10) {
          const v1 = item.goods_name.slice(0, 9)
          const v2 = v1 + '...'
          set.add(v2)
        } else {
          set.add(item.goods_name)
        }
        // 4. 将 Set 对象转化为 Array 数组
        this.historyList = Array.from(set)
      }

在这里插入图片描述

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

1.修改我们的推荐详情页里的数据

      // 跳转到详情页面
      gotoDetail(goods_id) {
        uni.navigateTo({
          url: '/subpkg/goods_detail/goods_detail?goods_id=' + goods_id
        })

        // 1. 将 Array 数组转化为 Set 对象
        const set = new Set(this.historyList)
        // 2. 调用 Set 对象的 delete 方法,移除对应的元素
        set.delete(this.kw)
        // 3. 调用 Set 对象的 add 方法,向 Set 中添加元素
        if (item.goods_name.length >= 10) {
          const v1 = item.goods_name.slice(0, 9)
          const v2 = v1 + '...'
          set.add(v2)
        } else {
          set.add(item.goods_name)
        }
        // 4. 将 Set 对象转化为 Array 数组
        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') || '[]')
    },

在这里插入图片描述

7.清空搜索历史记录

1.为清空的图标按钮绑定 click 事件:

<uni-icons type="trash" size="17" @click="cleanHistory()"></uni-icons>

2.删除历史的操作

      // 点击清楚搜索历史
      cleanHistory() {
        // 清空 data 中保存的搜索历史
        this.historyList = []
        // 清空本地存储中记录的搜索历史
        uni.setStorageSync('kw', '[]')
      }

在这里插入图片描述

8.点击标签页跳转到商品列表

在标签上添加方法

<uni-tag :text="item" v-for="(item, i) in historys" :key="i" @click="gotoGoodsList(item)"></uni-tag>

在methods中定义方法

// 点击跳转到商品列表页面
gotoGoodsList(kw) {
  uni.navigateTo({
    url: '/subpkg/goods_list/goods_list?query=' + kw
  })
}

在这里插入图片描述

(5).提交合并页面

1.提交分支

git add .
git commit -m "完成了search的修改"
git push -u origin search 

2.合并提交主分支

git checkout master
git add .
git commit -m "完成了search的修改"
git merge search
git push

3.删除分支

git branch -d search

在这里插入图片描述

5.配置页面 - 商品列表

(1).创建goodslist分支

git checkout -b gooddslist

在这里插入图片描述

在这里插入图片描述

(2).定义请求参数对象

1.为了方便发起请求获取商品列表的数据,我们要根据接口的要求,事先定义一个请求的参数对象: 里面的参数对象(API)

    data() {
      return {
        // 请求参数对象
        queryObj: {
          // 请求的参数
          query: '',
          // 请求的cid
          cid: '',
          // 初始化为第一页的数据
          pagenum: 1,
          // 每一页10篇数据
          pagesize: 10
        }
      }
    },

2.将页面跳转时携带的参数,转到queryObj对象中:

    onLoad(options) {
      console.log(options)
      // 为data区域的值,进行赋值的操作,假如说我们接收到的参数为null,那么我们就赋值这个数据为空字符串
      this.queryObj.query = options.query || ''
      // 为data区域中的数据赋值,假如数据不存在那么就对其进行
      this.queryObj.cid = options.cid || ''
    }

(3).获取商品列表数据

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

data() {
  return {
    // 商品列表的数据
    goodsList: [],
    // 总数量,用来实现分页
    total: 0
  }
}

2.在 onLoad 生命周期函数中,调用 getGoodsList 方法获取商品列表数据:

onLoad(options) {
  // 调用获取商品列表数据的方法
  this.getGoodsList()
}

3.在methods区域

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

在这里插入图片描述

(4).渲染商品列表的数据结构

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

<template>
  <view>
    <view class="goods_list">
      <block v-for="(item,index) in goodsList" :key="index">
        <view class="goods-item">
          <!-- 左侧的盒子  图标-->
          <view class="goods-item-left">
            <image :src="item.goods_small_logo || defaultPic " class="goods-pic" mode=""></image>
          </view>
          <!-- 右侧的盒子 名字-->
          <view class="goods-item-right">
            <!-- 名字 -->
            <view class="goods-name">
              {{item.goods_name}}
            </view>
            <!-- 价格 -->
            <view class="goods-info-box">
              <view class="goods-price">
                ¥{{item.goods_price}}
              </view>
            </view>
          </view>
        </view>
      </block>
    </view>
  </view>
</template>

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>

在这里插入图片描述
4.美化商品列表的 UI 结构:

<template>
  <view>
    <view class="goods_list">
      <block v-for="(item,index) in goodsList" :key="index">
        <view class="goods-item">
          <!-- 左侧的盒子  图标-->
          <view class="goods-item-left">
            <image :src="item.goods_small_logo || defaultPic " class="goods-pic" mode=""></image>
          </view>
          <!-- 右侧的盒子 -->
          <view class="goods-item-right">
            <!-- 名字 -->
            <view class="goods-name">
              {{item.goods_name}}
            </view>
            <!-- 价格 -->
            <view class="goods-info-box">
              <view class="goods-price">
                ¥{{item.goods_price}}
              </view>
            </view>
          </view>
        </view>
      </block>
    </view>
  </view>
</template>
<style lang="scss">
  // 商品列表

  .goods-item {
    // 模块水平对其
    display: flex;
    // 上下间距
    padding: 10px 5px;
    // 底部画线
    border-bottom: 1px solid #f0f0f0;

    // 左侧盒子
    .goods-item-left {
      // 和图片的距离
      margin-left: 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 {
        color: #C00000;
        font-size: 16px;
      }
    }
  }
</style>

(5).把商品 item 项封装为自定义组件 (提升复用性)

1.创建组件
在这里插入图片描述
2.将 goods_list 页面中,关于商品 item 项相关的 UI 结构、样式、data 数据,封装到 my-goods 组件中:

goods_list.vue:父组件绑定数据传送给子组件数据

<template>
  <view>
    <view class="goods_list">
      <block v-for="(item,index) in goodsList" :key="index">
        <!-- 绑定自定义事件 并传递 item数据。 子类接受的名字是 item -->
        <my-goods :item="item"></my-goods>
      </block>
    </view>
  </view>
</template>

my-goods.vue: 子组件接受数据

<template>
  <view>
    <view class="goods-item">
      <!-- 左侧的盒子  图标-->
      <view class="goods-item-left">
        <image :src="item.goods_small_logo || defaultPic " class="goods-pic" mode=""></image>
      </view>
      <!-- 右侧的盒子 -->
      <view class="goods-item-right">
        <!-- 名字 -->
        <view class="goods-name">
          {{item.goods_name}}
        </view>
        <!-- 价格 -->
        <view class="goods-info-box">
          <view class="goods-price">
            ¥{{item.goods_price}}
          </view>
        </view>
      </view>
    </view>
  </view>
</template>

<script>
  export default {
    name: "my-goods",
    // 儿子接受父亲传递过来的数据
    props: {
      item: {
        type: Object,
        default: {}
      }
    },
    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-left: 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 {
        color: #C00000;
        font-size: 16px;
      }
    }
  }
</style>

在这里插入图片描述

(6).使用过滤器处理价格

1.在 my-goods 组件中,和 data 节点平级,声明 filters 过滤器节点如下:

    filters:{
      // 把数字处理为带两个小数点的数字
      tofixed(num){
        // 保留两个橡树
        return Number(num).toFixed(2)
      }
    }

2.在渲染商品价格的时候,通过管道符 | 调用过滤器:

<!-- 商品价格 -->
<view class="goods-price">¥{{item.goods_price | tofixed}}</view>

在这里插入图片描述

(7).下拉加载更多

  1. 下拉加载更多

[...arr1,...arr2] 就是数组之间进行拼接的操作

   const arr = [...[1, 2, 3, 4], ...[1, 2, 3, 4]]
   console.log(arr, 'xxxxxxxxxxxxxxxxx')

在这里插入图片描述
1.打开项目根目录中的 pages.json 配置文件,为 subPackages 分包中的 goods_list 页面配置上拉触底的距离:

 {
      "path": "goods_list/goods_list",
      "style": {
        "navigationBarTitleText": "",
        "enablePullDownRefresh": false,
        "onReachBottomDistance": 150
      }

2.在 goods_list 页面中,和 methods 节点平级,声明 onReachBottom 事件处理函数,用来监听页面的上拉触底行为:

    onReachBottom() {
      // 让页码进行自增的操作
      this.queryObj.pagenum++
      // 进行触发函数方法
      this.getGoodsList()
    },

3.改造 methods 中的 getGoodsList 函数,当列表数据请求成功之后,进行新旧数据的拼接处理:

async getGoodsList() {
        const {
          data: resp
        } = await uni.$http.get('/api/public/v1/goods/search', this.queryObj)
        if (resp.meta.status === 200) {
          // 赋值商品数据-》 旧数据和新数据进行拼接,旧数据初始化为null,然后新数据默认先访问第一页的数据,进行了拼接的操作
          this.goodsList = [...this.goodsList, ...resp.message.goods]
          // 赋值总条数
          this.total = resp.message.total
        } else {
          uni.$showMsg()
        }
        console.log('商品链表的数据->', resp)
      }
    }
  }
  1. 通过节流阀防止发起额外的请求

goods_list.vue

        // ⭐设置mask节流
        uni.showLoading({
          title: '正在努力加载数据中...',
          mask: true  // 节流的操作
        })
        // 手动关闭加载的操作
        uni.hideLoading()

在这里插入图片描述

  1. 判断数据是否加载完毕 (这个拦截放在上拉函数中)
pageSize*page>=total

goods_list.vue

    onReachBottom() {
          // 放在页面自增的前面
      if (this.queryObj.pagesize * this.queryObj.pagenum >= this.total) {
        return uni.$showMsg('亲,到底了')
      }
      // 让页码进行自增的操作
      this.queryObj.pagenum++

      // 进行触发函数方法
      this.getGoodsList()
    },

在这里插入图片描述

  1. 开启下拉刷新 (需要关闭下拉)

page.json

 {
      "path": "goods_list/goods_list",
      "style": {
        "navigationBarTitleText": "",
        "enablePullDownRefresh": true,
        "onReachBottomDistance": 150,
        "backgroundColor": "#f8f8f8"
      }

goods_list.vue

    // 用户下拉的事件
    onPullDownRefresh() {
      this.queryObj.total = 0
      this.queryObj.pagenum = 1
      this.queryObj.goodsList = []
      // 重新调用请求的方法,并携带上关闭的回调函数
      this.getGoodsList(() => uni.stopPullDownRefresh())
    },
 methods: {
      // 通过点击的参数获取数据
      async getGoodsList(cb) {
        // ⭐设置mask节流
        uni.showLoading({
          title: '正在努力加载数据中...',
          mask: true
        })
        const {
          data: resp
        } = await uni.$http.get('/api/public/v1/goods/search', this.queryObj)
        
        
        // 只要数据请求完毕,就立即按需调用 cb 回调函数
        cb && cb()


        if (resp.meta.status === 200) {
          // 赋值商品数据-》 旧数据和新数据进行拼接,旧数据初始化为null,然后新数据默认先访问第一页的数据,进行了拼接的操作
          this.goodsList = [...this.goodsList, ...resp.message.goods]
          // 赋值总条数
          this.total = resp.message.total
        } else {
          uni.$showMsg()
        }
        console.log('商品链表的数据->', resp)
        // 手动关闭加载的操作
        uni.hideLoading()
      }
    }
  }

在这里插入图片描述

(8).商品点击item项目进入商品详情页

1.将循环时的 block 组件修改为 view 组件,并绑定 click 点击事件处理函数:

goods_list.vue

    <view class="goods_list">
      <view v-for="(item,index) in goodsList" :key="index">
        <!-- 绑定自定义事件 并传递 item数据。 子类接受的名字是 item -->
        <my-goods :item="item"></my-goods>
      </view>
    </view>
      // 进入详情页
      gotoDetail(item) {
        uni.navigateTo({
          url: '/subpkg/goods_detail/goods_detail?goods_id=' + item.goods_id
        })
      }

在这里插入图片描述

(9).分支的合并与提交

git add .
git commit -m "完成了商品列表"
git push -u origin gooddslist
git checkout master
git add .
git commit -m "完成了商品列表"
git merge gooddslist
git push

git branch -d goodslist

在这里插入图片描述
在这里插入图片描述

6.配置页面 - 商品详情页

(1).创建goodsdetail分支

git checkout -b goodsdetail

在这里插入图片描述
设置编译模式:
在这里插入图片描述

(2).获取商品详情数据

1.在 data 中定义商品详情的数据节点:

<script>
  export default {
    data() {
      return {
        goods_info: {}
      };
    },
    // 获取参数⭐⭐
    onLoad(options) {
      console.log(options)
      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: goods_id
        })
        if (res.meta.status === 200) {
          this.goods_info = res.message
        } else {
          return uni.$showMsg()
        }
      }
    }
  }
</script>

在这里插入图片描述

(3).渲染商品详情页的UI结构

  1. 渲染轮播图的UI结构
<style lang="scss">
  swiper {
    height: 750rpx;

    image {
      width: 100%;
      height: 100%;
    }
  }
</style>
<template>
  <view>
    <swiper :indicator-dots="true" :autoplay="true" :interval="3000" :duration="1000" :circular="true">
      <swiper-item v-for="(item,index) in goods_info.pics" :key="index">
        <image :src="item.pics_big" mode=""></image>
      </swiper-item>
    </swiper>
  </view>
</template>

在这里插入图片描述

  1. 实现轮播图预览效果

map进行遍历数组会获取到遍历数组的item项,我们通过回调函数获取item项的某一个属性,并返回一个新的数组


<image :src="item.pics_big" @click="preview(index)"></image>

      // 大图预览的操作
      preview(index) {
        const newArr = this.goods_info.pics.map(item => item.pics_big)
        uni.previewImage({
          current: index,  // 指定图片的索引
          urls: newArr  // 存放新的URL数组
        })
      }

在这里插入图片描述

  1. 渲染商品信息区域
<!-- 商品信息区域 -->

    <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"></uni-icons>
          <text>收藏</text>
        </view>
      </view>
      <!-- 运费 -->
      <view class="yf">
        快递 : 免运费
      </view>
    </view>

在这里插入图片描述

  1. 美化商品信息区域的样式:
/ 商品描述信息的样式
  .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;
        margin-right: 10px;
      }

      .favi {
        width: 120px;
        font-size: 12px;
        // 横向布局
        display: flex;
        // 垂直布局
        flex-direction: column;
        // 垂直居中
        align-items: center;
        // 横向居中
        justify-content: center;

        border-left: 1px solid #efefef;
        color: gray;
      }
    }

    .yf {
      font-size: 12px;
      color: gray;
      margin: 10px 0;
    }
  }

在这里插入图片描述

  1. 渲染商品详情信息

1.在页面结构中,使用 rich-text 组件,将带有 HTML 标签的内容,渲染为小程序的页面结构:
因为后端传递过来的是: html所以用富文本解析

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

在这里插入图片描述
2.修改 getGoodsDetail 方法,从而解决图片底部 空白间隙 的问题:

利用正则表达式进行替换的操作

     // 是哟个字符串的replace()方法,为img标签添加行内的style样式,从而解决图片独步空白间隙的问题
    // /<img /是指别替换的文本 g 表示全局替换 后面是被替换的文本
   res.message.goods_introduce = res.message.goods_introduce.replace(/<img/g, '<img style="display:block"')

3.解决 .webp 格式图片在 ios 设备上无法正常显示的问题:
在这里插入图片描述
正则替换图片后缀: 为什么在ios设备上图片不显示,因为webp这个文件后缀对ios设备支持的力度比较少,所以我们要替换成jpg``

.replace(/webp/g, 'jpg')
 // 使用字符串的 replace() 方法,将 webp 的后缀名替换为 jpg 的后缀名
  res.message.goods_introduce = res.message.goods_introduce.replace(/<img /g, '<img style="display:block;" ').replace(/webp/g, 'jpg')

在这里插入图片描述

  1. 解决商品价格闪烁的问题

当我们点击到商品详情页的时候,我们的价格会从undefined变为实际的标价

  1. 导致问题的原因:在商品详情数据请求回来之前,data 中 goods_info 的值为 {},因此初次渲染页面时,会导致 商品价格、商品名称 等闪烁的问题。

  2. 解决方案:判断 goods_info.goods_name 属性的值是否存在,从而使用 v-if 指令控制页面的显示与隐藏:

<template>
  <view v-if="goods_info.goods_name">
   <!-- 省略其它代码 -->
  </view>
</template>

(4).渲染详情页底部的商品导航区域

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

1.下载如下插件 : https://ext.dcloud.net.cn/plugin?id=865
导入插件之后需要重启小程序,否则不显示
在这里插入图片描述
2. 在 data 中,通过 optionsbuttonGroup 两个数组,来声明商品导航组件的按钮配置对象:

<!-- 商品导航组件 -->
<view class="goods_nav">
  <!-- fill 控制右侧按钮的样式 -->
  <!-- options 左侧按钮的配置项 -->
  <!-- buttonGroup 右侧按钮的配置项 -->
  <!-- click 左侧按钮的点击事件处理函数 -->
  <!-- buttonClick 右侧按钮的点击事件处理函数 -->
  <uni-goods-nav :fill="true" :options="options" :buttonGroup="buttonGroup" @click="onClick" @buttonClick="buttonClick" />
</view>


 options: [{
			icon: 'headphones',
			text: '客服'
		}, {
			icon: 'shop',
			text: '店铺',
			info: 2,
			infoBackgroundColor:'#007aff',
			infoColor:"red"
		}, {
			icon: 'cart',
			text: '购物车',
			info: 2
		}],
	    buttonGroup: [{
	      text: '加入购物车',
	      backgroundColor: '#ff0000',
	      color: '#fff'
	    },
	    {
	      text: '立即购买',
	      backgroundColor: '#ffa200',
	      color: '#fff'
	    }
	    ]

在这里插入图片描述
3. 美化商品导航组件,使之固定在页面最底部:

  // 商品导航栏目
  .goods_nav {
    position: sticky;
    bottom: 0;
    z-index: 999;
    width: 100%;
  }
//因为这几个按钮的组件的高度为50,所以我们放50避免遮挡试图
  .goods-detail-container {
    padding-bottom: 50px;
  }

在这里插入图片描述

  1. 点击跳转到购物车
  1. 点击商品导航组件左侧的按钮,会触发 uni-goods-nav@click 事件处理函数,事件对象 e中会包含当前点击的按钮相关的信息:
    在这里插入图片描述
      // 商品导航栏的信息
      onClick(e) {
        console.log(e)
      }

在这里插入图片描述

      // 商品导航栏的信息
      onClick(e) {
        console.log(e)
        if (e.content.text === "购物车") {
          uni.switchTab({
            url: '/pages/cart/cart'
          })
        }
      }

在这里插入图片描述

(5).分支的合并与提交

git add .
git commit -m "完成了商品详情页"
git push -u origin goodsdetail
git checkout master
git add .
git commit -m "完成了商品详情页"
git merge goodsdetail
git push
git branch -d goodsdetail

在这里插入图片描述

7.配置页面 - 加入购物车

(1).创建 cart 分支

git checkout -b cart

(2). 配置 vuex (uniapp封装了Vue不用安装vuex)

  1. 在项目根目录中创建 store 文件夹,专门用来存放 vuex 相关的模块

  2. 在 store 目录上鼠标右键,选择 新建 -> js文件,新建 store.js 文件:

在这里插入图片描述
store.js

// 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. 在 main.js 中导入 store 实例对象并挂载到 Vue 的实例上:
// 1. 导入 store 的实例对象
import store from './store/store.js'

// 省略其它代码...

const app = new Vue({
  ...App,
  // 2. 将 store 挂载到 Vue 实例上
  store,
})
app.$mount()

(3). 创建购物车的 store 模块

  1. store 目录上鼠标右键,选择 新建 -> js文件,创建购物车的 store 模块,命名为 cart.js

在这里插入图片描述
2. 在 cart.js 中,初始化如下的 vuex 模块:

export default {
  // 1.开启命名空间
  namespaced: true,
  // 返回数据节点

  state: () => ({
    // 购物车的数组,用来存储购物车中每个商品的信息对象
    // 每个商品的信息对象,都包含如下 6 个属性:
    // { goods_id, goods_name, goods_price, goods_count, goods_small_logo, goods_state }
    cart: []
  }),

  mutations: {},
  getters: {}
}
  1. 在 store/store.js 模块中,导入并挂载购物车的 vuex 模块,示例代码如下:
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

(4).在商品详情页中使用 Store 中的数据

  1. goods_detail.vue 页面中,修改 <script></script> 标签中的代码如下:
// 从 vuex 中按需导出 mapState 辅助方法
import { mapState } from 'vuex'

export default {
  computed: {
    // 调用 mapState 方法,把 m_cart 模块中的 cart 数组映射到当前页面中,作为计算属性来使用
    // ...mapState('模块的名称', ['要映射的数据名称1', '要映射的数据名称2'])
    ...mapState('m_cart', ['cart']),
  },
  // 省略其它代码...
}

注意:今后无论映射 mutations 方法,还是 getters 属性,还是 state 中的数据,都需要指定模块的名称,才能进行映射。

  1. 在页面渲染时,可以直接使用映射过来的数据,例如
<!-- 运费 -->
<view class="yf">快递:免运费 -- {{cart.length}}</view>

在这里插入图片描述

(5).实现加入购物车的功能

  1. store 目录下的 cart.js 模块中,封装一个将商品信息加入购物车的 mutations 方法,命名为 addToCart。示例代码如下:

Vuex共享的数据只有在mutations这个对象中才能进行修改

  mutations: {
    addToCart(state, item) { // 第一个参数:不占位置、第二个参数: 对象
      // findReusult : 要么得到undefind->没有找到信息(false) 要么得到商品的对象(true)
      const findReusult = state.cart.find(x => x.goods_id === item.goods_id) //这里会遍历catr数组,并得到对象x。然后进行判断购物车里存放的和添加的是否一致
      if (!findReusult) { // 加入说undefind
        state.cart.push(item) // 推入cart数组
      } else { //加入说已经存在 那么就自增1
        findReusult.goods_count++
      }
    }
  },
export default {
  // 1.开启命名空间
  namespaced: true,
  // 返回数据节点

  state: () => ({
    // 购物车的数组,用来存储购物车中每个商品的信息对象
    // 每个商品的信息对象,都包含如下 6 个属性:
    // { goods_id, goods_name, goods_price, goods_count, goods_small_logo, goods_state }
    cart: []
  }),

  mutations: {
    addToCart(state, item) { // 第一个参数:返回值的名(调用cart的不占位置)、第二个参数: 对象
      // findReusult : 要么得到undefind->没有找到信息(false) 要么得到商品的对象(true)
      const findReusult = state.cart.find(x => x.goods_id === item.goods_id) //这里会遍历catr数组,并得到对象x。然后进行判断购物车里存放的和添加的是否一致
      if (!findReusult) { // 加入说undefind
        state.cart.push(item) // 推入cart数组
      } else { //加入说已经存在 那么就自增1
        findReusult.goods_count++
      }

    }
  },
  getters: {}
}
  1. 在商品详情页面中,通过 mapMutations 这个辅助方法,把 vuex m_cart 模块下的 addToCart 方法映射到当前页面:

在这里插入图片描述

// 按需导入 mapMutations 这个辅助方法
import { mapMutations } from 'vuex'

export default {
  methods: {
    // 把 m_cart 模块中的 addToCart 方法映射到当前页面使用
    ...mapMutations('m_cart', ['addToCart']),
  },
}
  1. 为商品导航组件 uni-goods-nav 绑定 @buttonClick="buttonClick" 事件处理函数:

goods_deatil

 // 右侧的信息
      buttonClick(e) {
        console.log(e)
        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 // 商品的勾选状态
          }
          // 调用共享的方法 ⭐
          this.addToCart(goods)
          console.log(this.goods_info)

        }

在这里插入图片描述

(6). 动态统计购物车中商品的总数量

cart.js
编写计算属性: 一个方法出现return就会变成计算属性

监听的是: 假如说点击加入购物车的话,那么cart就会变化。cart只要变化就会引出c变化,c一变化就会触发监听事件

  getters: {
    total(state) {
      let c = 0
      state.cart.forEach(item => c += item.goods_count)
      return c
    }
  }

cart.vue

  // 引入Vuex
  import {
    // 映射属性
    mapState,
    // 映射方法增删改
    mapMutations,
    // 映射方法 读
    mapGetters
  } from "vuex"

 watch: {
 		// 调用计算属性 total,假如
      total(newValue) {
        // 这个方法是: 在一个未知的数组对象中找哪一个对象的text属性为"购物车"的这个对象。
        const findResult = this.options.find(item => item.text === '购物车')
        if (findResult) { // 加入说找到了就赋值新的值
          findResult.info = newValue
        }
      }
    },

(7).持久化存储购物车的功能

    // 持久化存储本地数据
    saveToStorage(state) {
      uni.setStorageSync('cart', JSON.stringify(state.cart))
    }
  1. 修改 mutations 节点中的 addToCart 方法,在处理完商品信息后,调用步骤 1 中定义的 saveToStorage 方法:
// 通过 commit 方法,调用 m_cart 命名空间下的 saveToStorage 方法
   this.commit('m_cart/saveToStorage')
  1. 修改 cart.js 模块中的 state 函数,读取本地存储的购物车数据,对 cart 数组进行初始化:
// 模块的 state 数据
state: () => ({
   // 购物车的数组,用来存储购物车中每个商品的信息对象
   // 每个商品的信息对象,都包含如下 6 个属性:
   // { goods_id, goods_name, goods_price, goods_count, goods_small_logo, goods_state }
   cart: JSON.parse(uni.getStorageSync('cart') || '[]')
}),

(8).优化商品详情页的监听器

  1. 使用普通函数的形式定义的 watch 侦听器,在页面首次加载后不会被调用。因此导致了商品详情页在首次加载完毕之后,不会将商品的总数量显示到商品导航区域

handler是源码的方法不能变,

    watch: {
      total: {
        handler(newValue) {
          // 这个方法是: 在一个未知的数组对象中找哪一个对象的text属性为"购物车"的这个对象。
          const findResult = this.options.find(item => item.text === '购物车')
          if (findResult) { // 加入说找到了就赋值新的值
            findResult.info = newValue
          }
        },
        immediate: true  // 立即执行
      }
    },

在这里插入图片描述

(9).动态为 tabBar 页面设置数字徽标

需求描述:从商品详情页面导航到购物车页面之后,需要为 tabBar 中的购物车动态设置数字徽标。

cate.vue

<template>
  <view>
    <h1>购物车</h1>
  </view>
</template>

<script>
  // 引入 ⭐
  import {
    mapGetters
  } from 'vuex'
  export default {
    onShow() {
      this.setBadge()
    },
    // 声明⭐⭐
    computed: {
      ...mapGetters('m_cart', ['total'])
    },
    data() {
      return {

      };
    },
    methods: {
      setBadge() {
        uni.setTabBarBadge({⭐⭐
          // 0索引是 : 首页 1索引是分类 2索引是购物车 3索引是我的
          index: 2,
          text: this.total + ''  // 这里必须是字符串
        })
      }
    }
  }
</script>

<style lang="scss">

</style>

(10).将设置 tabBar 徽标的代码抽离为 mixins

注意:除了要在 cart.vue 页面中设置购物车的数字徽标,还需要在其它 3 个 tabBar 页面中,为购物车设置数字徽标。

此时可以使用 Vue 提供的 mixins 特性,提高代码的可维护性。

实际上就是说: 代码的封装
在这里插入图片描述
tabbar-badge.js

// 引入 ⭐
import {
  mapGetters
} from "vuex"
// 抛出 ⭐⭐
export default {
  onShow() {
    this.setBadge()
  },
  // 声明
  computed: {
    ...mapGetters('m_cart', ['total'])
  },
  methods: {
    setBadge() {
      uni.setTabBarBadge({
        // 0索引是 : 首页 1索引是分类 2索引是购物车 3索引是我的
        index: 2,
        text: this.total + ''
      })
    }
  }
}

my.vue

<template>
  <view>
    <h1>个人中心 </h1>
  </view>
</template>

<script>
  import badgeMix from '@/mixins/tabbar-badge.js'  // 引入⭐
  export default {
    mixins: [badgeMix],  //使用⭐⭐
    data() {
      return {

      };
    }
  }
</script>

<style lang="scss">

</style>

在这里插入图片描述

8.Vuex 数据共享步骤 ⭐

将cart.js的方法共享的五个基本步骤
1.创建vuex 共享出vuex  ------->(store.js)
2.引入(store.js)并挂载出去 ----->(main.js)
3.创建要共享内容的js ------->(cart.js)
4.在store.js引入刚才创建的共享js并挂载出去  ------>(store.js)
5.使用共享的数据

(1).创建store.js并共享

store.js

// 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: {},
})

(2).挂载到主入口文件上

main.js

// #ifndef VUE3
import Vue from 'vue'
import App from './App'
// 创建store
import store from "@/store/store.js" ⭐ 引入 @/代表根目录
Vue.config.productionTip = false

App.mpType = 'app'

const app = new Vue({
  ...App,
  store    ⭐⭐ 挂载

})
app.$mount()
// #endif

// #ifdef VUE3
import {
  createSSRApp
} from 'vue'
import App from './App.vue'
export function createApp() {
  const app = createSSRApp(App)
  return {
    app
  }
}

(3).创建共享的数据.js

cart.js
只有共享数据.js才能修改要共享的数据

export default {
  // 1.开启命名空间 ⭐
  namespaced: true,
  // 返回数据⭐⭐

  state: () => ({
    // 购物车的数组,用来存储购物车中每个商品的信息对象
    // 每个商品的信息对象,都包含如下 6 个属性:
    // { goods_id, goods_name, goods_price, goods_count, goods_small_logo, goods_state }
    cart: []
  }),
	// 返回增删改的操作 ⭐⭐⭐
  mutations: {
    addToCart(state, item) { // 第一个参数:返回值的名(调用cart的)、第二个参数: 对象
      // findReusult : 要么得到undefind->没有找到信息(false) 要么得到商品的对象(true)
      const findReusult = state.cart.find(x => x.goods_id === item.goods_id) //这里会遍历catr数组,并得到对象x。然后进行判断购物车里存放的和添加的是否一致
      if (!findReusult) { // 加入说undefind
        state.cart.push(item) // 推入cart数组
      } else { //加入说已经存在 那么就自增1
        findReusult.goods_count++
      }

    }
  },
  // 用于读的操作
    getters: {
    total(state) {
      let c = 0
      state.cart.forEach(item => c += item.goods_count)
      return c
    }
  }
}

(4).引入要共享的数据.js

store.js

// 1. 导入 Vue 和 Vuex
import Vue from 'vue'
import Vuex from 'vuex'
import moduleCart from '@/store/cart.js'// 2. 将 Vuex 安装为 Vue 的插件
Vue.use(Vuex)

// 3. 创建 Store 的实例对象
const store = new Vuex.Store({
  // TODO:挂载 store 模块
  modules: {
    'm_cart': moduleCart ⭐⭐
  },
})

// 4. 向外共享 Store 的实例对象
export default store

(5).引入/使用共享的数据

1.共享属性

// 从 vuex 中按需导出 mapState 辅助方法
import { mapState } from 'vuex'export default {
  computed: {
    // 调用 mapState 方法,把 m_cart 模块中的 cart 数组映射到当前页面中,作为计算属性来使用
    // ...mapState('模块的名称', ['要映射的数据名称1', '要映射的数据名称2'])
    ...mapState('m_cart', ['cart']),},
  // 省略其它代码...
}

2.共享方法

  // 引入Vuex
  import {
    // 映射属性
    mapState,
    // 映射方法
    mapMutations
  } from "vuex"

 methods: {
      // store.js声明的模块名 共享的方法名
      ...mapMutations('m_cart', ['addToCart']),
      }

3.使用

属性的话当作 data区域中的数据一样
方法的话当作 methods方法区中的方法一样

eg:调用共享方法  this.addToCart()

9.配置页面 - 商品列表区域

(1).渲染购物车商品列表的标题区域

cart.vue

<template>
  <view>
    <!-- 商品列表的标题区域 -->
    <view class="cart-title">
      <!-- 左侧的图标 -->
      <uni-icons type="shop" size="18"></uni-icons>
      <!-- 描述文本 -->
      <text class="cart-title-text">购物车</text>
    </view>
  </view>
</template>
<style lang="scss">
  .cart-title {
    height: 40px;
    display: flex;
    align-items: center;
    padding-left: 5px;
    border-bottom: 1px solid #efefef;

    .cart-title-text {
      font-size: 14px;
      margin-left: 10px;
    }
  }
</style>

在这里插入图片描述

(2).渲染商品列表区域的基本结构

1.通过 mapState 辅助函数,将 Store 中的 cart 数组映射到当前页面中使用:

<script>
  import badgeMix from '@/mixins/tabbar-badge.js'
  // 引入辅助函数
  import {
    mapState
  } from "vuex"
  export default {
    mixins: [badgeMix],
    // 使用辅助函数
    computed: {
      ...mapState('m_cart', ['cart']),
    },
    data() {
      return {

      };
    },
  }
</script>

2.在 UI 结构中,通过 v-for 指令循环渲染自定义的 my-goods 组件:

这里调用了子组件并传递参数

  <!-- 循环渲染购物车中的数据 -->
    <block v-for="(item,index) in cart" :key="index">
      <my-goods :item="item"></my-goods>
    </block>

在这里插入图片描述

(3).my-goods 组件封装 radio 勾选状态

1.打开 my-goods.vue 组件的源代码,为商品的左侧图片区域添加 radio 组件:

<!-- 商品左侧图片区域 -->
<view class="goods-item-left">
  <radio checked color="#C00000"></radio>
  <image :src="goods.goods_small_logo || defaultPic" class="goods-pic"></image>
</view>

2.给类名为 goods-item-left 的 view 组件添加样式,实现 radio 组件和 image 组件的左右布局:

.goods-item-left {
  margin-right: 5px;
  display: flex;
  justify-content: space-between;
  align-items: center;

  .goods-pic {
    width: 100px;
    height: 100px;
    display: block;
  }
}

3.封装名称为 showRadio 的 props 属性,来控制当前组件中是否显示 radio 组件:

    // 儿子接受父亲传递过来的数据
    props: {
      item: {
        type: Object,
        default: {}
      },
      showRadio: {
        type: Boolean,
        default: false
      }
    },

4.使用v-show控制是否显示

<radio checked color="#C00000" v-show="showRadio"></radio>

5.需要展示的组件修改元素为true

<!-- 商品列表区域 -->
<block v-for="(goods, i) in cart" :key="i">
  <my-goods :goods="goods" :show-radio="true"></my-goods>
</block>

6.修改 my-goods.vue 组件,动态为 radio 绑定选中状态:

<!-- 商品左侧图片区域 -->
<view class="goods-item-left">
  <!-- 存储在购物车中的商品,包含 goods_state 属性,表示商品的勾选状态 -->
  <radio :checked="goods.goods_state" color="#C00000" v-if="showRadio"></radio>
  <image :src="goods.goods_small_logo || defaultPic" class="goods-pic"></image>
</view>

在这里插入图片描述

(4). 为 my-goods 组件封装 radio-change 事件

  1. 当用户点击 radio 组件,希望修改当前商品的勾选状态,此时用户可以为 my-goods 组件绑定 @radio-change 事件,从而获取当前商品的 goods_idgoods_state
<!-- 商品列表区域 -->
<block v-for="(goods, i) in cart" :key="i">
  <!-- 在 radioChangeHandler 事件处理函数中,通过事件对象 e,得到商品的 goods_id 和 goods_state -->
  <my-goods :goods="goods" :show-radio="true" @radio-change="radioChangeHandler"></my-goods>
</block>
  1. 定义 radioChangeHandler 事件处理函数如下:
methods: {
  // 商品的勾选状态发生了变化
  radioChangeHandler(e) {
    console.log(e) // 输出得到的数据 -> {goods_id: 395, goods_state: false}
  }
}
  1. my-goods.vue 组件中,为 radio 组件绑定 @click 事件处理函数如下:
<!-- 商品左侧图片区域 -->
<view class="goods-item-left">
  <radio :checked="goods.goods_state" color="#C00000" v-if="showRadio" @click="radioClickHandler"></radio>
  <image :src="goods.goods_small_logo || defaultPic" class="goods-pic"></image>
</view>
  1. 在 my-goods.vue 组件的 methods 节点中,定义 radioClickHandler 事件处理函数:
    methods: {
      radioChangeHandler() {
      // 这里传递的是一个对象
        this.$emit('radio-change', {
          goods_id: this.item.goods_id,
          goods_state: this.item.goods_state
        })
      }
    }

在这里插入图片描述

(5).修改购物车中商品的勾选状态

  1. store/cart.js 模块中,声明如下的 mutations 方法,用来修改对应商品的勾选状态:
// 更新购物车中商品的勾选状态
updateGoodsState(state, goods) {
  // 根据 goods_id 查询购物车中对应商品的信息对象
  const findResult = state.cart.find(x => x.goods_id === goods.goods_id)

  // 有对应的商品信息对象
  if (findResult) {
    // 更新对应商品的勾选状态
    findResult.goods_state = goods.goods_state
    // 持久化存储到本地
    this.commit('m_cart/saveToStorage')
  }
  1. 在 cart.vue 页面中,导入 mapMutations 这个辅助函数,从而将需要的 mutations 方法映射到当前页面中使用:
import badgeMix from '@/mixins/tabbar-badge.js'
import { mapState, mapMutations } from 'vuex'

export default {
  mixins: [badgeMix],
  computed: {
    ...mapState('m_cart', ['cart']),
  },
  data() {
    return {}
  },
  methods: {
    ...mapMutations('m_cart', ['updateGoodsState']),
    // 商品的勾选状态发生了变化
    radioChangeHandler(e) {
      this.updateGoodsState(e)
    },
  },
}

在这里插入图片描述

(6). 为 my-goods 组件封装 NumberBox

在这里插入图片描述

  1. 修改 my-goods.vue 组件的源代码,在类名为 goods-info-box 的 view 组件内部渲染 NumberBox 组件的基本结构:
<uni-number-box :min="1"></uni-number-box>

在这里插入图片描述
2. 美化样式

.goods-item-right {
  display: flex;
  flex: 1;
  flex-direction: column;
  justify-content: space-between;

  .goods-name {
    font-size: 13px;
  }

  .goods-info-box {
    display: flex;
    align-items: center;
    justify-content: space-between;
  }

  .goods-price {
    font-size: 16px;
    color: #c00000;
  }
}
  1. 在 my-goods.vue 组件中,动态为 NumberBox 组件绑定商品的数量值:
<view class="goods-info-box">
  <!-- 商品价格 -->
  <view class="goods-price">¥{{goods.goods_price | tofixed}}</view>
  <!-- 商品数量 -->
  <uni-number-box :min="1" :value="goods.goods_count"></uni-number-box>
</view>
  1. 控制NumberBox的显示与隐藏
    直接复用radios的
 <!-- 计算框 -->
  <uni-number-box :min="1" :value="item.goods_count" v-show="showRadio"></uni-number-box>

      showRadio: {
        type: Boolean,
        default: false
      }

(7).为 my-goods 组件封装 num-change 事件

  1. 先在父组件声明自定义事件 (cart.vue)
      <!-- 绑定自定义事件 -->
      <my-goods :item="item" :showRadio="true" @radioChange="radioChangeHandler"
        @numChange="numberChangeHandler"></my-goods>
numberChangeHandler(e) {
        console.log('yyyy->', e)

      }
  1. 在子组件声明触发事件 (my-goods.vue)
            <!-- 计算框 -->
            <uni-number-box :min="1" :value="item.goods_count" v-show="showRadio"
              @change="numChangeHandler"></uni-number-box>
      // 计数器 -> 触发就会传过来改变后的值
      numChangeHandler(val) {
        // 传递数量和id
        this.$emit('numChange', {
          goods_id: this.item.goods_id,
          // 要转换成数字
          goods_count: val - 0
        })
      }
  1. 永久化存储:在 store/cart.js 模块中,声明如下的 mutations 方法,用来修改对应商品的数量:
// 更新购物车中商品的数量
updateGoodsCount(state, goods) {
  // 根据 goods_id 查询购物车中对应商品的信息对象
  const findResult = state.cart.find(x => x.goods_id === goods.goods_id)

  if(findResult) {
    // 更新对应商品的数量
    findResult.goods_count = goods.goods_count
    // 持久化存储到本地
    this.commit('m_cart/saveToStorage')
  }
}

cart.vue 页面中,通过 mapMutations 这个辅助函数,将需要的 mutations 方法映射到当前页面中使用:

import badgeMix from '@/mixins/tabbar-badge.js'
import { mapState, mapMutations } from 'vuex'

export default {
  mixins: [badgeMix],
  computed: {
    ...mapState('m_cart', ['cart']),
  },
  data() {
    return {}
  },
  methods: {
    ...mapMutations('m_cart', ['updateGoodsState', 'updateGoodsCount']),
    // 商品的勾选状态发生了变化
    radioChangeHandler(e) {
      this.updateGoodsState(e)
    },
    // 商品的数量发生了变化
    numberChangeHandler(e) {
      this.updateGoodsCount(e)
    },
  },
}
  1. 优化购物车上的数量与新增新减同步

mixins/tabbar.js

import {
  mapGetters
} from "vuex"

export default {
  onShow() {
    this.setBadge()
  },
  watch: {
    total: {
      handler(val) {
        uni.setTabBarBadge({
          // 0索引是 : 首页 1索引是分类 2索引是购物车 3索引是我的
          index: 2,
          text: val + ''
        })
      },
      immediate: true
    }
  },
  // 声明
  computed: {
    ...mapGetters('m_cart', ['total'])
  },
  methods: {
    setBadge() {
      uni.setTabBarBadge({
        // 0索引是 : 首页 1索引是分类 2索引是购物车 3索引是我的
        index: 2,
        text: this.total + ''
      })
    }
  }
}

在这里插入图片描述

(8).渲染滑动删除的 UI 效果

滑动删除需要用到 uni-ui 的 uni-swipe-action 组件和 uni-swipe-action-item。详细的官方文档请参考SwipeAction 滑动操作。

  1. 改造 cart.vue 页面的 UI 结构,将商品列表区域的结构修改如下(可以使用 uSwipeAction 代码块快速生成基本的 UI 结构):

    <!-- 左滑删除购物信息 -->
    <uni-swipe-action>
      <!-- 循环渲染购物车中的数据 -->
      <block v-for="(item,index) in cart" :key="index">
        <uni-swipe-action-item :right-options="options" @click="">
          <!-- 绑定自定义事件 -->
          <my-goods :item="item" :showRadio="true" @radioChange="radioChangeHandler"
            @numChange="numberChangeHandler"></my-goods>
        </uni-swipe-action-item>
      </block>
    </uni-swipe-action>
    data() {
      return {
        options: [{
          text: '删除',
          style: {
            backgroundColor: '#C00000'
          }
        }]
      };
    },

在这里插入图片描述

(9).实现滑动删除的功能

  1. 在 store/cart.js 模块的 mutations 节点中声明如下的方法,从而根据商品的 Id 从购物车中移除对应的商品:
    // 左滑删除
    removeGoodsById(state, item) {
      // 调用数组的 filter 方法进行过滤
      state.cart = state.cart.filter(x => x.goods_id !== item.goods_id)
      // 通过commit方法,调用 m_cart命名空间下的saveToStorage
      this.commit('m_cart/saveToStorage')
    },
    // 持久化存储本地数据
    saveToStorage(state) {
      uni.setStorageSync('cart', JSON.stringify(state.cart))
    },
  1. 在 cart.vue 页面中,使用 mapMutations 辅助函数,把需要的方法映射到当前页面中使用:
    <!-- 左滑删除购物信息 -->
    <uni-swipe-action>
      <!-- 循环渲染购物车中的数据 -->
      <block v-for="(item,index) in cart" :key="index">
        <uni-swipe-action-item :right-options="options" @click="swiperItemHandler(item)">
          <!-- 绑定自定义事件 -->
          <my-goods :item="item" :showRadio="true" @radioChange="radioChangeHandler"
            @numChange="numberChangeHandler"></my-goods>
        </uni-swipe-action-item>
      </block>
    </uni-swipe-action>
    methods: {
      ...mapMutations('m_cart', ['updateGoodsState']),
      ...mapMutations('m_cart', ['updateGoodsNumber']),
      ...mapMutations('m_cart', ['removeGoodsById']),
      // 更改选中状态
      radioChangeHandler(e) {
        console.log('xxxx->', e)
        this.updateGoodsState(e)
      },
      // 更改数字
      numberChangeHandler(e) {
        console.log('yyyy->', e)
        this.updateGoodsNumber(e)
      },
      // 删除指定项目
      swiperItemHandler(e) {
        console.log('eeee>', e)
        this.removeGoodsById(e)
      }
    }

在这里插入图片描述

10.配置页面 - 收货地址区域

(1). 创建收货地址组件

  1. components 目录上鼠标右键,选择 新建组件,并填写组件相关的信息:

在这里插入图片描述
2. 渲染组件

<template>
  <view>
    <!-- 选择收获地址 -->
    <view class="address-chose-box">
      <button type="primary" size="mini" class="btnChoseAddress">请选择收获地址+</button>
    </view>
    <!-- 底部的边框线 -->
    <image src="@/static/cart_border@2x.png" class="address-border"></image>
  </view>
</template>

<script>
  export default {
    name: "my-address",
    data() {
      return {

      };
    }
  }
</script>

<style lang="scss">
  .address-border {
    // 去除图片与其他元素的间隙
    display: block;
    height: 5px;
    width: 100%;
  }

  .address-chose-box {
    height: 90px;
    display: flex;
    justify-content: center;
    align-items: center;
  }
</style>

在这里插入图片描述

  <!-- 渲染收货信息的盒子 -->
  <view class="address-info-box">
    <view class="row1">
      <view class="row1-left">
        <view class="username">收货人:<text>escook</text></view>
      </view>
      <view class="row1-right">
        <view class="phone">电话:<text>138XXXX5555</text></view>
        <uni-icons type="arrowright" size="16"></uni-icons>
      </view>
    </view>
    <view class="row2">
      <view class="row2-left">收货地址:</view>
      <view class="row2-right">河北省邯郸市肥乡区xxx 河北省邯郸市肥乡区xxx 河北省邯郸市肥乡区xxx 河北省邯郸市肥乡区xxx </view>
    </view>
  </view>
.address-info-box {
    font-size: 12px;
    height: 90px;
    display: flex;
    // 纵向分布
    flex-direction: column;
    justify-content: center;
    padding: 0 5px;

    .row1 {
      display: flex;
      justify-content: space-between;

      .row1-left {
        .username {}
      }

      .row1-right {
        display: flex;
        justify-content: space-around;

        .phone {}
      }

    }

    .row2 {
      display: flex;
      justify-content: space-around;
      align-items: center;
      margin-top: 10px;

      .row2-left {
        white-space: nowrap;
      }

      .row2-right {
        margin-left: 5px;
      }
    }
  }

在这里插入图片描述

(2).实现收货地址区域的按需展示

  1. 在data中定义收获地址的信息对象

在uniapp中,空对象的布尔值默认为true,空字符串的布尔值默认为false

    <!-- 选择收获地址 -->
    <view class="address-chose-box"  v-if="JSON.stringify(address) === '{}'">
      <button type="primary" size="mini" class="btnChoseAddress" >请选择收获地址+</button>
    </view>


    <!-- 渲染收获信息的盒子 -->
    <view class="address-info-box" v-else>
    省略...
    </view>

<script>
  export default {
    name: "my-address",
    data() {
      return {
        // 收货地址
        address: {

        },
      };
    }
  }
</script>

在这里插入图片描述

(3).现选择收货地址的功能

  1. 为 请选择收货地址+ 的 button 按钮绑定点击事件处理函数:
    <!-- 选择收获地址 -->
    <view class="address-chose-box">
      <button type="primary" size="mini" class="btnChoseAddress" v-show="address"
        @click="chooseAddress()">请选择收获地址+</button>
    </view>

    methods: {
      async chooseAddress() {
        // 这个是调用地理位置的封装方法
        const resp = await uni.chooseAddress().catch(err => err);
        console.log(resp)
      }
    }

微信小程序官方文档提示:自 2022 年 7 月 14 日后发布的小程序,若使用该接口,需要在 manifest.json
中进行声明,否则将无法正常使用该接口,2022年7月14日前发布的小程序不受影响。

如果要使用地理位置的API那么我们需要在manifest.json中声明

uni-app项目 在项目根目录中找到 manifest.json 文件,在左侧导航栏选择源码视图,找到mp-weixin 节点,在节点后面加上:

    "requiredPrivateInfos": [
      "getLocation",
      "onLocationChange",
      "startLocationUpdateBackground",
      "chooseAddress"
    ]

配置信息如下:
在这里插入图片描述
当我们点击的时候: 我们发现自动跳转到这个页面
在这里插入图片描述
当我们点击确定之后->会输出控制台的信息
在这里插入图片描述
2. 定义 chooseAddress 事件处理函数,调用小程序提供的 chooseAddress() API 实现选择收货地址的功能:

    methods: {
      async chooseAddress() {
        // 这个是调用地理位置的封装方法: 返回的结果是,第一个是错误信息,第二个是成功的结果
        const [err, succ] = await uni.chooseAddress().catch(err => err);
        if (err === null && succ.errMsg === "chooseAddress:ok") { // 加入说err为空且errMsg为chooseAddress:ok。
          console.log(succ)
          // 赋值到我们的address
          this.address = succ
        }
      }
    }
  1. 定义收货详细地址的计算属性:
    computed: {
      // 收货详细地址的计算属性
      addstr() {
        // 字符串为空布尔值为false
        if (!this.address.provinceName) return ''

        // 拼接 省,市,区,详细地址 的字符串并返回给用户
        return this.address.provinceName + this.address.cityName + this.address.countyName + this.address.detailInfo
      }
    }
  1. 渲染收货地址区域的数据:
<!-- 渲染收货信息的盒子 -->
<view class="address-info-box" v-else>
  <view class="row1">
    <view class="row1-left">
      <view class="username">收货人:<text>{{address.userName}}</text></view>
    </view>
    <view class="row1-right">
      <view class="phone">电话:<text>{{address.telNumber}}</text></view>
      <uni-icons type="arrowright" size="16"></uni-icons>
    </view>
  </view>
  <view class="row2">
    <view class="row2-left">收货地址:</view>
    <view class="row2-right">{{addstr}}</view>
  </view>
</view>

在这里插入图片描述

(4).将 address 信息存储到 vuex 中

  1. store 目录中,创建用户相关的 vuex 模块,命名为 user.js
    在这里插入图片描述
1. 创建js文件
    (1).export default{}
    (2).定义命名空间
    (3).state()=>({})
    (4).mutations:{}
    (5).getters:{}
2. 引入到store.js
	(1). import {moduleUser} from "@/store/user.js"
	(2). 'm_user': moduleUser
3. 引入到main.js	
	(1).import store from "@/store/store.js"
	(2).const app = new Vue({ ...App,store})

user.js

import {} from "vuex"

export default {
  // 开启命名空间
  namespaced: true,
  // 数据
  state: () => ({
    address: {}
  }),
  // 方法
  mutations: {
    // 更新收获地址
    updateAddress(state, address) {
      state.address = address
    }
  },
  getters: {

  }
}

store.js
切记: 引入的时候不要带花括号,否则会失败

// 1. 导入 Vue 和 Vuex
import Vue from 'vue'
import Vuex from 'vuex'
import moduleCart from '@/store/cart.js'
import moduleUser from "@/store/user.js"
// 2. 将 Vuex 安装为 Vue 的插件
Vue.use(Vuex)

// 3. 创建 Store 的实例对象
const store = new Vuex.Store({
  // TODO:挂载 store 模块
  modules: {
    'm_cart': moduleCart,
    'm_user': moduleUser
  },
})

// 4. 向外共享 Store 的实例对象
export default store

main.js

// #ifndef VUE3
import Vue from 'vue'
import App from './App'
// 创建store
import store from "@/store/store.js"
// 1.导入网络请求的包
import {
  $http
} from '@escook/request-miniprogram'
// 2.在 uni-app 项目中,可以把 $http 挂载到 uni 顶级对象之上,方便全局调用
uni.$http = $http

// ⭐ 设置根路径目的是为了防止 网址混乱
$http.baseUrl = 'https://api-hmugo-web.itheima.net'

// 3.请求开始之前做一些事情  (请求拦截器)
$http.beforeRequest = function(options) {
  // do somethimg...
  uni.showLoading({
    title: '数据加载中...'
  })
}
// 4.请求完成之后做一些事情 (响应拦截器)
$http.afterRequest = function() {
  // do something...
  uni.hideLoading()
}

// 封装的展示消息提示的方法 
uni.$showMsg = function(title = '数据加载失败!', duration = 1500) {
  uni.showToast({
    title,
    duration,
    icon: 'success',
  })
}

Vue.config.productionTip = false

App.mpType = 'app'

const app = new Vue({
  ...App,
  store
})
app.$mount()
// #endif

// #ifdef VUE3
import {
  createSSRApp
} from 'vue'
import App from './App.vue'
export function createApp() {
  const app = createSSRApp(App)
  return {
    app
  }
}
// #endif
  1. 改造 address.vue 组件中的代码,使用 vuex 提供的 address 计算属性 替代 data 中定义的本地 address 对象:
// 1. 按需导入 mapState 和 mapMutations 这两个辅助函数
import { mapState, mapMutations } from 'vuex'

export default {
  data() {
    return {
      // 2.1 注释掉下面的 address 对象,使用 2.2 中的代码替代之
      // address: {}
    }
  },
  methods: {
    // 3.1 把 m_user 模块中的 updateAddress 函数映射到当前组件
    ...mapMutations('m_user', ['updateAddress']),
    // 选择收货地址
    async chooseAddress() {
      const [err, succ] = await uni.chooseAddress().catch((err) => err)

      // 用户成功的选择了收货地址
      if (err === null && succ.errMsg === 'chooseAddress:ok') {
        // 3.2 把下面这行代码注释掉,使用 3.3 中的代码替代之
        // this.address = succ

        // 3.3 调用 Store 中提供的 updateAddress 方法,将 address 保存到 Store 里面
        this.updateAddress(succ)
      }
    },
  },
  computed: {
    // 2.2 把 m_user 模块中的 address 对象映射当前组件中使用,代替 data 中 address 对象
    ...mapState('m_user', ['address']),
    // 收货详细地址的计算属性
    addstr() {
      if (!this.address.provinceName) return ''

      // 拼接 省,市,区,详细地址 的字符串并返回给用户
      return this.address.provinceName + this.address.cityName + this.address.countyName + this.address.detailInfo
    },
  },
}

在这里插入图片描述

(5).将 Store 中的 address 持久化存储到本地

user.js

export default {
  // 开启命名空间
  namespaced: true,
  // 数据
  state: () => ({
  // 假如说左边的为空字符串(假)就执行右侧
    address: JSON.parse(uni.getStorageSync('address') || '{}')
  }),
  // 方法
  mutations: {
    // 更新收获地址
    updateAddress(state, item) {
      state.address = item
       // 2. 通过 this.commit() 方法,调用 m_user 模块下的 saveAddressToStorage 方法将 address 对象持久化存储到本地
      // 调用方法 ⭐⭐
      this.commit('m_user/saveAddressToStorage')
    },
    // 声明方法 ⭐
    saveAddressToStorage(state) {
      uni.setStorageSync('address', JSON.stringify(state.address))
    }
  },
  getters: {

  },
}

在这里插入图片描述

(6).将 addstr 抽离为 getters

  1. 剪切 my-address.vue 组件中的 addstr 计算属性的代码,粘贴到 user.js 模块中,作为一个 getters 节点:
// 数据包装器
getters: {
  // 收货详细地址的计算属性
  addstr(state) {
    if (!state.address.provinceName) return ''

    // 拼接 省,市,区,详细地址 的字符串并返回给用户
    return state.address.provinceName + state.address.cityName + state.address.countyName + state.address.detailInfo
  }
}
  1. 改造 my-address.vue 组件中的代码,通过 mapGetters 辅助函数,将 m_user 模块中的 addstr 映射到当前组件中使用:
// 按需导入 mapGetters 辅助函数
import { mapState, mapMutations, mapGetters } from 'vuex'

export default {
  // 省略其它代码
  computed: {
    ...mapState('m_user', ['address']),
    // 将 m_user 模块中的 addstr 映射到当前组件中使用
    ...mapGetters('m_user', ['addstr']),
  },
}

(7).重新选择收货地址

  1. 为 class 类名为 address-info-box 的盒子绑定 click 事件处理函数如下:
<!-- 渲染收货信息的盒子 -->
<view class="address-info-box" v-else @click="chooseAddress">
  <!-- 省略其它代码 -->
</view>

在这里插入图片描述

11.配置页面 - 结算区域

(1).把结算区域封装为组件

  1. components 目录中,新建 my-settle 结算组件:
    在这里插入图片描述
  2. 初始化 my-settle 组件的基本结构和样式:
<template>
  <view class="my-settle-container">
    <h1>结算</h1>
  </view>
</template>

<script>
  export default {
    name: "my-settle",
    data() {
      return {

      };
    }
  }
</script>

<style lang="scss">
  .my-settle-container {
    position: fixed;
    bottom: 0;
    width: 100%;
    height: 50px;
    background-color: rebeccapurple;
  }
</style>
  1. 在 cart.vue 页面中使用自定义的 my-settle 组件,并美化页面样式,防止页面底部被覆盖
<template>
  <view class="cart-container">
    <!-- 使用自定义的 address 组件 -->

    <!-- 购物车商品列表的标题区域 -->

    <!-- 商品列表区域 -->

    <!-- 结算区域 -->
    <my-settle></my-settle>
  </view>
</template>

<style lang="scss">
.cart-container {
  padding-bottom: 50px;
}
</style>

在这里插入图片描述

(2).渲染结算区域的结构和样式

  1. 定义如下的 UI 结构:
<template>
  <view class="my-settle-container">
    <!-- 全选 -->
    <label class="radio">
      <radio color="#C00000" checked="true" /><text>全选</text>
    </label>
    <!-- 合计 -->
    <view class="amount-box">
      合计: <text class="amount">¥123.00</text>
    </view>
    <!-- 结算按钮 -->
    <view class="btn-settle">
      结算(0)
    </view>
  </view>
</template>
  1. 美化样式:
<style lang="scss">
  .my-settle-container {
    position: fixed;
    bottom: 0;
    width: 100%;
    height: 50px;
    background-color: white;

    display: flex;
    justify-content: space-between;
    align-items: center;
    font-size: 14px;
    padding-left: 5px;

    // 全选
    .radio {
      display: flex;
      align-items: center;
    }

    // 合计
    .amount-box {

      // 金钱
      .amount {
        color: #C00000;
        font-weight: bold;
      }
    }

    // 结算
    .btn-settle {
      // 盒子高度
      height: 50px;
      // 行高
      line-height: 50px;
      background-color: #C00000;
      align-items: center;
      padding: 0 10px;
      min-width: 100px;
      text-align: center;
    }
  }
</style>

在这里插入图片描述

(3). 动态渲染已勾选商品的总数量

  1. 在 store/cart.js 模块中,定义一个名称为 checkedCount 的 getters,用来统计已勾选商品的总数量:
    // 勾选的商品的个数
    checkedCount(state) {
      // 首先获取所有已勾选的项组成一个数组
      // 然后利用数组的reduce((回调函数的返回值也就是return返回的,循环的item项)=>{},0)方法 : 第一个参数是回调函数,第二个参数是初始值
      // 这里total初始化为0
      return state.cart.filter(x => x.goods_state === true).reduce((total, item) => {
        return total += item.goods_count
      }, 0)
    }
  1. 在 my-settle 组件中,通过 mapGetters 辅助函数,将需要的 getters 映射到当前组件中使用:
import { mapGetters } from 'vuex'

export default {
  computed: {
    ...mapGetters('m_cart', ['checkedCount']),
  },
  data() {
    return {}
  },
}
  1. 将 checkedCount 的值渲染到页面中:
<!-- 结算按钮 -->
<view class="btn-settle">结算({{checkedCount}})</view>

在这里插入图片描述

(5).动态渲染全选按钮的选中状态

1 .使用 mapGetters 辅助函数,将商品的总数量映射到当前组件中使用,并定义一个叫做 isFullCheck 的计算属性:

import { mapGetters } from 'vuex'

export default {
  computed: {
    // 1. 将 total 映射到当前组件中
    ...mapGetters('m_cart', ['checkedCount', 'total']),
    // 2. 是否全选
    isFullCheck() {
      return this.total === this.checkedCount
    },
  },
  data() {
    return {}
  },
}

  1. 为 radio 组件动态绑定 checked 属性的值:
<!-- 全选区域 -->
<label class="radio">
  <radio color="#C00000" :checked="isFullCheck" /><text>全选</text>
</label>

在这里插入图片描述

(6). 实现商品的全选/反选功能

  1. store/cart.js 模块中,定义一个叫做 updateAllGoodsStatemutations 方法,用来修改所有商品的勾选状态:
// 更新所有商品的勾选状态
updateAllGoodsState(state, newState) {
  // 循环更新购物车中每件商品的勾选状态
  state.cart.forEach(x => x.goods_state = newState)
  // 持久化存储到本地
  this.commit('m_cart/saveToStorage')
}
  1. 在 my-settle 组件中,通过 mapMutations 辅助函数,将需要的 mutations 方法映射到当前组件中使用:
// 1. 按需导入 mapMutations 辅助函数
import { mapGetters, mapMutations } from 'vuex'

export default {
  // 省略其它代码
  methods: {
    // 2. 使用 mapMutations 辅助函数,把 m_cart 模块提供的 updateAllGoodsState 方法映射到当前组件中使用
    ...mapMutations('m_cart', ['updateAllGoodsState']),
  },
}
  1. 为 UI 中的 label 组件绑定 click 事件处理函数:
<!-- 全选区域 -->
<label class="radio" @click="changeAllState">
  <radio color="#C00000" :checked="isFullCheck" /><text>全选</text>
</label>
  1. 在 my-settle 组件的 methods 节点中,声明 changeAllState 事件处理函数:
methods: {
  ...mapMutations('m_cart', ['updateAllGoodsState']),
  // label 的点击事件处理函数
  changeAllState() {
    // 修改购物车中所有商品的选中状态
    // !this.isFullCheck 表示:当前全选按钮的状态取反之后,就是最新的勾选状态
    this.updateAllGoodsState(!this.isFullCheck)
  }
}

在这里插入图片描述

(7). 动态渲染已勾选商品的总价格

  1. 在 store/cart.js 模块中,定义一个叫做 checkedGoodsAmount 的 getters,用来统计已勾选商品的总价格:
// 已勾选的商品的总价
checkedGoodsAmount(state) {
  // 先使用 filter 方法,从购物车中过滤器已勾选的商品
  // 再使用 reduce 方法,将已勾选的商品数量 * 单价之后,进行累加
  // reduce() 的返回值就是已勾选的商品的总价
  // 最后调用 toFixed(2) 方法,保留两位小数
  return state.cart.filter(x => x.goods_state)
                   .reduce((total, item) => total += item.goods_count * item.goods_price, 0)
                   .toFixed(2)
}
  1. 在 my-settle 组件中,使用 mapGetters 辅助函数,把需要的 checkedGoodsAmount 映射到当前组件中使用:
...mapGetters('m_cart', ['total', 'checkedCount', 'checkedGoodsAmount'])
  1. 在组件的 UI 结构中,渲染已勾选的商品的总价:
<!-- 合计区域 -->
<view class="amount-box">
  合计:<text class="amount">¥{{checkedGoodsAmount}}</text>
</view>

(8).渲染购物车为空时的页面结构

  1. 将 资料 目录中的 cart_empty@2x.png 图片复制到项目的 /static/ 目录中

  2. 改造 cart.vue 页面的 UI 结构,使用 v-if 和 v-else 控制购物车区域和空白购物车区域的按需展示:

<template>
  <view class="cart-container" v-if="cart.length !== 0">

    <!-- 使用自定义的 address 组件 -->

    <!-- 购物车商品列表的标题区域 -->

    <!-- 商品列表区域 -->

    <!-- 结算区域 -->

  </view>

  <!-- 空白购物车区域 -->
  <view class="empty-cart" v-else>
    <image src="/static/cart_empty@2x.png" class="empty-img"></image>
    <text class="tip-text">空空如也~</text>
  </view>
</template>
  1. 美化空白购物车区域的样式:
.empty-cart {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding-top: 150px;

  .empty-img {
    width: 90px;
    height: 90px;
  }

  .tip-text {
    font-size: 12px;
    color: gray;
    margin-top: 15px;
  }
}

在这里插入图片描述

(9).分支的合并与提交

git add .
git commit -m "完成购物车的开发"
git push -u origin cart
git checkout master
git add .
git commit -m "完成购物车的开发"
git merge cart
git push

git branch -d cart

在这里插入图片描述

12.配置页面 - 登入与支付

(1).创建settle分支

git checkout -b settle

在这里插入图片描述

(2). 点击结算按钮进行条件判断

说明:用户点击了结算按钮之后,需要先后判断是否勾选了要结算的商品、是否选择了收货地址、是否登录。

  1. 在 my-settle 组件中,为结算按钮绑定点击事件处理函数:
<!-- 结算按钮 -->
<view class="btn-settle" @click="settlement">结算({{checkedCount}})</view>
  1. 在 my-settle 组件的 methods 节点中声明 settlement 事件处理函数如下:
// 点击了结算按钮
settlement() {
  // 1. 先判断是否勾选了要结算的商品
  if (!this.checkedCount) return uni.$showMsg('请选择要结算的商品!')

  // 2. 再判断用户是否选择了收货地址
  if (!this.addstr) return uni.$showMsg('请选择收货地址!')

  // 3. 最后判断用户是否登录了
  if (!this.token) return uni.$showMsg('请先登录!')
}
  1. 在 my-settle 组件中,使用 mapGetters 辅助函数,从 m_user 模块中将 addstr 映射到当前组件中使用:
export default {
  computed: {
    ...mapGetters('m_cart', ['total', 'checkedCount', 'checkedGoodsAmount']),
    // addstr 是详细的收货地址
    ...mapGetters('m_user', ['addstr']),
    isFullCheck() {
      return this.total === this.checkedCount
    },
  },
}
  1. 在 store/user.js 模块的 state 节点中,声明 token 字符串:
export default {
  // 开启命名空间
  namespaced: true,

  // state 数据
  state: () => ({
    // 收货地址
    address: JSON.parse(uni.getStorageSync('address') || '{}'),
    // 登录成功之后的 token 字符串
    token: '',
  }),

  // 省略其它代码
}
  1. 在 my-settle 组件中,使用 mapState 辅助函数,从 m_user 模块中将 token 映射到当前组件中使用
// 按需从 vuex 中导入 mapState 辅助函数
import { mapGetters, mapMutations, mapState } from 'vuex'

export default {
  computed: {
    ...mapGetters('m_cart', ['total', 'checkedCount', 'checkedGoodsAmount']),
    ...mapGetters('m_user', ['addstr']),
    // token 是用户登录成功之后的 token 字符串
    ...mapState('m_user', ['token']),
    isFullCheck() {
      return this.total === this.checkedCount
    },
  },
}

在这里插入图片描述

(3).登入页面

  1. 点击 微信开发者工具 工具栏上的编译模式下拉菜单,选择 添加编译模式:
    在这里插入图片描述

(4). 实现登录和用户信息组件的按需展示

  1. 在 components 目录中新建登录组件:
  2. 在 components 目录中新建用户信息组件:
    新增组件一定要重新更新
    在这里插入图片描述

在这里插入图片描述
3. 在 my.vue 页面中,通过 mapState 辅助函数,导入需要的 token 字符串*并进行按需判断:

<template>
  <view>
    <!-- 为登入的时候展示 空字符串白默认为false -->
    <my-login v-if="!tocken"></my-login>
    <!-- 登入的时候展示 -->
    <my-userinfo v-else></my-userinfo>
  </view>
</template>

<script>
  import badgeMix from '@/mixins/tabbar-badge.js'
  import {
    mapState
  } from "vuex"
  export default {
    mixins: [badgeMix],
    computed: {
      ...mapState('m_user', ['tocken'])
    },
    data() {
      return {

      };
    }
  }
</script>

<style lang="scss">

</style>

在这里插入图片描述

(5).实现登入组件的基本布局

my-login.vue

<template>
  <view class="login-container">
    <!-- 提示登录的图标 -->
    <uni-icons type="contact-filled" size="100" color="#AFAFAF"></uni-icons>
    <button type="primary" class="btn-login">一键登入</button>
    <text class="tips-text">登入后尽享更多权益</text>
  </view>
</template>

<script>
  export default {
    name: "my-login",
    data() {
      return {

      };
    }
  }
</script>

<style lang="scss">
  .login-container {
    height: 750rpx;
    background-color: #f8f8f8;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;

    .contact-filled {}

    .btn-login {
      width: 90%;
      // 圆角
      border-radius: 100px;
      margin: 15px 0;
      background-color: #c00000;
    }

    .tips-text {
      font-size: 14px;
      color: gray;
    }
  }
</style>

在这里插入图片描述

(6).点击登录按钮获取微信用户的基本信息

需求描述:需要获取微信用户的头像、昵称等基本信息。

  1. 为登录的 button 按钮绑定 open-type="getUserInfo" 属性,表示点击按钮时,希望获取用户的基本信息:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/646683.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

java循环语句

文章目录 for循环while循环do-while循环嵌套循环关键字break和continue的使用break带标签的使用 for循环 语法格式&#xff1a; for (①初始化部分; ②循环条件部分; ④迭代部分)&#xff5b;③循环体部分; &#xff5d;说明&#xff1a; for(;;)中的两个&#xff1b;不能多…

视觉SLAM十四讲——ch7实践(视觉里程计1)

视觉SLAM十四讲----ch7的实践操作及避坑 1. 实践操作前的准备工作2. 实践过程2.1 特征提取与匹配2.2 对极几何2.3 三角测量2.4 求解PnP2.5 求解ICP 3. 遇到的问题3.1 准备工作遇到的问题 1. 实践操作前的准备工作 在终端中进入ch7文件夹下&#xff0c;顺序执行以下命令进行编译…

Sentinel的限流和Gateway的限流差别?

Sentinel的限流与Gateway的限流有什么差别&#xff1f; 问题说明&#xff1a;考察对限流算法的掌握情况 限流算法常见的有三种实现&#xff1a;滑动时间窗口&#xff0c;令牌桶算法&#xff0c;漏桶算法。gateway则采用基于Redis实现的令牌桶算法。但是我们不会去用&#xff…

PCB做了盲埋孔,还有必要再做盘中孔工艺吗

一博高速先生成员--王辉东 初夏的西湖美艳无边&#xff0c;若不去看看人生总觉遗憾。 杭州两大美女明明和琪琪约好这个星期天&#xff0c;一起去西湖转转&#xff0c;到灵隐寺许个愿&#xff0c;再到北高峰爬个山。 话说两人正行之间&#xff0c;看到正对面也有两个美女结伴同…

Spring Security Oauth2.1 最新版 1.1.0 整合 gateway 完成授权认证(基于 springboot 3.1)

目录 背景 版本 Spring Boot 3.1 Spring Authorization Server 1.1.0官方文档 基础 spring security OAuth2.0 模块构成 授权方式 集成过程 官方demo 代码集成 依赖 授权服务AuthorizationServerConfig配置 重要组件 测试 查看授权服务配置 访问授权服务 授…

渗透测试与自动化安全测试工具比较

应用程序安全性并不新鲜&#xff0c;但它在需求、复杂性和深度方面正迅速增长。随着网络犯罪自疫情爆发以来增长了近600%&#xff0c;越来越多的SaaS企业开始争相保护他们的应用程序。即使那些运行最新端点保护的系统也面临重大漏洞。 然而随之而来的一个问题是&#xff1a;即…

【javaweb+springboot】旅游网页面设计(主购物车功能)——前后端分离+服务端客户端增删改查(完整代码+文档)

一、项目背景 由于疫情原因&#xff0c;张家界旅游业受到很大的影响&#xff0c;为了促进旅游业的发展&#xff0c;吸引更多游客来到张家界旅游&#xff0c;帮助游客更好地了解张家界&#xff0c;创建张家界旅游网&#xff0c;推进旅游发展大会的开展&#xff0c;展示当地风土人…

商城系统功能有哪些?

商城系统是一种以电子商务为基础的技术工具&#xff0c;为企业涉足电子商务提供了完整的解决方案。商城系统不仅可以帮助企业降低成本&#xff0c;提高效率&#xff0c;还可以实现全方位的在线营销&#xff0c;为企业争取更多的竞争优势&#xff0c;如SHOP、Magento等一系列成熟…

EBU5476 Microprocessor System Design 知识点总结_3 Assembly

Assembly 汇编语法。 顺序结构 label ; 可省略&#xff0c;用于跳转到此位置助记符 operand1, operand2, … ; CommentsMOV r1, #0x01 ; 数据0x01放入r1 MOV r1, #A ; 数据A的ascii码放入r1 MOV R0, R1 ; move R1 into R0 MOVS R0, R1 ; move R1 i…

当 GraphQL 遇上图数据库,便有了更方便查询数据的方式

人之初&#xff0c;性本鸽。 大家好&#xff0c;我叫储惠龙&#xff08;实名上网&#xff09;&#xff0c;你可以叫我小龙人&#xff0c;00 后一枚。目前从事后端开发工作。 今天给大家带来一个简单的为 NebulaGraph 提供 GraphQL 查询支持的 DEMO&#xff0c;为什么是简单的…

职业教育机构转线上时,选择平台要注意哪些方面?

职业教育是提升技能和知识的重要途径&#xff0c;有效的职业教育能够帮助培养和发展人才&#xff0c;相比较线下面授课程相比&#xff0c;在线直播的教学&#xff0c;可以节省较大成本&#xff0c;那么在选型直播平台时&#xff0c;要注意哪些方面呢&#xff1f; 1.需要实现高清…

记录一次使用__dirname和./引出的bug

JS项目中 保存本地生成的图片时使用的路径:__dirname“/waitToFinishTask.png"。 但是在获取这张图片的时候我使用的是“./waitToFinishTask.png”。 从而抛出异常&#xff1a;Error: ENOENT, No such file or directory ./waitToFinishTask.png 找了好久都不知道为什么会…

【无标题】windows下使用cmake编译c++

好久没有更新博客了 最近在做c相关的&#xff0c;编译起来确实很痛苦。 所以心血来潮&#xff0c;继续更新一下 主要还是一些跨平台的库&#xff0c;比如zlib、libpng、opencv、ffmpeg 编译工具使用mingw作为主要编译环境支持&#xff0c;使用msys进行编译。 一、下载mingw…

利用etcd实现分布式锁

python etcd3模块的lock使用 观察lock的加解锁影响 在python中已经自带了分布式锁的实现方式&#xff0c;下面我们尝试一下加锁与解锁的流程 在运行该demo同时也对lock对应的key进行watch&#xff0c;观察其变化&#xff0c;注意python-etcd3在实现分布式锁的时候&#xff0…

前端终止请求的三种方式(ajax、axios)

一、原生ajax终止请求 1、abort() ​ XMLHttpRequest.abort() 方法用于终止 XMLHttpRequest 对象的请求&#xff0c;该方法没有参数&#xff0c;也没有返回值。当调用该方法时&#xff0c;如果对应 XMLHttpRequest 对象的请求已经被发送并且正在处理中&#xff0c;则会中止该…

一个专科生的 Python 转行之路,虽然很难,但如今月薪1w,一切值得

一个专科生的 Python 转行之路&#xff0c;虽然很难&#xff0c;但如今月薪1w&#xff0c;一切值得 相信每个转 IT 的人, 大部分是兴趣驱动。然而我并不是, 只能说是不反感。一开始接触编程, 是一位同事&#xff0c;他会 java &#xff0c;也会一点前端。 印象最深刻的一次&…

前端如何处理「并发」问题?

&#x1f431; 个人主页&#xff1a;不叫猫先生&#xff0c;公众号&#xff1a;前端舵手 &#x1f64b;‍♂️ 作者简介&#xff1a;2022年度博客之星前端领域TOP 2&#xff0c;前端领域优质作者、阿里云专家博主&#xff0c;专注于前端各领域技术&#xff0c;共同学习共同进步…

Avalon 学习系列(四)—— 循环遍历

Avalon2 的 ms-for 绑定集齐了 ms-repeat, ms-each, ms-with 的所有功能&#xff0c; 更加好用&#xff0c; 性能也提升了很多。 Avalon 不需要 vue 或 react 那样使用 key 属性来提高性能&#xff0c;内部已经帮你搞定了。 循环数组 ms-for 循环数组示例&#xff1a; <…

运维圣经:勒索病毒应急响应指南

目录 勒索病毒简介 常见勒索病毒种类 WannaCry Globelmposter Crysis/ Dharma 攻击特点 应急响应方法指南 一. 隔离被感染的服务器/主机 二. 排查业务系统 三. 确定勒索病毒种类&#xff0c; 进行溯源分析 四. 恢复数据和业务 五. 清理加固 病毒清理及加固 感染文…

零基础也能懂的python办公自动化教程,从此上班摸鱼轻轻松松

前言 如今Python在自动化办公领域的表现越来越亮眼&#xff0c;受到了很多非IT的职场人士的推崇&#xff0c;也引得更多的人去了解、学习Python。但是很多初学者都会面临这么一个困惑&#xff1a;想把Python应用在工作中&#xff0c;却不知从何下手&#xff01;&#xff08;资…