Dark Horse WeChat Mini Program Project·Black Horse Yougou

Table of contents

Overall project effect display:

1. Create a new uni-app project

 Project directory structure

 Run the project on WeChat Developer Tools

Use git to manage projects

 Host the project on github for management

2. Implement tabBar effect

1. Create a new tabBar branch

2. Create a new tabBar page

3. Configure tabBar effect

4. Modify the navigation bar style effect

 5. Submit tabBar code

3. Achieve home page effect

1. Configure network requests

2. Mount $http, configure request interceptor and response interceptor

3. Complete the carousel chart

4. Complete the category navigation area

5. Floor structure

6. Submit the code for the home page

4. Implement classification

1. Implement the basic structure

2. Obtain classified data

3. Render a first-level classification list

4. Render the secondary classification list

5. Render three-level classification structure

6. Reset the position of the scroll bar after switching the first-level category

7. Click on the third-level classification to jump to the product list page

 5. Search function

1. Create a new custom component

 2. Enhance the versatility of components through custom attributes

3. Encapsulate click events for custom components

 4. Realize the ceiling effect of the homepage search component

5. Implementation of search suggestions 

 6. Search history

 6. Product list

1. Define the request parameter object

 2. Get product list data

3. Render product list structure 

 4. Encapsulate product items into custom components

 5. Use filters to process prices

 6. Pull up to load more

7. Prevent additional requests from being initiated through throttling 

8. Pull down to refresh

 9. Click on the product item to jump to the details page

7. Product details

1. Get product details data

2. Render the product details page carousel area

3. Render product information area 

 4. Render product details information

5. Solve the problem of product price flashing 

 6. Render the UI structure of the product navigation area

7. Click to jump to the shopping cart page 

8. Add to cart 

1. Configure vuex

 2. Create the store module of the shopping cart

 3. Use the data in the Store in the product details page

4. Implement the function of adding to shopping cart 

 5. Dynamically count the total number of items in the shopping cart

 6. Persistent storage of items in the shopping cart

7. Optimize the total listener of the product details page 

8. Dynamically set a digital logo for the tabBar page 

9. Extract the code that sets the tabBar logo into mixins 

9. Shopping cart page 

Product list area

1. Render the title area of ​​the shopping cart product list

 2. Render the basic structure of the product list area

 3. Encapsulate the radio check status for the my-goods component

 4. Encapsulate the radio-change event for the my-goods component

5. Modify the check status of the items in the shopping cart 

 6. Encapsulate NumberBox for my-goods component

 7. Encapsulate the num-change event for the my-goods component

8. Modify the quantity of items in the shopping cart

9. Render the UI effect of sliding deletion 

 10. Implement sliding delete function

Shipping address area 

1. Create a component for the shipping address

2. Render the basic structure of the shipping address component:

3. Achieve on-demand display of the delivery address area

4. Implement the function of selecting the delivery address 

 5. Store address information in vuex

6. Persistently store the address in the Store locally 

7. Extract addstr into getters 

 8.Reselect the delivery address

settlement area 

1. First create a new settlement component

2. Initialize the basic structure and style of the my-settle component:

 3. Render the structure and style of the settlement area

4. Dynamically render the total quantity of checked products

5. Dynamically render the selected state of the select all button 

6. Implement the function of selecting all/inverse selection of products

7 Dynamically render the total price of the selected items

 8. Dynamically calculate the value of the shopping cart logo

9. Render the page structure when the shopping cart is empty 

10. Login and payment 

1. Click the settlement button to judge the conditions

 2. Log in

2.1 Implement on-demand display of login and user information components

 2.2 Implement the basic layout of the login component

2.3 Click the login button to obtain basic information of WeChat users

 2.4 Store user’s basic information in vuex

2.5 Log in to obtain the Token string 

2.6 Store Token in vuex 

In the mutations node of the store/user.js module, declare the following two methods:

3. User information 

3.1 Implement the basic layout of the user avatar nickname area

3.2 Render the user’s avatar and nickname 

 3.3 Rendering panel information area

renderings

3.4 Implement the function of logging out

4. Automatically jump to the login page after three seconds 

 5. WeChat payment

1. Add the Token identity authentication field in the request header

2. WeChat payment process


Overall project effect display:

 

 

 

 

1. Create a new uni-app project

 Project directory structure

 Run the project on WeChat Developer Tools

 

 

Use git to manage projects

Step 1: Create a new .gitignore file in the project directory, and then write the files to be ignored in it

/node_modules
/unpackage/dist

Step 2: In order for git to track the unpackage file, you need to create a new .gitkeep file in the file directory.

Step 3: Open the git Bash terminal to initialize a git warehouse

git init 

Then add the file to the staging area

git add .

Then submit the project for the first time

git commit -m "init project"

 Host the project on github for management

First create a new warehouse

 Follow the instructions below to operate on git Bash

! ! ! There is a wrong place marked here. The fourth item is not to create a new branch, but to modify the name of the existing branch. The default branch name of our new warehouse is master.

 After the submission is successfully refreshed, you can see that our project warehouse already has the files you just submitted.

 Later, we only need  the git push -u origin branch name command to submit the code to github every time we submit the code, but we must pay attention to the differences in branches.

2. Implement tabBar effect

1. Create a new tabBar branch

git checkout -b tabBar

You can use this command to view all branches

git branch -v

2. Create a new tabBar page

 Similarly, create the cate cart my page separately.

3. Configure tabBar effect

first step:

Replace the original static file with the icon of the tabBar that needs to be used.

Step two:

Configure tabBar in page.json file

"tabBar": {
		"selectedColor": "#C00000",
		"list": [
			{
				"pagePath": "pages/home/home",
				"text": "首页",
				"selectedIconPath": "static/tab_icons/home-active.png",
				"iconPath": "static/tab_icons/home.png"
			},
			{
				"pagePath": "pages/cate/cate",
				"text": "分类",
				"selectedIconPath": "static/tab_icons/cate-active.png",
				"iconPath": "static/tab_icons/cate.png"
			},
			{
				"pagePath": "pages/cart/cart",
				"text": "购物车",
				"selectedIconPath": "static/tab_icons/cart-active.png",
				"iconPath": "static/tab_icons/cart.png"
			},
			{
				"pagePath": "pages/my/my",
				"text": "我的",
				"selectedIconPath": "static/tab_icons/my-active.png",
				"iconPath": "static/tab_icons/my.png"
			}
		]
	}

third step:

Delete the original index page

 The effect comes out

4. Modify the navigation bar style effect

 5. Submit tabBar code

Open git Bash and enter the following command

git add . 
git commit -m "完成tabBar效果" //提交本地
git push -u origin tabBar  //提交分支tabBar至远程仓库
git checkout main //切换分支到main
git merge tabBar //合并tabBar至main
git push origin main //提交合并后main至远程仓库
git branch -d tabBar //删除本地的tabBar分支

Subsequent new branches and submitted codes will no longer be displayed.

3. Achieve home page effect

1. Configure network requests

Because the mini program does not support axios, and the effect of the native wx.request API cannot support interceptors and other functions, we use a third-party package to initialize an npm package manager in the terminal of the project directory, and then download the package

npm init -y
npm i @escook/request-miniprogram

2. Mount $http, configure request interceptor and response interceptor

//main.js文件中
//导入网络请求的包
import	{$http} from '@escook/request-miniprogram'
uni.$http = $http

$http.beforeRequest = function(options){
	wx.showLoading({
		title:"数据加载中"
	})
}

$http.afterRequest = function(){
	uni.hideLoading()
}

3. Complete the carousel chart

3.1 Get carousel chart data

First configure the request path in the main.js file

//请求根路径
$http.baseUrl = 'https://api-hmugo-web.itheima.net'

Request data in home component

	data() {
			return {
				swiperList:[]
			};
		},
		onLoad() {
			//调用方法获取轮播图数据
			this.getSwiperList()
		},
		methods:{
			async getSwiperList(){
			const {data:res} = await uni.$http.get('/api/public/v1/home/swiperdata')
			if(res.meta.status !== 200){  //请求失败给一个弹窗
				return uni.showToast({
						title:'数据请求失败',
						duration:1500,
						icon:'none'
					})
			}
			this.swiperList = res.message
			}
		}

3.2 Render carousel image

<swiper :indicator-dots="true" :autoplay="true" :interval="3000" :duration="1000" :circular="true">
			<swiper-item v-for="(item , index) in swiperList" :key = 'index'>
				<view class="swiper-item"> 
				<image :src="item.image_src" mode=""></image>
				</view>
			</swiper-item>
		</swiper>

//scss样式

swiper{
	height: 330rpx;
	.swiper-item,image{
		width: 100%;
		height: 100%;
	}
}

3.3 Configure subcontracting

Add the following configuration to pages.json and save it

"subPackages": [
		{
			"root": "subpkg",
			"pages": []
		}
	],

Then create a new page in the subpkg directory and select subpkg subcontracting

 3.4 Implement clicking on the carousel image to jump to the details page and passing the goods_id parameter

Modify ul structure

	<!-- 轮播图 -->
		<swiper :indicator-dots="true" :autoplay="true" :interval="3000" :duration="1000" :circular="true">
			<swiper-item v-for="(item , index) in swiperList" :key = 'index'>
				<navigator class="swiper-item" :url="'/subpkg/goods_detail/goods_detail?goods_id=' + item.goods_id"> 
				<image :src="item.image_src" mode=""></image>
				</navigator>
			</swiper-item>
		</swiper>

3.5 Encapsulate the uni.$showMsg() method

In  main.js ,  uni mount a custom  $showMsg() method for the object:

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

In the future, when you need to prompt a message,  uni.$showMsg() you can directly call the method:

async getSwiperList() {
   const { data: res } = await uni.$http.get('/api/public/v1/home/swiperdata')
   if (res.meta.status !== 200) return uni.$showMsg()
   this.swiperList = res.message
}

4. Complete the category navigation area

4.1 Obtain classified data

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

4.2 Render nav structure

<!-- 分类导航区域 -->
		<view class="nav-list">
		   <view class="nav-item" v-for="(item, i) in navList" :key="i" @click="navClickHandler(item)>
		     <image :src="item.image_src" class="nav-img"></image>
		   </view>
		</view>

scss

.nav-list {
  display: flex;
  justify-content: space-around;
  margin: 15px 0;

  .nav-img {
    width: 128rpx;
    height: 140rpx;
  }
}

Click event function

// nav-item 项被点击时候的事件处理函数
navClickHandler(item) {
  // 判断点击的是哪个 nav
  if (item.name === '分类') {
    uni.switchTab({
      url: '/pages/cate/cate'
    })
  }
}

5. Floor structure

5.1 Get data

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

5.2 Rendering structure

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

scss

	.floor-title {
		height: 60rpx;
		width: 100%;
		display: flex;
	}
	.right-img-box {
	  display: flex;
	  flex-wrap: wrap;
	  justify-content: space-around;
	}
	
	.floor-img-box {
	  display: flex;
	  padding-left: 10rpx;
	}

5.3 Implementation of clicking on the image to jump to the product list page

In  subpkg subcontracting, create  a new goods_list page

After the floor data request is successful,  forEach the URL address is processed through a double-layer loop:

// 获取楼层列表数据
async getFloorList() {
  const { data: res } = await uni.$http.get('/api/public/v1/home/floordata')
  if (res.meta.status !== 200) return uni.$showMsg()

  // 通过双层 forEach 循环,处理 URL 地址
  res.message.forEach(floor => {
    floor.product_list.forEach(prod => {
      prod.url = '/subpkg/goods_list/goods_list?' + prod.navigator_url.split('?')[1]
    })
  })

  this.floorList = res.message
}

Transform the outer  view components of the image into  navigator components and dynamically bind  url 属性 the values:

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

6. Submit the code for the home page

Just follow what you did before

Effect picture after implementation

4. Implement classification

1. Implement the basic structure

  • Use scroll-view to achieve sliding effect
  • Use uni.getSystemInfoSync() to obtain device information to achieve adaptive effects
  • Beautify the style, using pseudo elements to implement red indicator edges
