Table of contents
Overall project effect display:
1. Create a new uni-app project
Run the project on WeChat Developer Tools
Host the project on github for management
4. Modify the navigation bar style effect
2. Mount $http, configure request interceptor and response interceptor
3. Complete the carousel chart
4. Complete the category navigation area
6. Submit the code for the home page
1. Implement the basic structure
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
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
1. Define the request parameter object
3. Render product list structure
4. Encapsulate product items into custom components
5. Use filters to process prices
7. Prevent additional requests from being initiated through throttling
9. Click on the product item to jump to the details page
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
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
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
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
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
1. Click the settlement button to judge the conditions
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
In the mutations node of the store/user.js module, declare the following two methods:
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
3.4 Implement the function of logging out
4. Automatically jump to the login page after three seconds
1. Add the Token identity authentication field in the request header
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 :view
click
<view class="my-search-box" :style="{'border-radius': radius + 'px'}" @click="searchBoxHandler">
<uni-icons type="search" size="17"></uni-icons>
<text class="placeholder">搜索</text>
</view>
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
-
Do not make any modifications in data
historyList
, still use push to append at the end -
Define a calculated property
historys
. After invertinghistoryList
the arrayreverse
, the value of the calculated property is:
computed: {
historys() {
// 注意:由于数组是引用类型,所以不要直接基于原数组调用 reverse 方法,以免修改原数组中元素的顺序
// 而是应该新建一个内存无关的数组,再进行 reverse 反转
return [...this.historyList].reverse()
}
}
historyList
When 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
-
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. -
Solution: Determine
goods_info.goods_name
whether the value of the attribute exists, and then usev-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:addstr
user.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
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.
Solution : Renovate
mixins/tabbar-badge.js
the code, usewatch
a listener, listen fortotal
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 :mapState
token
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 :methods
logout
// 退出登录
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.js
and 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 touni.requestPayment()
the method- Listen to
uni.requestPayment()
this APIsuccess
,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