<template>
  <view>
    <view class="scroll-view-container">
      <!-- 左侧的滚动视图区域 -->
      <scroll-view class="left-scroll-view" scroll-y :style="{height: wh + 'px'}">
        <view class="left-scroll-view-item active">xxx</view>
        <view class="left-scroll-view-item">xxx</view>
        <view class="left-scroll-view-item">xxx</view>
        <view class="left-scroll-view-item">xxx</view>
        <view class="left-scroll-view-item">xxx</view>
		<view class="left-scroll-view-item">xxx</view>
		<view class="left-scroll-view-item">xxx</view>
		<view class="left-scroll-view-item">xxx</view>
		<view class="left-scroll-view-item">xxx</view>
		<view class="left-scroll-view-item">xxx</view>
		<view class="left-scroll-view-item">xxx</view>
		<view class="left-scroll-view-item">xxx</view>
		<view class="left-scroll-view-item">xxx</view>
		<view class="left-scroll-view-item">xxx</view>
		<view class="left-scroll-view-item">xxx</view>
		<view class="left-scroll-view-item">xxx</view>
		<view class="left-scroll-view-item">xxx</view>
		<view class="left-scroll-view-item">xxx</view>
		<view class="left-scroll-view-item">xxx</view>
		<view class="left-scroll-view-item">xxx</view>
		<view class="left-scroll-view-item">xxx</view>
		<view class="left-scroll-view-item">xxx</view>
		<view class="left-scroll-view-item">xxx</view>
		<view class="left-scroll-view-item">xxx</view>
        <view class="left-scroll-view-item">多复制一些节点,演示纵向滚动效果...</view>
      </scroll-view>
      <!-- 右侧的滚动视图区域 -->
      <scroll-view class="right-scroll-view" scroll-y :style="{height: wh + 'px'}">
        <view class="left-scroll-view-item">zzz</view>
        <view class="left-scroll-view-item">zzz</view>
        <view class="left-scroll-view-item">zzz</view>
        <view class="left-scroll-view-item">zzz</view>
		<view class="left-scroll-view-item">zzz</view>
		<view class="left-scroll-view-item">zzz</view>
		<view class="left-scroll-view-item">zzz</view>
		<view class="left-scroll-view-item">zzz</view>
		<view class="left-scroll-view-item">zzz</view>
		<view class="left-scroll-view-item">zzz</view>
		<view class="left-scroll-view-item">zzz</view>
		<view class="left-scroll-view-item">zzz</view>
		<view class="left-scroll-view-item">zzz</view>
		<view class="left-scroll-view-item">zzz</view>
		<view class="left-scroll-view-item">zzz</view>
		<view class="left-scroll-view-item">zzz</view>
		<view class="left-scroll-view-item">zzz</view>
		<view class="left-scroll-view-item">zzz</view>
		<view class="left-scroll-view-item">zzz</view>
		<view class="left-scroll-view-item">zzz</view>
		<view class="left-scroll-view-item">zzz</view>
		<view class="left-scroll-view-item">zzz</view>
		<view class="left-scroll-view-item">zzz</view>
		<view class="left-scroll-view-item">zzz</view>
		<view class="left-scroll-view-item">zzz</view>
		<view class="left-scroll-view-item">zzz</view>
		<view class="left-scroll-view-item">zzz</view>
		<view class="left-scroll-view-item">zzz</view>
		<view class="left-scroll-view-item">zzz</view>
		<view class="left-scroll-view-item">zzz</view>
		<view class="left-scroll-view-item">zzz</view>
		<view class="left-scroll-view-item">zzz</view>
		<view class="left-scroll-view-item">zzz</view>
		<view class="left-scroll-view-item">zzz</view>
		<view class="left-scroll-view-item">zzz</view>
		<view class="left-scroll-view-item">zzz</view>
		<view class="left-scroll-view-item">zzz</view>
		<view class="left-scroll-view-item">zzz</view>
		<view class="left-scroll-view-item">zzz</view>
		<view class="left-scroll-view-item">zzz</view>
		<view class="left-scroll-view-item">zzz</view>
		<view class="left-scroll-view-item">zzz</view>
		<view class="left-scroll-view-item">zzz</view>
		<view class="left-scroll-view-item">zzz</view>
        <view class="left-scroll-view-item">多复制一些节点,演示纵向滚动效果</view>
      </scroll-view>
    </view>
  </view>
</template>

<script>
	export default {
		data() {
			return {
				wh:0
			};
		},
		onLoad() {
			const sysInfo = uni.getSystemInfoSync() //获取设备的信息
			this.wh = sysInfo.windowHeight //设置高度达到自适应的效果
		}
	}
</script>

<style lang="scss">
.scroll-view-container{
	display: flex;
	.left-scroll-view{
		width: 240rpx;
		.left-scroll-view-item{
			background-color: #f7f7f7;
			line-height: 60px;
			text-align: center;
			font-size: 12px;
			&.active{
				background-color: #fff;
				position:relative;
				&::before{
					content: " ";
					display: block;
					width: 3px;
					height: 30px;
					background-color: #c00000;
					position: absolute;
					top: 50%;
					left: 0;
					transform: translateY(-50%);
				}
			}
		}
	}
}
</style>

2. Obtain classified data

<script>
	export default {
		data() {
			return {
				cateList: []
			};
		},
		onLoad() {
			this.getCateList()
		},
		methods: {
			async getCateList() {
				const {
					data: res
				} = await uni.$http.get('/api/public/v1/categories')
				if (res.meta.status !== 200) return uni.$showMsg()
				this.cateList = res.message
			}
		},
	}
</script>

3. Render a first-level classification list

  • Rendering structures using v-for loop
  • Add a click event to switch active class name dynamic binding

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

click event

methods: {
  // 选中项改变的事件处理函数
  activeChanged(i) {
    this.active = i
  }
}

4. Render the secondary classification list

4.1  data Define the data node of the secondary classification list in

data() {
  return {
    // 二级分类列表
    cateLevel2: []
  }
}

 4.2 Modify  getCateList the method. After requesting the data, assign values ​​to the secondary classification list data:

async getCateList() {
  const { data: res } = await uni.$http.get('/api/public/v1/categories')
  if (res.meta.status !== 200) return uni.$showMsg()
  this.cateList = res.message
  // 为二级分类赋值
  this.cateLevel2 = res.message[0].children
}

4.3 Modification  activeChanged method: After the selected items of the first-level classification are changed, reassign the second-level classification list data:

activeChanged(i) {
  this.active = i
  // 为二级分类列表重新赋值
  this.cateLevel2 = this.cateList[i].children
}

4.4 Loop to render the UI structure of the secondary classification list on the right:

<!-- 右侧的滚动视图区域 -->
<scroll-view class="right-scroll-view" scroll-y :style="{height: wh + 'px'}">
  <view class="cate-lv2" v-for="(item2, i2) in cateLevel2" :key="i2">
    <view class="cate-lv2-title">/ {
   
   {item2.cat_name}} /</view>
  </view>
</scroll-view>

4.5 Beautify the title style of the secondary classification:

.cate-lv2-title {
  font-size: 12px;
  font-weight: bold;
  text-align: center;
  padding: 15px 0;
}

5. Render three-level classification structure

5.1 In the second-level classification  <view> component, loop to render the list structure of the third-level classification:

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

5.2 Beautify the style of the three-level classification:

.cate-lv3-list {
  display: flex;
  flex-wrap: wrap;

  .cate-lv3-item {
    width: 33.33%;
    margin-bottom: 10px;
    display: flex;
    flex-direction: column;
    align-items: center;

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

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

6. Reset the position of the scroll bar after switching the first-level category

6.1 Defined in data  滚动条距离顶部的距离:

data() {
  return {
    // 滚动条距离顶部的距离
    scrollTop: 0
  }
}

6.2 Dynamically  <scroll-view> bind  scroll-top the value of the property to the component on the right:

<!-- 右侧的滚动视图区域 -->
<scroll-view class="right-scroll-view" scroll-y :style="{height: wh + 'px'}" :scroll-top="scrollTop"></scroll-view>

6. 3 When switching the first-level classification, the dynamically set  scrollTop value: Note that when switching here, it cannot be switched to the same value as the original one.

// 选中项改变的事件处理函数
activeChanged(i) {
  this.active = i
  this.cateLevel2 = this.cateList[i].children

  // 让 scrollTop 的值在 0 与 1 之间切换
  this.scrollTop = this.scrollTop === 0 ? 1 : 0

  // 可以简化为如下的代码:
  // this.scrollTop = this.scrollTop ? 0 : 1
}

7. Click on the third-level classification to jump to the product list page

7.1 Bind the click event processing function to the three-level Item items as follows:

<view class="cate-lv3-item" v-for="(item3, i3) in item2.children" :key="i3" @click="gotoGoodsList(item3)">
  <image :src="item3.cat_icon"></image>
  <text>{
   
   {item3.cat_name}}</text>
</view>

7.2 Define the event processing function as follows:

// 点击三级分类项跳转到商品列表页面
gotoGoodsList(item3) {
  uni.navigateTo({
    url: '/subpkg/goods_list/goods_list?cid=' + item3.cat_id
  })
}

Realization renderings:

 5. Search function

1. Create a new custom component

Create a new components folder in the project directory, and then create a new custom component

 In the UI structure of the category page, use  my-search custom components directly in the form of labels:

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

The UI structure of the defined  my-search component is as follows:

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

Beautify the style of the custom search component:

.my-search-container {
  background-color: #c00000;
  height: 50px;
  padding: 0 10px;
  display: flex;
  align-items: center;
}

.my-search-box {
  height: 36px;
  background-color: #ffffff;
  border-radius: 15px;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;

  .placeholder {
    font-size: 15px;
    margin-left: 5px;
  }
}

Since the custom  my-search component height is  50px, therefore, the available height of the category page window needs to be recalculated:

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

 2. Enhance the versatility of components through custom attributes

In order to enhance the versatility of the component, we allow users to customize  背景颜色 the sum  of the search components 圆角尺寸.

By  props defining  bgcolor and  radius two properties, and specifying the value type and property default value:

props: {
  // 背景颜色
  bgcolor: {
    type: String,
    default: '#C00000'
  },
  // 圆角尺寸
  radius: {
    type: Number,
    // 单位是 px
    default: 18
  }
}

.my-search-container Dynamically bind attributes to boxes and  .my-search-box boxes  through  attribute binding style :

<view class="my-search-container" :style="{'background-color': bgcolor}">
  <view class="my-search-box" :style="{'border-radius': radius + 'px'}">
    <uni-icons type="search" size="17"></uni-icons>
    <text class="placeholder">搜索</text>
  </view>
</view>

Remove   the sum  scss from the corresponding style  :背景颜色圆角尺寸

.my-search-container {
  // 移除背景颜色,改由 props 属性控制
  // background-color: #C00000;
  height: 50px;
  padding: 0 10px;
  display: flex;
  align-items: center;
}

.my-search-box {
  height: 36px;
  background-color: #ffffff;
  // 移除圆角尺寸,改由 props 属性控制
  // border-radius: 15px;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;

  .placeholder {
    font-size: 15px;
    margin-left: 5px;
  }
}

This way you can pass parameters when using the component

<my-search :bgcolor="'pink'" :radius="3"></my-search>

3. Encapsulate click events for custom components

3.1  my-search Inside the custom component, give  the  bound   event handler the class .my-search-box name  :viewclick

<view class="my-search-box" :style="{'border-radius': radius + 'px'}" @click="searchBoxHandler">
  <uni-icons type="search" size="17"></uni-icons>
  <text class="placeholder">搜索</text>
</view>

3.2 In  the node my-search of the custom component  methods , declare the event processing function as follows:

methods: {
  // 点击了模拟的 input 输入框
  searchBoxHandler() {
    // 触发外界通过 @click 绑定的 click 事件处理函数
    this.$emit('click')
  }
}

my-search 3.3 When using a custom component  in a category page  , you can @click bind a click event handler to it: 

<my-search @click="gotoSearch"></my-search>

 3.4 At the same time, in the category page, define  gotoSearch the event processing function as follows:

methods: {
   // 跳转到分包中的搜索页面
   gotoSearch() {
     uni.navigateTo({
       url: '/subpkg/search/search'
     })
   }
}

 4. Realize the ceiling effect of the homepage search component

4.1 Define the following UI structure on the home page:

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

4.2 Define the following event processing function on the home page:

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

 4.3 Achieve the ceiling effect through the following styles:

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

5. Implementation of search suggestions 

5.1 Basic structure of rendering search page

Define the following UI structure:

<view class="search-box">
  <!-- 使用 uni-ui 提供的搜索组件 -->
  <uni-search-bar @input="input" :radius="100" cancelButton="none"></uni-search-bar>
</view>

To achieve the ceiling effect of the search box:

.search-box {
  position: sticky;
  top: 0;
  z-index: 999;
}

Define the input event handler function as follows:

methods: {
  input(e) {
    // e 是最新的搜索内容
    console.log(e)
  }
}

 5.2 Implement automatic focus acquisition

 5.3 Implement anti-shake processing

Define the anti-shake delay timerId in data as follows:

data() {
  return {
    // 延时器的 timerId
    timer: null,
    // 搜索关键词
    kw: ''
  }
}

Modify  input the event processing function as follows: use timer

input(e) {
  // 清除 timer 对应的延时器
  clearTimeout(this.timer)
  // 重新启动一个延时器,并把 timerId 赋值给 this.timer
  this.timer = setTimeout(() => {
    // 如果 500 毫秒内,没有触发新的输入事件,则为搜索关键词赋值
    this.kw = e.value
    console.log(this.kw)
  }, 500)
}

5.4 Search suggestion list based on keyword query

Define the following data nodes in data to store list data of search suggestions:

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

In anti-shake  setTimeout , call  getSearchList the method to get the list of search suggestions:

this.timer = setTimeout(() => {
  this.kw = e.value
  // 根据关键词,查询搜索建议列表
  this.getSearchList()
}, 500)

methods The method defined  in  getSearchList is as follows: 

// 根据搜索关键词,搜索商品建议列表
async getSearchList() {
  // 判断关键词是否为空
  if (this.kw === '') {
    this.searchResults = []
    return
  }
  // 发起请求,获取搜索建议列表
  const { data: res } = await uni.$http.get('/api/public/v1/goods/qsearch', { query: this.kw })
  if (res.meta.status !== 200) return uni.$showMsg()
  this.searchResults = res.message
}

 5.5 Rendering the search suggestion list

Define the following UI structure:

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

 Beautify search suggestion list:

.sugg-list {
  padding: 0 5px;

  .sugg-item {
    font-size: 12px;
    padding: 13px 0;
    border-bottom: 1px solid #efefef;
    display: flex;
    align-items: center;
    justify-content: space-between;

    .goods-name {
      // 文字不允许换行(单行文本)
      white-space: nowrap;
      // 溢出部分隐藏
      overflow: hidden;
      // 文本溢出后,使用 ... 代替
      text-overflow: ellipsis;
      margin-right: 3px;
    }
  }
}

Click the search suggested Item to jump to the product details page:

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

 6. Search history

6.1 Basic structure of rendering search history

Define search history in data 假数据:

data() {
  return {
    // 搜索关键词的历史记录
    historyList: ['a', 'app', 'apple']
  }
}

Render the UI structure of the search history area:

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

Beautify the style of the search history area:

.history-box {
  padding: 0 5px;

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

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

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

Implement on-demand display of search suggestions and search history

<!-- 搜索建议列表 -->
<view class="sugg-list" v-if="searchResults.length !== 0">
  <!-- 省略其它代码... -->
</view>

<!-- 搜索历史 -->
<view class="history-box" v-else>
  <!-- 省略其它代码... -->
</view>

 Store search keywords in historyList

methods: {
  // 根据搜索关键词,搜索商品建议列表
  async getSearchList() {
    // 省略其它不必要的代码...

    // 1. 查询到搜索建议之后,调用 saveSearchHistory() 方法保存搜索关键词
    this.saveSearchHistory()
  },
  // 2. 保存搜索关键词的方法
  saveSearchHistory() {
    // 2.1 直接把搜索关键词 push 到 historyList 数组中
    this.historyList.push(this.kw)
  }
}

6.2 Solve the problem of keyword order

  1. Do not make any modifications in data  historyList , still use push to append at the end

  2. Define a calculated property  historys.  After inverting historyList the array  reverse , the value of the calculated property is:

computed: {
  historys() {
    // 注意:由于数组是引用类型,所以不要直接基于原数组调用 reverse 方法,以免修改原数组中元素的顺序
    // 而是应该新建一个内存无关的数组,再进行 reverse 反转
    return [...this.historyList].reverse()
  }
}

historyListWhen rendering search keywords on the page, calculated attributes  are no longer used in data  historys

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

6.3 Solve the problem of repeated keywords

The modification  saveSearchHistory method is as follows:

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

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

 6.4 Persistent storage of search history locally

The modification  saveSearchHistory method is as follows:

// 保存搜索关键词为历史记录
saveSearchHistory() {
  const set = new Set(this.historyList)
  set.delete(this.kw)
  set.add(this.kw)
  this.historyList = Array.from(set)
  // 调用 uni.setStorageSync(key, value) 将搜索历史记录持久化存储到本地
  uni.setStorageSync('kw', JSON.stringify(this.historyList))
}

 In  onLoad the lifecycle function, load the locally stored search history:

onLoad() {
  this.historyList = JSON.parse(uni.getStorageSync('kw') || '[]')
}

6.5 Clear search history

Bind events to the cleared icon button  click : 

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

methods Define  the handler function in  cleanHistory :

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

 6.6 Click on the search history to jump to the product list page

click Bind an event handler function to the Item item in the search history  :

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

methods Define  the handler function  in  gotoGoodsList :

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

 Rendering:

 6. Product list

1. Define the request parameter object

data() {
  return {
    // 请求参数对象
    queryObj: {
      // 查询关键词
      query: '',
      // 商品分类Id
      cid: '',
      // 页码值
      pagenum: 1,
      // 每页显示多少条数据
      pagesize: 10
    }
  }
}

Dump  the parameters carried when the page jumps queryObj into the object:

onLoad(options) {
  // 将页面参数转存到 this.queryObj 对象中
  this.queryObj.query = options.query || ''
  this.queryObj.cid = options.cid || ''
}

 2. Get product list data

Add the following data nodes in data:

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

In  onLoad the life cycle function, call  getGoodsList the method to obtain the product list data:

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

 In  methods the node, declare  getGoodsList the method as follows:

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

3. Render product list structure 

On the page, through  v-for instructions, the UI structure of the product is rendered in a loop:

<template>
  <view>
    <view class="goods-list">
      <block v-for="(goods, i) in goodsList" :key="i">
        <view class="goods-item">
          <!-- 商品左侧图片区域 -->
          <view class="goods-item-left">
            <image :src="goods.goods_small_logo || defaultPic" class="goods-pic"></image>
          </view>
          <!-- 商品右侧信息区域 -->
          <view class="goods-item-right">
            <!-- 商品标题 -->
            <view class="goods-name">{
   
   {goods.goods_name}}</view>
            <view class="goods-info-box">
              <!-- 商品价格 -->
              <view class="goods-price">¥{
   
   {goods.goods_price}}</view>
            </view>
          </view>
        </view>
      </block>
    </view>
  </view>
</template>

In order to prevent images of certain products from not existing, a default image needs to be defined in data:

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

 And use it on demand when the page is rendering:

<image :src="goods.goods_small_logo || defaultPic" class="goods-pic"></image>

Beautify the UI structure of the product list:

.goods-item {
  display: flex;
  padding: 10px 5px;
  border-bottom: 1px solid #f0f0f0;

  .goods-item-left {
    margin-right: 5px;

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

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

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

    .goods-price {
      font-size: 16px;
      color: #c00000;
    }
  }
}

 4. Encapsulate product items into custom components

4.1 First create a new corresponding component in the directory

4.2  goods_list Encapsulate the UI structure, style, and data related to the product items on the page into  my-goods components:

<template>
  <view class="goods-item">
    <!-- 商品左侧图片区域 -->
    <view class="goods-item-left">
      <image :src="goods.goods_small_logo || defaultPic" class="goods-pic"></image>
    </view>
    <!-- 商品右侧信息区域 -->
    <view class="goods-item-right">
      <!-- 商品标题 -->
      <view class="goods-name">{
   
   {goods.goods_name}}</view>
      <view class="goods-info-box">
        <!-- 商品价格 -->
        <view class="goods-price">¥{
   
   {goods.goods_price}}</view>
      </view>
    </view>
  </view>
</template>

<script>
  export default {
    // 定义 props 属性,用来接收外界传递到当前组件的数据
    props: {
      // 商品的信息对象
      goods: {
        type: Object,
        defaul: {},
      },
    },
    data() {
      return {
        // 默认的空图片
        defaultPic: 'https://img3.doubanio.com/f/movie/8dd0c794499fe925ae2ae89ee30cd225750457b4/pics/movie/celebrity-default-medium.png',
      }
    },
  }
</script>

<style lang="scss">
  .goods-item {
    display: flex;
    padding: 10px 5px;
    border-bottom: 1px solid #f0f0f0;

    .goods-item-left {
      margin-right: 5px;

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

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

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

      .goods-price {
        font-size: 16px;
        color: #c00000;
      }
    }
  }
</style>

4.3 In  goods_list the component, just render  my-goods the component in a loop:

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

 5. Use filters to process prices

In  my-goods the component, at  data the same level as the node, declare  filters the filter node as follows:

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

When rendering product prices,  | call the filter through the pipe character:

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

 6. Pull up to load more

pages.json Open the configuration file  in the project root directory  and  configure the pull-to-bottom distance for the pages subPackages in the subpackage  :goods_list

 In  goods_list the page, at  methods the same level as the node, declare  onReachBottom an event handler function to monitor the pull-up and bottom-out behavior of the page:

// 触底的事件
onReachBottom() {
  // 让页码值自增 +1
  this.queryObj.pagenum += 1
  // 重新获取列表数据
  this.getGoodsList()
}

methods The function under   transformation  getGoodsList , after the list data request is successful, will splice the old and new data:

// 获取商品列表数据的方法
async getGoodsList() {
  // 发起请求
  const { data: res } = await uni.$http.get('/api/public/v1/goods/search', this.queryObj)
  if (res.meta.status !== 200) return uni.$showMsg()

  // 为数据赋值:通过展开运算符的形式,进行新旧数据的拼接
  this.goodsList = [...this.goodsList, ...res.message.goods]
  this.total = res.message.total
}

7. Prevent additional requests from being initiated through throttling 

Define the throttle valve in data  isloading as follows:

data() {
  return {
    // 是否正在请求数据
    isloading: false
  }
}

Modify  getGoodsList the method to open and close the throttle valve before and after requesting data:

// 获取商品列表数据的方法
async getGoodsList() {
  // ** 打开节流阀
  this.isloading = true
  // 发起请求
  const { data: res } = await uni.$http.get('/api/public/v1/goods/search', this.queryObj)
  // ** 关闭节流阀
  this.isloading = false

  // 省略其它代码...
}

 In  onReachBottom the bottoming event handling function, decide whether to initiate a request based on the status of the throttle valve:

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

  this.queryObj.pagenum += 1
  this.getGoodsList()
}

Determine whether the data has been loaded 

Modify  onReachBottom the event handling function as follows:

// 触底的事件
onReachBottom() {
  // 判断是否还有下一页数据
  if (this.queryObj.pagenum * this.queryObj.pagesize >= this.total) return uni.$showMsg('数据加载完毕!')

  // 判断是否正在请求其它数据,如果是,则不发起额外的请求
  if (this.isloading) return

  this.queryObj.pagenum += 1
  this.getGoodsList()
}

8. Pull down to refresh

In  the configuration file,  enable the pull-down refresh effect separately pages.json for the current  page:goods_list

onPullDownRefresh Listen to the event handler  of the page  :

// 下拉刷新的事件
onPullDownRefresh() {
  // 1. 重置关键数据
  this.queryObj.pagenum = 1
  this.total = 0
  this.isloading = false
  this.goodsList = []

  // 2. 重新发起请求
  this.getGoodsList(() => uni.stopPullDownRefresh())
}

Modify  getGoodsList the function to receive  cb a callback function and call it as needed:

// 获取商品列表数据的方法
async getGoodsList(cb) {
  this.isloading = true
  const { data: res } = await uni.$http.get('/api/public/v1/goods/search', this.queryObj)
  this.isloading = false
  // 只要数据请求完毕,就立即按需调用 cb 回调函数
  cb && cb()

  if (res.meta.status !== 200) return uni.$showMsg()
  this.goodsList = [...this.goodsList, ...res.message.goods]
  this.total = res.message.total
}

 9. Click on the product item to jump to the details page

Modify the component during the loop  block to  view a component and bind  click the click event handler:

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

In  methods the node, define  gotoDetail the event handler function:

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

Realization renderings:

 

7. Product details

1. Get product details data

1.1  data Define the data node of product details in:

data() {
  return {
    // 商品详情对象
    goods_info: {}
  }
}

1.2  onLoad Get the product ID in and call the method to request product details:

onLoad(options) {
  // 获取商品 Id
  const goods_id = options.goods_id
  // 调用请求商品详情数据的方法
  this.getGoodsDetail(goods_id)
}

 1.3  methods Declare  getGoodsDetail methods in:

methods: {
  // 定义请求商品详情数据的方法
  async getGoodsDetail(goods_id) {
    const { data: res } = await uni.$http.get('/api/public/v1/goods/detail', { goods_id })
    if (res.meta.status !== 200) return uni.$showMsg()
    // 为 data 中的数据赋值
    this.goods_info = res.message
  }
}

2. Render the product details page carousel area

2.1 Use  v-for instructions to render the following carousel UI structure in a loop:

<!-- 轮播图区域 -->
<swiper :indicator-dots="true" :autoplay="true" :interval="3000" :duration="1000" :circular="true">
  <swiper-item v-for="(item, i) in goods_info.pics" :key="i">
    <image :src="item.pics_big"></image>
  </swiper-item>
</swiper>

 2.2 Beautify the style of the carousel:

swiper {
  height: 750rpx;

  image {
    width: 100%;
    height: 100%;
  }
}

2.3 Implement carousel image preview effect

 Bind an event handler function image to the image  in the carousel  :click

<swiper-item v-for="(item, i) in goods_info.pics" :key="i">
  <!-- 把当前点击的图片的索引,传递到 preview() 处理函数中 -->
  <image :src="item.pics_big" @click="preview(i)"></image>
</swiper-item>

methods Define  the event handler function  in  preview :

// 实现轮播图的预览效果
preview(i) {
  // 调用 uni.previewImage() 方法预览图片
  uni.previewImage({
    // 预览时,默认显示图片的索引
    current: i,
    // 所有图片 url 地址的数组
    urls: this.goods_info.pics.map(x => x.pics_big)
  })
}

3. Render product information area 

3.1 Define the UI structure of the product information area as follows:

<!-- 商品信息区域 -->
<view class="goods-info-box">
  <!-- 商品价格 -->
  <view class="price">¥{
   
   {goods_info.goods_price}}</view>
  <!-- 信息主体区域 -->
  <view class="goods-info-body">
    <!-- 商品名称 -->
    <view class="goods-name">{
   
   {goods_info.goods_name}}</view>
    <!-- 收藏 -->
    <view class="favi">
      <uni-icons type="star" size="18" color="gray"></uni-icons>
      <text>收藏</text>
    </view>
  </view>
  <!-- 运费 -->
  <view class="yf">快递:免运费</view>
</view>

3.2 Beautify the style of the product information area:

// 商品信息区域的样式
.goods-info-box {
  padding: 10px;
  padding-right: 0;

  .price {
    color: #c00000;
    font-size: 18px;
    margin: 10px 0;
  }

  .goods-info-body {
    display: flex;
    justify-content: space-between;

    .goods-name {
      font-size: 13px;
      padding-right: 10px;
    }
    // 收藏区域
    .favi {
      width: 120px;
      font-size: 12px;
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      border-left: 1px solid #efefef;
      color: gray;
    }
  }

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

 4. Render product details information

4.1 In the page structure, use  rich-text components to render the content with HTML tags into the page structure of the mini program:

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

4.2 Modify  getGoodsDetail the method to solve  空白间隙 the problem at the bottom of the picture:

// 定义请求商品详情数据的方法
async getGoodsDetail(goods_id) {
  const { data: res } = await uni.$http.get('/api/public/v1/goods/detail', { goods_id })
  if (res.meta.status !== 200) return uni.$showMsg()

  // 使用字符串的 replace() 方法,为 img 标签添加行内的 style 样式,从而解决图片底部空白间隙的问题
  res.message.goods_introduce = res.message.goods_introduce.replace(/<img /g, '<img style="display:block;" ')
  this.goods_info = res.message
}

 4.3 Solve  the problem that .webp formatted pictures  ios cannot be displayed normally on the device:

// 定义请求商品详情数据的方法
async getGoodsDetail(goods_id) {
  const { data: res } = await uni.$http.get('/api/public/v1/goods/detail', { goods_id })
  if (res.meta.status !== 200) return uni.$showMsg()

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

5. Solve the problem of product price flashing 

  1. Cause of the problem: Before the product details data is requested back,  goods_info the value  in data is {}, so when the page is rendered for the first time, it will cause  商品价格、商品名称 flickering problems.

  2. Solution: Determine  goods_info.goods_name whether the value of the attribute exists, and then use  v-if instructions to control the display and hiding of the page:

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

 6. Render the UI structure of the product navigation area

Based on the GoodsNav component provided by uni-ui to realize the effect of product navigation area

6.1 In data,   declare the button configuration object of the product navigation component by options and  two arrays:buttonGroup

data() {
  return {
    // 商品详情对象
    goods_info: {},
    // 左侧按钮组的配置对象
    options: [{
      icon: 'shop',
      text: '店铺'
    }, {
      icon: 'cart',
      text: '购物车',
      info: 2
    }],
    // 右侧按钮组的配置对象
    buttonGroup: [{
        text: '加入购物车',
        backgroundColor: '#ff0000',
        color: '#fff'
      },
      {
        text: '立即购买',
        backgroundColor: '#ffa200',
        color: '#fff'
      }
    ]
  }
}

6.2 Use the product navigation component in the page  uni-goods-nav :

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

 6.3 Beautify the product navigation component and fix it at the bottom of the page:

.goods-detail-container {
  // 给页面外层的容器,添加 50px 的内padding,
  // 防止页面内容被底部的商品导航组件遮盖
  padding-bottom: 50px;
}

.goods_nav {
  // 为商品导航组件添加固定定位
  position: fixed;
  bottom: 0;
  left: 0;
  width: 100%;
}

7. Click to jump to the shopping cart page 

e.content.text Determine the jump page based on  the value of

// 左侧按钮的点击事件处理函数
onClick(e) {
  if (e.content.text === '购物车') {
    // 切换到购物车页面
    uni.switchTab({
      url: '/pages/cart/cart'
    })
  }
}

renderings

 

8. Add to cart 

1. Configure vuex

1.1 Create a folder in the project root directory  store specifically to store vuex-related modules

1.2  store Right-click the directory, select  新建 -> js文件, and create  a new store.js file:

 1.3  store.js Follow the following 4 steps to initialize the instance object of the Store in :

// 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.4  main.js Import  store the instance object in and mount it on the Vue instance:

// 1. 导入 store 的实例对象
import store from './store/store.js'

// 省略其它代码...

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

 2. Create the store module of the shopping cart

2.1  store Right-click on the directory, select  新建 -> js文件, create the store module of the shopping cart, and name it cart.js

2.2 In  cart.js , initialize the vuex module as follows:

export default {
  // 为当前模块开启命名空间
  namespaced: true,

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

  // 模块的 mutations 方法
  mutations: {},

  // 模块的 getters 属性
  getters: {},
}

2.3 In  store/store.js the module, import and mount the vuex module of the shopping cart. The sample code is as follows:

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

 3. Use the data in the Store in the product details page

3.1 On  goods_detail.vue the page, modify  <script></script> the code in the tag as follows:

// 从 vuex 中按需导出 mapState 辅助方法
import { mapState } from 'vuex'

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

3.2 When rendering the page, you can directly use the mapped data, for example: 

<!-- 运费 -->
<view class="yf">快递:免运费 -- {
   
   {cart.length}}</view>

4. Implement the function of adding to shopping cart 

 4.1 In the module under the store directory  cart.js , encapsulate a mutations method for adding product information to the shopping cart, named  addToCart. The sample code is as follows:

export default {
  // 为当前模块开启命名空间
  namespaced: true,

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

  // 模块的 mutations 方法
  mutations: {
    addToCart(state, goods) {
      // 根据提交的商品的Id,查询购物车中是否存在这件商品
      // 如果不存在,则 findResult 为 undefined;否则,为查找到的商品信息对象
      const findResult = state.cart.find((x) => x.goods_id === goods.goods_id)

      if (!findResult) {
        // 如果购物车中没有这件商品,则直接 push
        state.cart.push(goods)
      } else {
        // 如果购物车中有这件商品,则只更新数量即可
        findResult.goods_count++
      }
    },
  },

  // 模块的 getters 属性
  getters: {},
}

4.2 In the product details page, use  mapMutations this auxiliary method to  map the methods m_cart under the module  in vuex addToCart to the current page:

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

export default {
  methods: {
    // 把 m_cart 模块中的 addToCart 方法映射到当前页面使用
    ...mapMutations('m_cart', ['addToCart']),
  },
}

uni-goods-nav 4.3 Bind  the event handling function to the product navigation component  @buttonClick="buttonClick" :

// 右侧按钮的点击事件处理函数
buttonClick(e) {
   // 1. 判断是否点击了 加入购物车 按钮
   if (e.content.text === '加入购物车') {

      // 2. 组织一个商品的信息对象
      const goods = {
         goods_id: this.goods_info.goods_id,       // 商品的Id
         goods_name: this.goods_info.goods_name,   // 商品的名称
         goods_price: this.goods_info.goods_price, // 商品的价格
         goods_count: 1,                           // 商品的数量
         goods_small_logo: this.goods_info.goods_small_logo, // 商品的图片
         goods_state: true                         // 商品的勾选状态
      }

      // 3. 通过 this 调用映射过来的 addToCart 方法,把商品信息对象存储到购物车中
      this.addToCart(goods)

   }
}

 5. Dynamically count the total number of items in the shopping cart

5.1 In  cart.js the module,  getters define a  total method under the node to count the total number of items in the shopping cart:

// 模块的 getters 属性
getters: {
   // 统计购物车中商品的总数量
   total(state) {
      let c = 0
      // 循环统计商品的数量,累加到变量 c 中
      state.cart.forEach(goods => c += goods.goods_count)
      return c
   }
}

 5.2 In the label of the product details page  script , import  mapGetters the method as needed and use it:

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

export default {
  computed: {
    // 把 m_cart 模块中名称为 total 的 getter 映射到当前页面中使用
    ...mapGetters('m_cart', ['total']),
  },
}

5.3 Use  watch the listener to monitor  total changes in calculated attribute values ​​and dynamically assign values ​​to the logo of the shopping cart button :

export default {
  watch: {
    // 1. 监听 total 值的变化,通过第一个形参得到变化后的新值
    total(newVal) {
      // 2. 通过数组的 find() 方法,找到购物车按钮的配置对象
      const findResult = this.options.find((x) => x.text === '购物车')

      if (findResult) {
        // 3. 动态为购物车按钮的 info 属性赋值
        findResult.info = newVal
      }
    },
  },
}

 6. Persistent storage of items in the shopping cart

6.1 In  cart.js the module, declare a  saveToStorage mutation method called, which is responsible for persisting the data in the shopping cart locally:

// 将购物车中的数据持久化存储到本地
saveToStorage(state) {
   uni.setStorageSync('cart', JSON.stringify(state.cart))
}

6.2 Modify  the method mutations in the node  addToCart . After processing the product information, call  saveToStorage the method defined in step 1:

addToCart(state, goods) {
   // 根据提交的商品的Id,查询购物车中是否存在这件商品
   // 如果不存在,则 findResult 为 undefined;否则,为查找到的商品信息对象
   const findResult = state.cart.find(x => x.goods_id === goods.goods_id)

   if (!findResult) {
      // 如果购物车中没有这件商品,则直接 push
      state.cart.push(goods)
   } else {
      // 如果购物车中有这件商品,则只更新数量即可
      findResult.goods_count++
   }

   // 通过 commit 方法,调用 m_cart 命名空间下的 saveToStorage 方法
   this.commit('m_cart/saveToStorage')
}

 6.3 Modify  the function cart.js in the module  state , read the locally stored shopping cart data, and initialize the cart array:

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

7. Optimize the total listener of the product details page 

Watch listeners defined in the form of ordinary functions will not be called after the page is first loaded . As a result, after the product details page is loaded for the first time, the total quantity of the product will not be displayed in the product navigation area: in order to prevent the above problem, you can use the object form to define the watch listener

watch: {
   // 定义 total 侦听器,指向一个配置对象
   total: {
      // handler 属性用来定义侦听器的 function 处理函数
      handler(newVal) {
         const findResult = this.options.find(x => x.text === '购物车')
         if (findResult) {
            findResult.info = newVal
         }
      },
      // immediate 属性用来声明此侦听器,是否在页面初次加载完毕后立即调用
      immediate: true
   }
}

8. Dynamically set a digital logo for the tabBar page 

8.1 Map total in Store  cart.vue to use:

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

export default {
  data() {
    return {}
  },
  computed: {
    // 将 m_cart 模块中的 total 映射为当前页面的计算属性
    ...mapGetters('m_cart', ['total']),
  },
}

8.2 When the page is first displayed, call  setBadge the method immediately to set the digital logo for the tabBar:

onShow() {
   // 在页面刚展示的时候,设置数字徽标
   this.setBadge()
}

 8.3 In  methods the node, declare  setBadge the method as follows, by  uni.setTabBarBadge() setting the digital logo for the tabBar:

methods: {
   setBadge() {
      // 调用 uni.setTabBarBadge() 方法,为购物车设置右上角的徽标
      uni.setTabBarBadge({
         index: 2, // 索引
         text: this.total + '' // 注意:text 的值必须是字符串,不能是数字
      })
   }
}

9. Extract the code that sets the tabBar logo into mixins 

9.1 Create a new folder in the project root directory  mixins and  create a mixins new file under the folder  tabbar-badge.js to encapsulate the code for setting the tabBar logo into a mixin file:

import { mapGetters } from 'vuex'

// 导出一个 mixin 对象
export default {
  computed: {
    ...mapGetters('m_cart', ['total']),
  },
  onShow() {
    // 在页面刚展示的时候,设置数字徽标
    this.setBadge()
  },
  methods: {
    setBadge() {
      // 调用 uni.setTabBarBadge() 方法,为购物车设置右上角的徽标
      uni.setTabBarBadge({
        index: 2,
        text: this.total + '', // 注意:text 的值必须是字符串,不能是数字
      })
    },
  },
}

9.2 Modify  home.vue, cate.vue, cart.vue, my.vue the source code of these four tabBar pages, import  @/mixins/tabbar-badge.js the modules respectively and use them: 

// 导入自己封装的 mixin 模块
import badgeMix from '@/mixins/tabbar-badge.js'

export default {
  // 将 badgeMix 混入到当前的页面中进行使用
  mixins: [badgeMix],
  // 省略其它代码...
}

9. Shopping cart page 

Product list area

1. Render the title area of ​​the shopping cart product list

1.1 Define the following UI structure:

<!-- 购物车商品列表的标题区域 -->
<view class="cart-title">
  <!-- 左侧的图标 -->
  <uni-icons type="shop" size="18"></uni-icons>
  <!-- 描述文本 -->
  <text class="cart-title-text">购物车</text>
</view>

1.2 Beautification style:

.cart-title {
  height: 40px;
  display: flex;
  align-items: center;
  font-size: 14px;
  padding-left: 5px;
  border-bottom: 1px solid #efefef;
  .cart-title-text {
    margin-left: 10px;
  }
}

 2. Render the basic structure of the product list area

2.1  Use mapState the auxiliary function to map the array in the Store  cart to the current page:

import badgeMix from '@/mixins/tabbar-badge.js'
// 按需导入 mapState 这个辅助函数
import { mapState } from 'vuex'

export default {
  mixins: [badgeMix],
  computed: {
    // 将 m_cart 模块中的 cart 数组映射到当前页面中使用
    ...mapState('m_cart', ['cart']),
  },
  data() {
    return {}
  },
}

2.2 In the UI structure,  v-for render customized  my-goods components through instruction loops:

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

 3. Encapsulate the radio check status for the my-goods component

3.1 Open  my-goods.vue the source code of the component and add the component to the left image area of ​​the product  radio :

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

3.2 Add a style to  the component goods-item-left with  the class name view to realize  the left and right layout of radio the components and  image components:

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

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

 3.3 Encapsulate the attribute named  showRadio to  props control whether the radio component is displayed in the current component:

export default {
  // 定义 props 属性,用来接收外界传递到当前组件的数据
  props: {
    // 商品的信息对象
    goods: {
      type: Object,
      default: {},
    },
    // 是否展示图片左侧的 radio
    showRadio: {
      type: Boolean,
      // 如果外界没有指定 show-radio 属性的值,则默认不展示 radio 组件
      default: false,
    },
  },
}

3.4 Use  v-if commands to control  radio the on-demand display of components: 

<!-- 商品左侧图片区域 -->
<view class="goods-item-left">
  <!-- 使用 v-if 指令控制 radio 组件的显示与隐藏 -->
  <radio checked color="#C00000" v-if="showRadio"></radio>
  <image :src="goods.goods_small_logo || defaultPic" class="goods-pic"></image>
</view>

3.5 In  cart.vue the product list area of ​​the page, specify  :show-radio="true" attributes to display the radio component: 

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

3.6 Modify  my-goods.vue the component and dynamically  radio select it for binding:

<!-- 商品左侧图片区域 -->
<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. Encapsulate the radio-change event for the my-goods component

4.1 When the user clicks on the radio component and wants to modify the check status of the current product , the user can  my-goods bind  @radio-change an event to the component to obtain  goods_id the sum  of the current product goods_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>

 4.2 Define  radioChangeHandler the event processing function as follows:

methods: {
  // 商品的勾选状态发生了变化
  radioChangeHandler(e) {
    console.log(e) // 输出得到的数据 -> {goods_id: 395, goods_state: false}
  }
}

4.3 In  my-goods.vue the component,  radio bind  @click the event handling function to the component as follows:

<!-- 商品左侧图片区域 -->
<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>

 4.4 In  my-goods.vue the methods node of the component, define  radioClickHandler the event processing function:

methods: {
  // radio 组件的点击事件处理函数
  radioClickHandler() {
    // 通过 this.$emit() 触发外界通过 @ 绑定的 radio-change 事件,
    // 同时把商品的 Id 和 勾选状态 作为参数传递给 radio-change 事件处理函数
    this.$emit('radio-change', {
      // 商品的 Id
      goods_id: this.goods.goods_id,
      // 商品最新的勾选状态
      goods_state: !this.goods.goods_state
    })
  }
}

5. Modify the check status of the items in the shopping cart 

5.1 In  store/cart.js the module, declare the following  mutations method to modify the check status of the corresponding product:

// 更新购物车中商品的勾选状态
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')
  }
}

5.2 In  cart.vue the page, import  mapMutations this auxiliary function to map the required mutations methods to the current page for use:

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. Encapsulate NumberBox for my-goods component

6.1 Modify  my-goods.vue the source code of the component and  render  the basic structure of the component goods-info-box inside the view component with  the class nameNumberBox

<view class="goods-info-box">
  <!-- 商品价格 -->
  <view class="goods-price">¥{
   
   {goods.goods_price | tofixed}}</view>
  <!-- 商品数量 -->
  <uni-number-box :min="1"></uni-number-box>
</view>

6.2 Beautify the structure of the page:

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

 6.3 In  my-goods.vue the component, dynamically  NumberBox bind the quantity value of the product to the component

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

6.4 In  my-goods.vue the component, encapsulate the attribute named  showNum to  props control whether  NumberBox the component is displayed in the current component:

export default {
  // 定义 props 属性,用来接收外界传递到当前组件的数据
  props: {
    // 商品的信息对象
    goods: {
      type: Object,
      defaul: {},
    },
    // 是否展示图片左侧的 radio
    showRadio: {
      type: Boolean,
      // 如果外界没有指定 show-radio 属性的值,则默认不展示 radio 组件
      default: false,
    },
    // 是否展示价格右侧的 NumberBox 组件
    showNum: {
      type: Boolean,
      default: false,
    },
  },
}

 6.5 In  my-goods.vue the component, use  v-if instructions to control  NumberBox the on-demand display of the component:

<view class="goods-info-box">
  <!-- 商品价格 -->
  <view class="goods-price">¥{
   
   {goods.goods_price | tofixed}}</view>
  <!-- 商品数量 -->
  <uni-number-box :min="1" :value="goods.goods_count" @change="numChangeHandler" v-if="showNum"></uni-number-box>
</view>

6.6 In  cart.vue the product list area of ​​the page, specify  :show-num="true" attributes to display  NumberBox the component:

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

 7. Encapsulate the num-change event for the my-goods component

7.1 When the user modifies  NumberBox the value of , and hopes to update the latest product quantity to the shopping cart, the user can  my-goods bind  @num-change events to the component to obtain  goods_id the sum  of the current products.goods_count:

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

7.2 Define  numberChangeHandler the event processing function as follows:

// 商品的数量发生了变化
numberChangeHandler(e) {
  console.log(e)
}

 7.3 In  my-goods.vue the component,  uni-number-box bind  @change the event handler function to the component as follows:

<view class="goods-info-box">
  <!-- 商品价格 -->
  <view class="goods-price">¥{
   
   {goods.goods_price | tofixed}}</view>
  <!-- 商品数量 -->
  <uni-number-box :min="1" :value="goods.goods_count" @change="numChangeHandler"></uni-number-box>
</view>

7.4 In  the node my-goods.vue of the component  methods , define  numChangeHandler the event handling function:

methods: {
  // NumberBox 组件的 change 事件处理函数
  numChangeHandler(val) {
    // 通过 this.$emit() 触发外界通过 @ 绑定的 num-change 事件
    this.$emit('num-change', {
      // 商品的 Id
      goods_id: this.goods.goods_id,
      // 商品的最新数量
      goods_count: +val
    })
  }
}

The official component has been optimized in version 1.1.2, and the illegal verification is already included, so there is no need to perform legal verification.

8. Modify the quantity of items in the shopping cart

8.1 In  store/cart.js the module, declare the following mutations method to modify the quantity of the corresponding product:

// 更新购物车中商品的数量
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')
  }
}

8.2 In  cart.vue the page,  use mapMutations this auxiliary function to map the required  mutations methods to the current page:

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

9. Render the UI effect of sliding deletion 

9.1 Modify  cart.vue the UI structure of the page and modify the structure of the product list area as follows (you can use  the uSwipeAction  code block to quickly generate the basic UI structure):

<!-- 商品列表区域 -->
<!-- uni-swipe-action 是最外层包裹性质的容器 -->
<uni-swipe-action>
  <block v-for="(goods, i) in cart" :key="i">
    <!-- uni-swipe-action-item 可以为其子节点提供滑动操作的效果。需要通过 options 属性来指定操作按钮的配置信息 -->
    <uni-swipe-action-item :options="options" @click="swipeActionClickHandler(goods)">
      <my-goods :goods="goods" :show-radio="true" :show-num="true" @radio-change="radioChangeHandler" @num-change="numberChangeHandler"></my-goods>
    </uni-swipe-action-item>
  </block>
</uni-swipe-action>

9.2 Declare an array in the data node  options to define the configuration information of the operation button: 

data() {
  return {
    options: [{
      text: '删除', // 显示的文本内容
      style: {
        backgroundColor: '#C00000' // 按钮的背景颜色
      }
    }]
  }
}

9.3  Declare  the event handling function  of the component  methods in  :uni-swipe-action-item@click

// 点击了滑动操作按钮
swipeActionClickHandler(goods) {
  console.log(goods)
}

 10. Implement sliding delete function

10.1   Declare the following method in the node store/cart.js of the module  to remove the corresponding product from the shopping cart based on the product's ID:mutations

// 根据 Id 从购物车中删除对应的商品信息
removeGoodsById(state, goods_id) {
  // 调用数组的 filter 方法进行过滤
  state.cart = state.cart.filter(x => x.goods_id !== goods_id)
  // 持久化存储到本地
  this.commit('m_cart/saveToStorage')
}

10.2 In  cart.vue the page, use  mapMutations auxiliary functions to map the required methods to the current page:

methods: {
  ...mapMutations('m_cart', ['updateGoodsState', 'updateGoodsCount', 'removeGoodsById']),
  // 商品的勾选状态发生了变化
  radioChangeHandler(e) {
    this.updateGoodsState(e)
  },
  // 商品的数量发生了变化
  numberChangeHandler(e) {
    this.updateGoodsCount(e)
  },
  // 点击了滑动操作按钮
  swipeActionClickHandler(goods) {
    this.removeGoodsById(goods.goods_id)
  }
}

Shipping address area 

1. Create a component for the shipping address

2. Render the basic structure of the shipping address component:

<view>

  <!-- 选择收货地址的盒子 -->
  <view class="address-choose-box">
    <button type="primary" size="mini" class="btnChooseAddress">请选择收货地址+</button>
  </view>

  <!-- 渲染收货信息的盒子 -->
  <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>

  <!-- 底部的边框线 -->
  <image src="/static/[email protected]" class="address-border"></image>
</view>

// 底部边框线的样式
.address-border {
  display: block;
  width: 100%;
  height: 5px;
}

// 选择收货地址的盒子
.address-choose-box {
  height: 90px;
  display: flex;
  align-items: center;
  justify-content: center;
}

// 渲染收货信息的盒子
.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-right {
      display: flex;
      align-items: center;

      .phone {
        margin-right: 5px;
      }
    }
  }

  // 第二行
  .row2 {
    display: flex;
    align-items: center;
    margin-top: 10px;

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

3. Achieve on-demand display of the delivery address area

3.1 Define the information object of the shipping address in data: 

export default {
  data() {
    return {
      // 收货地址
      address: {},
    }
  },
}

3.2 Use  v-if and  v-else implement on-demand display: 

<!-- 选择收货地址的盒子 -->
<view class="address-choose-box" v-if="JSON.stringify(address) === '{}'">
  <button type="primary" size="mini" class="btnChooseAddress">请选择收货地址+</button>
</view>

<!-- 渲染收货信息的盒子 -->
<view class="address-info-box" v-else>
  <!-- 省略其它代码 -->
</view>

4. Implement the function of selecting the delivery address 

4.1   Bind the click event handler function to the button 请选择收货地址+ : button

<!-- 选择收货地址的盒子 -->
<view class="address-choose-box" v-if="JSON.stringify(address) === '{}'">
  <button type="primary" size="mini" class="btnChooseAddress" @click="chooseAddress">请选择收货地址+</button>
</view>

4.2 Define  chooseAddress the event processing function and call the API provided by the mini program  chooseAddress() to implement the function of selecting the delivery address:

methods: {
  // 选择收货地址
  async chooseAddress() {
    // 1. 调用小程序提供的 chooseAddress() 方法,即可使用选择收货地址的功能
    //    返回值是一个数组:第 1 项为错误对象;第 2 项为成功之后的收货地址对象
    const [err, succ] = await uni.chooseAddress().catch(err => err)

    // 2. 用户成功的选择了收货地址
    if (err === null && succ.errMsg === 'chooseAddress:ok') {
      // 为 data 里面的收货地址对象赋值
      this.address = succ
    }
  }
}

4.3 Define the calculated attributes of the detailed delivery address :

computed: {
  // 收货详细地址的计算属性
  addstr() {
    if (!this.address.provinceName) return ''

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

 4.4 Render data in the delivery address area:

<!-- 渲染收货信息的盒子 -->
<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>

 5. Store address information in vuex

5.1 In  store the directory, create a user-related  vuex module and name it  user.js:

export default {
  // 开启命名空间
  namespaced: true,

  // state 数据
  state: () => ({
    // 收货地址
    address: {},
  }),

  // 方法
  mutations: {
    // 更新收货地址
    updateAddress(state, address) {
      state.address = address
    },
  },

  // 数据包装器
  getters: {},
}

5.2 In  store/store.js the module, import and mount  user.js the module:

// 1. 导入 Vue 和 Vuex
import Vue from 'vue'
import Vuex from 'vuex'
// 导入购物车的 vuex 模块
import moduleCart from './cart.js'
// 导入用户的 vuex 模块
import moduleUser from './user.js'

// 2. 将 Vuex 安装为 Vue 的插件
Vue.use(Vuex)

// 3. 创建 Store 的实例对象
const store = new Vuex.Store({
  // TODO:挂载 store 模块
  modules: {
    // 挂载购物车的 vuex 模块,模块内成员的访问路径被调整为 m_cart,例如:
    // 购物车模块中 cart 数组的访问路径是 m_cart/cart
    m_cart: moduleCart,
    // 挂载用户的 vuex 模块,访问路径为 m_user
    m_user: moduleUser,
  },
})

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

 5.3 Transform  address.vue the code in the component and use  the address computed property provided by vuex  to replace  the local address object defined in data :

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

6. Persistently store the address in the Store locally 

6.1 Persistently store the address in the Store locally

export default {
  // 开启命名空间
  namespaced: true,

  // state 数据
  state: () => ({
    // 3. 读取本地的收货地址数据,初始化 address 对象
    address: JSON.parse(uni.getStorageSync('address') || '{}'),
  }),

  // 方法
  mutations: {
    // 更新收货地址
    updateAddress(state, address) {
      state.address = address

      // 2. 通过 this.commit() 方法,调用 m_user 模块下的 saveAddressToStorage 方法将 address 对象持久化存储到本地
      this.commit('m_user/saveAddressToStorage')
    },
    // 1. 定义将 address 持久化存储到本地 mutations 方法
    saveAddressToStorage(state) {
      uni.setStorageSync('address', JSON.stringify(state.address))
    },
  },

  // 数据包装器
  getters: {},
}

7. Extract addstr into getters 

Purpose: In order to improve the reusability of the code, the detailed address of the delivery can be extracted into getters to facilitate reuse between multiple pages and components.

7.1 Cut   the code of the calculated property  my-address.vue in the component  and paste it  into the module as a getters node:addstruser.js

// 数据包装器
getters: {
  // 收货详细地址的计算属性
  addstr(state) {
    if (!state.address.provinceName) return ''

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

7.2 Transform  my-address.vue the code in the component and  use mapGetters the auxiliary function to  map the code m_user in the module  addstr to the current component:

// 按需导入 mapGetters 辅助函数
import { mapState, mapMutations, mapGetters } from 'vuex'

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

 8.Reselect the delivery address

address-info-box 8.1 Bind  the event handler function to the box with class name  click as follows:

<!-- 渲染收货信息的盒子 -->
<view class="address-info-box" v-else @click="chooseAddress">
  <!-- 省略其它代码 -->
</view>

settlement area 

1. First create a new settlement component

2. Initialize  my-settle the basic structure and style of the component:

<template>
  <!-- 最外层的容器 -->
  <view class="my-settle-container">
    结算组件
  </view>
</template>

<script>
export default {
  data() {
    return {}
  },
}
</script>

<style lang="scss">
.my-settle-container {
  /* 底部固定定位 */
  position: fixed;
  bottom: 0;
  left: 0;
  /* 设置宽高和背景色 */
  width: 100%;
  height: 50px;
  background-color: cyan;
}
</style>

cart.vue Use custom components in the page  and  my-settle beautify the page style to prevent the bottom of the page from being covered:

<template>
  <view class="cart-container">
    <!-- 使用自定义的 address 组件 -->

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

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

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

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

 3. Render the structure and style of the settlement area

3.1 Define the following UI structure:

<!-- 最外层的容器 -->
<view class="my-settle-container">
  <!-- 全选区域 -->
  <label class="radio">
    <radio color="#C00000" :checked="true" /><text>全选</text>
  </label>

  <!-- 合计区域 -->
  <view class="amount-box">
    合计:<text class="amount">¥1234.00</text>
  </view>

  <!-- 结算按钮 -->
  <view class="btn-settle">结算(0)</view>
</view>

3.2 Beautification style:

.my-settle-container {
  position: fixed;
  bottom: 0;
  left: 0;
  width: 100%;
  height: 50px;
  // 将背景色从 cyan 改为 white
  background-color: white;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding-left: 5px;
  font-size: 14px;

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

  .amount {
    color: #c00000;
  }

  .btn-settle {
    height: 50px;
    min-width: 100px;
    background-color: #c00000;
    color: white;
    line-height: 50px;
    text-align: center;
    padding: 0 10px;
  }
}

4. Dynamically render the total quantity of checked products

4.1 In  store/cart.js the module, define a  checkedCount getters named to count the total quantity of selected products:

// 勾选的商品的总数量
checkedCount(state) {
  // 先使用 filter 方法,从购物车中过滤器已勾选的商品
  // 再使用 reduce 方法,将已勾选的商品总数量进行累加
  // reduce() 的返回值就是已勾选的商品的总数量
  return state.cart.filter(x => x.goods_state).reduce((total, item) => total += item.goods_count, 0)
}

4.2 In  my-settle the component,  mapGetters map the required getters to the current component through the auxiliary function:

import { mapGetters } from 'vuex'

export default {
  computed: {
    ...mapGetters('m_cart', ['checkedCount']),
  },
  data() {
    return {}
  },
}

 4.3  checkedCount Render the value to the page:

<!-- 结算按钮 -->
<view class="btn-settle">结算({
   
   {checkedCount}})</view>

5. Dynamically render the selected state of the select all button 

5.1 Use  mapGetters the auxiliary function to map the total quantity of the product to the current component and define a  isFullCheck calculated attribute called:

import { mapGetters } from 'vuex'

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

5.2 Dynamically bind  checked attribute values ​​for radio components:

<!-- 全选区域 -->
<label class="radio">
  <radio color="#C00000" :checked="isFullCheck" /><text>全选</text>
</label>

 5.3 Improve total

    total(state) {
            // let c = 0
            // state.cart.forEach(x => c += x.goods_count)
            // return c
            return state.cart.reduce((total,item)=>total += item.goods_count,0)
        },

6. Implement the function of selecting all/inverse selection of products

6.1 In  store/cart.js the module, define a  updateAllGoodsState mutation method called to modify the check status of all products:

// 更新所有商品的勾选状态
updateAllGoodsState(state, newState) {
  // 循环更新购物车中每件商品的勾选状态
  state.cart.forEach(x => x.goods_state = newState)
  // 持久化存储到本地
  this.commit('m_cart/saveToStorage')
}

6.2 In  my-settle the component,  mapMutations map the required mutations methods to the current component through the auxiliary function:

// 1. 按需导入 mapMutations 辅助函数
import { mapGetters, mapMutations } from 'vuex'

export default {
  // 省略其它代码
  methods: {
    // 2. 使用 mapMutations 辅助函数,把 m_cart 模块提供的 updateAllGoodsState 方法映射到当前组件中使用
    ...mapMutations('m_cart', ['updateAllGoodsState']),
  },
}

label 6.3 Bind  click event handling functions  to components in the UI  :

<!-- 全选区域 -->
<label class="radio" @click="changeAllState">
  <radio color="#C00000" :checked="isFullCheck" /><text>全选</text>
</label>

6.4 In  my-settle the methods node of the component, declare  changeAllState the event processing function:

methods: {
  ...mapMutations('m_cart', ['updateAllGoodsState']),
  // label 的点击事件处理函数
  changeAllState() {
    // 修改购物车中所有商品的选中状态
    // !this.isFullCheck 表示:当前全选按钮的状态取反之后,就是最新的勾选状态
    this.updateAllGoodsState(!this.isFullCheck)
  }
}

7 Dynamically render the total price of the selected items

7.1 In  store/cart.js the module, define a  checkedGoodsAmount getters called getters to count the total price of the selected products:

// 已勾选的商品的总价
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)
}

 7.2 In  my-settle the component, use  mapGetters the auxiliary function to map what is needed  checkedGoodsAmount to the current component:

...mapGetters('m_cart', ['total', 'checkedCount', 'checkedGoodsAmount'])

7.3 In the UI structure of the component, render the total price of the selected items:

<!-- 合计区域 -->
<view class="amount-box">
  合计:<text class="amount">¥{
   
   {checkedGoodsAmount}}</text>
</view>

 8. Dynamically calculate the value of the shopping cart logo

  1. Description of the problem : When the quantity of items in the shopping cart is modified, the digital logo on the tabBar does not automatically update.

  2. Solution : Renovate  mixins/tabbar-badge.js the code, use  watch a listener, listen for  total changes in the total number, and dynamically assign a value to the tabBar's logo:

import { mapGetters } from 'vuex'

// 导出一个 mixin 对象
export default {
  computed: {
    ...mapGetters('m_cart', ['total']),
  },
  watch: {
    // 监听 total 值的变化
    total() {
      // 调用 methods 中的 setBadge 方法,重新为 tabBar 的数字徽章赋值
      this.setBadge()
    },
  },
  onShow() {
    // 在页面刚展示的时候,设置数字徽标
    this.setBadge()
  },
  methods: {
    setBadge() {
      // 调用 uni.setTabBarBadge() 方法,为购物车设置右上角的徽标
      uni.setTabBarBadge({
        index: 2,
        text: this.total + '', // 注意:text 的值必须是字符串,不能是数字
      })
    },
  },
}

9. Render the page structure when the shopping cart is empty 

Modify  cart.vue the UI structure of the page and use  v-if and  v-else control the on-demand display of the shopping cart area and blank shopping cart area :

	<view class="cart-container">
		<view v-if="cart.length !== 0"> 
		<my-address></my-address>
			<!-- 购物车商品列表的标题区域 -->
			<view class="cart-title">
				<!-- 左侧的图标 -->
				<uni-icons type="shop" size="18"></uni-icons>
				<!-- 描述文本 -->
				<text class="cart-title-text">购物车</text>
			</view>
			<!-- 商品列表区域 -->
			<uni-swipe-action>
				<block v-for="(goods, i) in cart" :key="i">
					<uni-swipe-action-item :right-options="options" @click="swipeActionClickHandler(goods)">
						<my-goods :goods="goods" :showRadio="true" @radio-change="radioChangeHandler" :showNum="true"
							@num-change="numberChangeHandler"></my-goods>
					</uni-swipe-action-item>
				</block>
			</uni-swipe-action>
			<my-settle></my-settle>
		</view>

		<!-- 空白购物车区域 -->
		<view class="empty-cart" v-else>
			<image src="../../static/[email protected]" class="empty-img"></image>
			<text class="tip-text">空空如也~</text>
		</view>
	</view>

style

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

 

10. Login and payment 

1. Click the settlement button to judge the conditions

Note: After the user clicks the settlement button, he or she needs to determine whether to check the product to be settled , whether to select the delivery address , and whether to log in .

1.1 In  my-settle the component, bind the click event handler function to the settlement button:

<!-- 结算按钮 -->
<view class="btn-settle" @click="settlement">结算({
   
   {checkedCount}})</view>

 1.2  my-settle Declare the settlement event handling function in the methods node of the component as follows:

// 点击了结算按钮
settlement() {
  // 1. 先判断是否勾选了要结算的商品
  if (!this.checkedCount) return uni.$showMsg('请选择要结算的商品!')

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

  // 3. 最后判断用户是否登录了
  if (!this.token) return uni.$showMsg('请先登录!')
}

1.3 In  my-settle the component, use  mapGetters the auxiliary function to map it from  m_user the module  addstr to the current component:

export default {
  computed: {
    ...mapGetters('m_cart', ['total', 'checkedCount', 'checkedGoodsAmount']),
    // addstr 是详细的收货地址
    ...mapGetters('m_user', ['addstr']),
    isFullCheck() {
      return this.total === this.checkedCount
    },
  },
}

1.4 In  the node store/user.js of the module  state , declare  token the string:

export default {
  // 开启命名空间
  namespaced: true,

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

  // 省略其它代码
}

 1.5 In  my-settle the component, use  mapState the auxiliary function to map it from  m_user the module  token to the current component:

// 按需从 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
    },
  },
}

 2. Log in

2.1 Implement on-demand display of login and user information components

First create two new components, my-userinfo and my-login.

In  the page,  import the required  string my.vue through  the auxiliary function  :mapStatetoken

import badgeMix from '@/mixins/tabbar-badge.js'
// 1. 从 vuex 中按需导入 mapState 辅助函数
import { mapState } from 'vuex'

export default {
  mixins: [badgeMix],
  computed: {
    // 2. 从 m_user 模块中导入需要的 token 字符串
    ...mapState('m_user', ['token']),
  },
  data() {
    return {}
  },
}

On  my.vue the page, implement on-demand display of login components and user information components :

<template>
  <view>

    <!-- 用户未登录时,显示登录组件 -->
    <my-login v-if="!token"></my-login>

    <!-- 用户登录后,显示用户信息组件 -->
    <my-userinfo v-else></my-userinfo>

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


<style lang="scss">
.login-container {
  // 登录盒子的样式
  height: 750rpx;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background-color: #f8f8f8;
  position: relative;
  overflow: hidden;

  // 绘制登录盒子底部的半椭圆造型
  &::after {
    content: ' ';
    display: block;
    position: absolute;
    width: 100%;
    height: 40px;
    left: 0;
    bottom: 0;
    background-color: white;
    border-radius: 100%;
    transform: translateY(50%);
  }

  // 登录按钮的样式
  .btn-login {
    width: 90%;
    border-radius: 100px;
    margin: 15px 0;
    background-color: #c00000;
  }

  // 按钮下方提示消息的样式
  .tips-text {
    font-size: 12px;
    color: gray;
  }
}
</style>
2.3 Click the login button to obtain basic information of WeChat users

button Bind  attributes to the login  button open-type="getUserInfo" , indicating that when the button is clicked, you want to obtain the user's basic information:

<!-- 登录按钮 -->
<!-- 可以从 @getuserinfo 事件处理函数的形参中,获取到用户的基本信息 -->
<button type="primary" class="btn-login" open-type="getUserInfo" @getuserinfo="getUserInfo">一键登录</button>

methods Declare  getUserInfo the event handling function in  the node as follows:

methods: {
  // 获取微信用户的基本信息
  getUserInfo(e) {
    // 判断是否获取用户信息成功
    if (e.detail.errMsg === 'getUserInfo:fail auth deny') return uni.$showMsg('您取消了登录授权!')

    // 获取用户信息成功, e.detail.userInfo 就是用户的基本信息
    console.log(e.detail.userInfo)
  }
}
 2.4 Store user’s basic information in vuex

In  store/user.js the module's state node, the declared  userinfo information objects are as follows:

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

In  store/user.js the mutations node of the module, declare the following two methods:

// 方法
mutations: {
  // 省略其它代码...

  // 更新用户的基本信息
  updateUserInfo(state, userinfo) {
    state.userinfo = userinfo
    // 通过 this.commit() 方法,调用 m_user 模块下的 saveUserInfoToStorage 方法,将 userinfo 对象持久化存储到本地
    this.commit('m_user/saveUserInfoToStorage')
  },

  // 将 userinfo 持久化存储到本地
  saveUserInfoToStorage(state) {
    uni.setStorageSync('userinfo', JSON.stringify(state.userinfo))
  }
}

 Use  mapMutations auxiliary functions to map the required methods to  my-login components:

// 1. 按需导入 mapMutations 辅助函数
import { mapMutations } from 'vuex'

export default {
  data() {
    return {}
  },
  methods: {
    // 2. 调用 mapMutations 辅助方法,把 m_user 模块中的 updateUserInfo 映射到当前组件中使用
    ...mapMutations('m_user', ['updateUserInfo']),
    // 获取微信用户的基本信息
    getUserInfo(e) {
      // 判断是否获取用户信息成功
      if (e.detail.errMsg === 'getUserInfo:fail auth deny') return uni.$showMsg('您取消了登录授权!')
      // 获取用户信息成功, e.detail.userInfo 就是用户的基本信息
      // console.log(e.detail.userInfo)

      // 3. 将用户的基本信息存储到 vuex 中
      this.updateUserInfo(e.detail.userInfo)
    },
  },
}
2.5 Log in to obtain the Token string 

Requirement description: After obtaining the basic information of the WeChat user, you need to further call the login-related interface to exchange for the Token string after successful login .

In  getUserInfo the method, pre-call  this.getToken() the method and pass the obtained user information into it:

// 获取微信用户的基本信息
getUserInfo(e) {
  // 判断是否获取用户信息成功
  if (e.detail.errMsg === 'getUserInfo:fail auth deny') return uni.$showMsg('您取消了登录授权!')

  // 将用户的基本信息存储到 vuex 中
  this.updateUserInfo(e.detail.userInfo)

  // 获取登录成功后的 Token 字符串
  this.getToken(e.detail)
}

methods Define  methods  in  getToken and call login-related APIs to implement the login function.

Because we don’t have permission to use the token here, we can only simulate one ourselves.

"Bearer 	eyJhbGci0iJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWOiOjEyLCJpYXQi0OjE1MjUGNDIyMjMsImV4cCI6NTUyNTO40DvYyN30.g-4GtEQNPwT_Xs8Pq7Lrco_9nlHQQsBi0KZerkO-0-o"
// 调用登录接口,换取永久的 token
async getToken(info) {
  // 调用微信登录接口
  const [err, res] = await uni.login().catch(err => err)
  // 判断是否 uni.login() 调用失败
  if (err || res.errMsg !== 'login:ok') return uni.$showError('登录失败!')

  // 准备参数对象
  const query = {
    code: res.code,
    encryptedData: info.encryptedData,
    iv: info.iv,
    rawData: info.rawData,
    signature: info.signature
  }

  // 换取 token
  const { data: loginResult } = await uni.$http.post('/api/public/v1/users/wxlogin', query)
  if (loginResult.meta.status !== 200) return uni.$showMsg('登录失败!')
  uni.$showMsg('登录成功')
  
}
2.6 Store Token in vuex 
In  the node store/user.js of the module  mutations , declare the following two methods:
mutations: {
  // 省略其它代码...

  // 更新 token 字符串
  updateToken(state, token) {
    state.token = token
    // 通过 this.commit() 方法,调用 m_user 模块下的 saveTokenToStorage 方法,将 token 字符串持久化存储到本地
    this.commit('m_user/saveTokenToStorage')
  },

  // 将 token 字符串持久化存储到本地
  saveTokenToStorage(state) {
    uni.setStorageSync('token', state.token)
  }
}

The nodes to modify  store/user.js the module  state are as follows: 

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

In  my-login the component, map the methods in vuex  updateToken to the current component and use: 

methods: {
  // 1. 使用 mapMutations 辅助方法,把 m_user 模块中的 updateToken 方法映射到当前组件中使用
  ...mapMutations('m_user', ['updateUserInfo', 'updateToken'])

  // 省略其它代码...

  // 调用登录接口,换取永久的 token
  async getToken(info) {
    // 调用微信登录接口
    const [err, res] = await uni.login().catch(err => err)
    // 判断是否 uni.login() 调用失败
    if (err || res.errMsg !== 'login:ok') return uni.$showError('登录失败!')

    // 准备参数对象
    const query = {
      code: res.code,
      encryptedData: info.encryptedData,
      iv: info.iv,
      rawData: info.rawData,
      signature: info.signature
    }
	const token ="Bearer 	eyJhbGci0iJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWOiOjEyLCJpYXQi0OjE1MjUGNDIyMjMsImV4cCI6NTUyNTO40DvYyN30.g-4GtEQNPwT_Xs8Pq7Lrco_9nlHQQsBi0KZerkO-0-o"

    // 换取 token
    const { data: loginResult } = await uni.$http.post('/api/public/v1/users/wxlogin', query)
    //if (loginResult.meta.status !== 200) return uni.$showMsg('登录失败!')

    // 2. 更新 vuex 中的 token
    //this.updateToken(loginResult.message.token)
    this.updateToken(token)
  }
}

3. User information 

3.1 Implement the basic layout of the user avatar nickname area
<template>
  <view class="my-userinfo-container">

    <!-- 头像昵称区域 -->
    <view class="top-box">
      <image src="" class="avatar"></image>
      <view class="nickname">xxx</view>
    </view>

  </view>
</template>

<style lang="scss">
.my-userinfo-container {
  height: 100%;
  // 为整个组件的结构添加浅灰色的背景
  background-color: #f4f4f4;

  .top-box {
    height: 400rpx;
    background-color: #c00000;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;

    .avatar {
      display: block;
      width: 90px;
      height: 90px;
      border-radius: 45px;
      border: 2px solid white;
      box-shadow: 0 1px 5px black;
    }

    .nickname {
      color: white;
      font-weight: bold;
      font-size: 16px;
      margin-top: 10px;
    }
  }
}

</style>
3.2 Render the user’s avatar and nickname 

In  my-userinfo the component,  mapState use the auxiliary function to map the required members to the current component:

// 按需导入 mapState 辅助函数
import { mapState } from 'vuex'

export default {
  computed: {
    // 将 m_user 模块中的 userinfo 映射到当前页面中使用
    ...mapState('m_user', ['userinfo']),
  },
  data() {
    return {}
  },
}

Render the user's avatar and nickname into the page:

<!-- 头像昵称区域 -->
<view class="top-box">
  <image :src="userinfo.avatarUrl" class="avatar"></image>
  <view class="nickname">{
   
   {userinfo.nickName}}</view>
</view>
 3.3 Rendering panel information area
		<!-- 面板的列表区域 -->
		<view class="panel-list">
			<!-- 第一个面板 -->
			<view class="panel">
				<!-- panel 的主体区域 -->
				<view class="panel-body">
					<!-- panel 的 item 项 -->
					<view class="panel-item">
						<text>8</text>
						<text>收藏的店铺</text>
					</view>
					<view class="panel-item">
						<text>14</text>
						<text>收藏的商品</text>
					</view>
					<view class="panel-item">
						<text>18</text>
						<text>关注的商品</text>
					</view>
					<view class="panel-item">
						<text>84</text>
						<text>足迹</text>
					</view>
				</view>
			</view>

			<!-- 第二个面板 -->
			<!-- 第二个面板 -->
			<view class="panel">
				<!-- 面板的标题 -->
				<view class="panel-title">我的订单</view>
				<!-- 面板的主体 -->
				<view class="panel-body">
					<!-- 面板主体中的 item 项 -->
					<view class="panel-item">
						<image src="/static/my-icons/icon1.png" class="icon"></image>
						<text>待付款</text>
					</view>
					<view class="panel-item">
						<image src="/static/my-icons/icon2.png" class="icon"></image>
						<text>待收货</text>
					</view>
					<view class="panel-item">
						<image src="/static/my-icons/icon3.png" class="icon"></image>
						<text>退款/退货</text>
					</view>
					<view class="panel-item">
						<image src="/static/my-icons/icon4.png" class="icon"></image>
						<text>全部订单</text>
					</view>
				</view>
			</view>
			<!-- 第三个面板 -->
			<!-- 第三个面板 -->
			<view class="panel">
				<view class="panel-list-item">
					<text>收货地址</text>
					<uni-icons type="arrowright" size="15"></uni-icons>
				</view>
				<view class="panel-list-item">
					<text>联系客服</text>
					<uni-icons type="arrowright" size="15"></uni-icons>
				</view>
				<view class="panel-list-item">
					<text>退出登录</text>
					<uni-icons type="arrowright" size="15"></uni-icons>
				</view>
			</view>
		</view>

<style lang="scss">

	.panel-list {
		padding: 0 10px;
		position: relative;
		top: -10px;

		.panel {
			background-color: white;
			border-radius: 3px;
			margin-bottom: 8px;

			.panel-title {
				line-height: 45px;
				padding-left: 10px;
				font-size: 15px;
				border-bottom: 1px solid #f4f4f4;
			}

			.panel-body {
				display: flex;
				justify-content: space-around;

				.panel-item {
					display: flex;
					flex-direction: column;
					align-items: center;
					justify-content: space-around;
					font-size: 13px;
					padding: 10px 0;

					.icon {
						width: 35px;
						height: 35px;
					}
				}
			}
		}
	}
	.panel-list-item {
	  height: 45px;
	  display: flex;
	  justify-content: space-between;
	  align-items: center;
	  font-size: 15px;
	  padding: 0 10px;
	}
</style>
renderings

3.4 Implement the function of logging out

退出登录 Bind  click a click event handler to the item in the third panel area 

<view class="panel-list-item" @click="logout">
  <text>退出登录</text>
  <uni-icons type="arrowright" size="15"></uni-icons>
</view>

 Define  the event handler function in  my-userinfo the component  node  :methodslogout

// 退出登录
async logout() {
  // 询问用户是否退出登录
  const [err, succ] = await uni.showModal({
    title: '提示',
    content: '确认退出登录吗?'
  }).catch(err => err)

  if (succ && succ.confirm) {
     // 用户确认了退出登录的操作
     // 需要清空 vuex 中的 userinfo、token 和 address
     this.updateUserInfo({})
     this.updateToken('')
     this.updateAddress({})
  }
}

 Use  mapMutations auxiliary methods to map the required mutations methods to the current component:

// 按需导入辅助函数
import { mapState, mapMutations } from 'vuex'

export default {
  methods: {
    ...mapMutations('m_user', ['updateUserInfo', 'updateToken', 'updateAddress']),
  },
}

4. Automatically jump to the login page after three seconds 

Requirement description: On the shopping cart page, when the user clicks the "Checkout" button, if the user is not logged in, it will automatically jump to the login page after 3 seconds.

In  the node my-settle of the component  methods , declare a  showTips method called, specifically used to display the countdown prompt message:

// 展示倒计时的提示消息
showTips(n) {
  // 调用 uni.showToast() 方法,展示提示消息
  uni.showToast({
    // 不展示任何图标
    icon: 'none',
    // 提示的消息
    title: '请登录后再结算!' + n + ' 秒后自动跳转到登录页',
    // 为页面添加透明遮罩,防止点击穿透
    mask: true,
    // 1.5 秒后自动消失
    duration: 1500
  })
}

data Declare the countdown seconds in the node

data() {
  return {
    // 倒计时的秒数
    seconds: 3
  }
}

 Modify  the event handler function 结算 of the button  click . If the user is not logged in, a method called  will be called in advancedelayNavigate  to perform a countdown navigation jump:

// 点击了结算按钮
settlement() {
  // 1. 先判断是否勾选了要结算的商品
  if (!this.checkedCount) return uni.$showMsg('请选择要结算的商品!')

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

  // 3. 最后判断用户是否登录了,如果没有登录,则调用 delayNavigate() 进行倒计时的导航跳转
  // if (!this.token) return uni.$showMsg('请先登录!')
  if (!this.token) return this.delayNavigate()
},

 Define  delayNavigate the method to initially implement the countdown prompt function :

// 延迟导航到 my 页面
delayNavigate() {
  // 1. 展示提示消息,此时 seconds 的值等于 3
  this.showTips(this.seconds)

  // 2. 创建定时器,每隔 1 秒执行一次
  setInterval(() => {
    // 2.1 先让秒数自减 1
    this.seconds--
    // 2.2 再根据最新的秒数,进行消息提示
    this.showTips(this.seconds)
  }, 1000)
},

The problem with the above code: the timer will not stop automatically , and the number of seconds will be equal to 0 or less than 0!

data Declare the timer's ID in the node as follows 

data() {
  return {
    // 倒计时的秒数
    seconds: 3,
    // 定时器的 Id
    timer: null
  }
}

The transformation  delayNavigate method is as follows:

// 延迟导航到 my 页面
delayNavigate() {
  this.showTips(this.seconds)

  // 1. 将定时器的 Id 存储到 timer 中
  this.timer = setInterval(() => {
    this.seconds--

    // 2. 判断秒数是否 <= 0
    if (this.seconds <= 0) {
      // 2.1 清除定时器
      clearInterval(this.timer)

      // 2.2 跳转到 my 页面
      uni.switchTab({
        url: '/pages/my/my'
      })

      // 2.3 终止后续代码的运行(当秒数为 0 时,不再展示 toast 提示消息)
      return
    }

    this.showTips(this.seconds)
  }, 1000)
},

 The problem with the above code: seconds will not be reset , causing the second, third, and nth countdown jump function to not work properly.

To further modify  delayNavigate the method, when executing this method, immediately  seconds reset the seconds to  3 : 

// 延迟导航到 my 页面
delayNavigate() {
  // 把 data 中的秒数重置成 3 秒
  this.seconds = 3
  this.showTips(this.seconds)

  this.timer = setInterval(() => {
    this.seconds--

    if (this.seconds <= 0) {
      clearInterval(this.timer)
      uni.switchTab({
        url: '/pages/my/my'
      })
      return
    }

    this.showTips(this.seconds)
  }, 1000)
}

 5. WeChat payment

1. Add the Token identity authentication field in the request header

Open the project root directory  main.jsand modify  $http.beforeRequest the code in the request interceptor as follows:

// 请求开始之前做一些事情
$http.beforeRequest = function(options) {
  uni.showLoading({
    title: '数据加载中...',
  })

  // 判断请求的是否为有权限的 API 接口
  if (options.url.indexOf('/my/') !== -1) {
    // 为请求头添加身份认证字段
    options.header = {
      // 字段的值可以直接从 vuex 中进行获取
      Authorization: store.state.m_user.token,
    }
  }
}
2. WeChat payment process

1. Create an order

  • API interface to request the creation of an order: send (order amount, shipping address, product information contained in the order) to the server
  • Server response result: order number

2. Order advance payment

  • API interface for requesting order prepayment: send (order number) to the server
  • The result of the server response: the parameter object of the order prepayment , which contains the necessary parameters related to the order payment.

3. Initiate WeChat payment

  • Call  uni.requestPayment() this API to initiate WeChat payment; pass the "order prepayment object" obtained in step 2 as a parameter to  uni.requestPayment() the method
  • Listen to  uni.requestPayment() this API  success, fail, complete callback function

 Modify  the method my-settle in the component  settlement . After the current three judgment conditions are passed, the method to implement WeChat payment is called:

// 点击了结算按钮
settlement() {
  // 1. 先判断是否勾选了要结算的商品
  if (!this.checkedCount) return uni.$showMsg('请选择要结算的商品!')

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

  // 3. 最后判断用户是否登录了
  // if (!this.token) return uni.$showMsg('请先登录!')
  if (!this.token) return this.delayNavigate()

  // 4. 实现微信支付功能
  this.payOrder()
},

payOrder payment function 

// 微信支付
async payOrder() {
  // 1. 创建订单
  // 1.1 组织订单的信息对象
  const orderInfo = {
    // 开发期间,注释掉真实的订单价格,
    // order_price: this.checkedGoodsAmount,
    // 写死订单总价为 1 分钱
    order_price: 0.01,
    consignee_addr: this.addstr,
    goods: this.cart.filter(x => x.goods_state).map(x => ({ goods_id: x.goods_id, goods_number: x.goods_count, goods_price: x.goods_price }))
  }
  // 1.2 发起请求创建订单
  const { data: res } = await uni.$http.post('/api/public/v1/my/orders/create', orderInfo)
  if (res.meta.status !== 200) return uni.$showMsg('创建订单失败!')
  // 1.3 得到服务器响应的“订单编号”
  const orderNumber = res.message.order_number

   // 2. 订单预支付
   // 2.1 发起请求获取订单的支付信息
   const { data: res2 } = await uni.$http.post('/api/public/v1/my/orders/req_unifiedorder', { order_number: orderNumber })
   // 2.2 预付订单生成失败
   if (res2.meta.status !== 200) return uni.$showError('预付订单生成失败!')
   // 2.3 得到订单支付相关的必要参数
   const payInfo = res2.message.pay

   // 3. 发起微信支付
   // 3.1 调用 uni.requestPayment() 发起微信支付
   const [err, succ] = await uni.requestPayment(payInfo)
   // 3.2 未完成支付
   if (err) return uni.$showMsg('订单未支付!')
   // 3.3 完成了支付,进一步查询支付的结果
   const { data: res3 } = await uni.$http.post('/api/public/v1/my/orders/chkOrder', { order_number: orderNumber })
   // 3.4 检测到订单未支付
   if (res3.meta.status !== 200) return uni.$showMsg('订单未支付!')
   // 3.5 检测到订单支付完成
   uni.showToast({
     title: '支付完成!',
     icon: 'success'
   })
 }

 At this point, the entire project is considered over. The project code that I need can be cloned locally using git.

Just enter this line of command in a folder

git clone https://github.com/jiangjunjie666/uni-shop.git

Guess you like

Origin blog.csdn.net/m0_64642443/article/details/131534831