一,H5端和小程序的tabbar和顶部标题兼容性问题
1.1,此时我们的需求是鼠标滚动,让推荐推荐标签栏一下的页面滚动,而不是整个页面滚动
此时让html中滚动区域包裹scroll-view标签
<template> <view class="indexContainer"> <view class="header"> <image class="logo" src="/static/images/logo.png" mode=""></image> <view class="search"> <view class="iconfont icon-sousuo"></view> <input class="searchInput" type="text" value="" placeholder-class="placeholder" placeholder="搜索商品"/> </view> <button class="username">七月</button> </view> <scroll-view scroll-x="true" class="navScroll" enable-flex="true" v-if="indexData.kingKongModule" > <view class="navItem " :class="currentIndex ===-1? 'active' :''" @click="changeNavIndex(-1)" >推荐</view> <view class="navItem" :class="currentIndex===index ?'active':''" @click="changeNavIndex(index)" v-for="(item,index) in indexData.kingKongModule.kingKongList" :key="item.L1Id" >{{item.text}}</view> </scroll-view> <!-- 轮播图 --> <scroll-view scroll-y="true" class="recommendScroll"> <Recommend></Recommend> </scroll-view> </view> </template>
此时h5页面和小程序计算高度发生了分歧
小程序中,要计算滚动区域的高度时,不需计算顶部标题的高度,以及tabbar栏的高度,此时滚动的区域高度为,height calc(100vh - 162upx)
在h5页面中,要计算滚动区域的高度时,需计算顶部标题的高度,以及tabbar栏的高度,此时滚动的区域高度为,height calc(100vh - 162upx- 88upx - 100upx)
此时他们高度不兼容,我们需要引入css变量,因为,
// --window-top->header占用的高度->H5端 44px->小程序端 0px
// --window-bottom->tabBar占用的高度->H5端 50px->小程序端 0px
stylus样式
.recommendScroll // --window-top->header占用的高度->H5端 44px->小程序端 0px // --window-bottom->tabBar占用的高度->H5端 50px->小程序端 0px height calc(100vh - 162upx - var(--window-top) - var(--window-bottom))
二,登录个人中心模块,用户首次授权以及二次授权逻辑
2.1,在个人中心页面person.vue中,点击顶部登录区域,跳转到登录页面
<template> <div> <div class="header" @click="toLogin"> <image class="userImg" :src="userinfo.avatarUrl? userinfo.avatarUrl : '../../static/images/personal/personal.png' " mode=""></image> <div class='userInfo'> <p>{{userinfo.nickName ? userinfo.nickName : '未登录'}}</p> <p>点击登录账号</p> </div> </div> <div class="content"> <h2>我的资产</h2> <p class='line'></p> <div class="myAssetList"> <div class='assetItem'> <span>¥0</span> <span>回馈金</span> </div> <div class='assetItem'> <span>¥0</span> <span>红包</span> </div> <div class='assetItem'> <span>¥0</span> <span>优惠券</span> </div> <div class='assetItem'> <span>¥0</span> <span>津贴</span> </div> <div class='assetItem'> <span>¥0</span> <span>礼品卡</span> </div> </div> <!-- 列表选项 --> <div class="personalList"> <div class="navItem" v-for='(item, index) in personalList' :key='index'> <i class='iconfont ' :class='item.icon'></i> <p>{{item.name}}</p> </div> </div> </div> </div> </template>
js
methods: { toLogin(){ // 如果用户授权登录了,不用再跳转 if(this.userinfo) return uni.navigateTo({ url:"/pages/login/login" }) } }
2.2,在登录页面,点击微信登录,获取授权行为,获取到用户信息,将用户信息保存到本地,以及路由授权成功后路由跳转到个人中新页
<template> <view class="loginContainer"> <image class="logo" src="http://yanxuan.nosdn.127.net/39c5e4583753d4c3cb868a64c2c109ea.png" mode=""></image> <p class='text'>网易自营,精品生活家居品牌</p> <div class="loginMethods"> <button class="login wechatLogin" open-type="getUserInfo" @getuserinfo="getuserinfo"> 微信登录 </button> <button class="login emailLogin"> 邮箱登录 </button> </div> </view> </template>
js
methods: { getUserInfo(res){ let {rawData}=res.detail; console.log(res) if(rawData){ //如果授权成功,将用户信息存储至storage uni.setStorage({ key:"userinfo", data:rawData }) //关闭所有页面,打开指定页面,指定页面会被重新挂载 uni.reLaunch({ url:"/pages/personal/personal?userinfo="+rawData }) } // console.log(res) } }
2.3, 在个人中心页获取到路由传递过来的query参数,并且对于二次登录授权,采取逻辑处理
mounted(){ //this身上拥有$mp属性,内部存储小程序原生options // console.log(this.$mp.query.userinfo) let {userinfo}=this.$mp.query; if(userinfo){ //用于首次用户授权成功,login页面跳转回来之后触发 this.userinfo=JSON.parse(userinfo) }else{ //用于二次登录,用户授权已成功,直接获取用户之前的授权信息 uni.getUserInfo({ success:(res)=>{ this.userinfo=res.userInfo } }) } },
data(){ return { userinfo:{},
2.4,如果用户授权登录成功后,不需要在跳转到登录页面了
methods: { toLogin(){ if(this.userinfo.nickName)return; uni.navigateTo({ url:"/pages/login/login" }) } }
模板数据渲染
<div class="header" @click="toLogin"> <image class="userImg" :src="userinfo.avatarUrl? userinfo.avatarUrl : '../../static/images/personal/personal.png' " mode=""></image> <div class='userInfo'> <p>{{userinfo.nickName ? userinfo.nickName : '未登录'}}</p> <p>{{userinfo.nickName ? '微信用户' : '点击登录账号'}}</p> </div> </div>
个人中心顶部标题背景颜色修改,在page.json中配置
"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages { "path": "pages/personal/personal", "style": { "navigationBarTitleText": "个人中心", "navigationBarBackgroundColor":"#EED7B5" } }
三,商品详情页发送请求获取数据
3.1,在商品列表页,点击商品,路由跳转到商品详情页,携带id参数,然后商品详情页,发送请求,获取数据
商品列表页
<template> <div class="listContainer"> <div @click='toDetail(item.id)' class="listItem" v-for='(item, index) in shopList' :key='item.id'> <image :src="item.listPicUrl" mode=""></image> <p>{{item.name}}</p> <p style='color: red;font-weight: bold;'>$ {{item.retailPrice}}</p> </div> </div> </template> <script> export default { props: ['shopList'], methods: { toDetail(goodId){ // wx.navigateTo({ // url: '/pages/detail/detail?shopItem=' + JSON.stringify(shopItem) // }) wx.navigateTo({ url:"/pages/detail/detail?goodId="+goodId }) } } }
商品详情页,发送请求,获取数据
async mounted(){ let {goodId}= this.$mp.query let goodDetail = await request('/getGoodDetail',{ id:goodId }) this.goodDetail =goodDetail },
data() { return { goodDetail:{} } },
搭建服务端的路由配置
//json数据, const goods = require('./datas/goods.json'); router.get('/getGoodDetail' ,function(ctx,next){ let {id} = ctx.query // console.log(id) // 注意路由传参过来的数字是字符串格式 let good= goods.find((item,index)=> item.id === id*1) ctx.body = good })
四,点击商品购物车按钮,将商品添加到购物车页面
4.1,新建cart.js的vuex,将数据保存在vuex中,cartList数据是购物车的数据,数据是太长,不显示
const state ={ cartList:[
在商品详情组件中,映射mutations中的函数,将商品详情的数据传给store中的mutations中,然后修改state中的cartList数据
<view class="btn addShopCart" @click="addShopItem">加入购物车</view>
import {mapMutations} from 'vuex';
async mounted(){ let {goodId} = this.$mp.query; let good = await request('/getGoodDetail',{ id:goodId }); this.good=good }, methods:{ addShopItem(){ //触发增加商品的mutation // this.$store.commit this.addShopItemMutation(this.good) }, ...mapMutations(["addShopItemMutation"]) }
在cart.js的vuex中mutations修改cartList
const mutations={ addShopItemMutation(state,good){ // console.log(good); //查询购物车列表中是否已存在准备新增的商品 let shopItem=state.cartList.find(shopItem=>shopItem.id===good.id); if(shopItem){ //如果购物车中已存在,只需要将他的数量+1 shopItem.count++; }else{ //如果购物车中不存在,给商品对象添加属性count,设置初始值为1,并添加至购物车列表中 good.count=1; } // console.log('已添加至购物车') },
4.2,购物车组件的数据渲染,循环遍历
<view class="cartList"> <view class="cartItem" v-for="shopItem in cartList" :key="shopItem.id"> <text class='iconfont icon-xuanzhong selected'></text> <view class="shopItem"> <image class="shopImg" :src="shopItem.listPicUrl" mode=""></image> <view class="shopInfo"> <text>{{shopItem.name}}</text> <text class="price">¥{{shopItem.retailPrice}}</text> </view> </view> <!-- 控制数量 --> <view class="countCtrl"> <text class="add"> + </text> <text class="count"> {{shopItem.count}}</text> <text class="del"> - </text> </view> </view> </view>
此时有个bug,新增的商品,count属性不是响应式的,不会触发视图的更新,需要设置响应式属性
const mutations={ addShopItemMutation(state,good){ // console.log(good); //查询购物车列表中是否已存在准备新增的商品 let shopItem=state.cartList.find(shopItem=>shopItem.id===good.id); if(shopItem){ //如果购物车中已存在,只需要将他的数量+1 shopItem.count++; }else{ //如果购物车中不存在,给商品对象添加属性count,设置初始值为1,并添加至购物车列表中 // good.count=1; // 给新增的商品对象,添加响应式属性count,初始值为1 Vue.set(good,"count",1); Vue.set(good,"selected",true); state.cartList.push(good); } // console.log('已添加至购物车') },
五,购物车模块,修改商品数量逻辑
5.1,点击加减号,修改逻辑。对加减号进行标识,而且是对那个商品修改数量,将参数传到cart.js的vuex中,cartlist数组进行操作
<!-- 登陆之后的购物车 --> <view class="cartList"> <view class="cartItem" v-for="(shopItem,index) in cartList" :key="shopItem.id"> <text class='iconfont icon-xuanzhong' @click="changeShopItemSelected(!shopItem.selected,index)" :class="shopItem.selected?'selected':''"></text> <view class="shopItem"> <image class="shopImg" :src="shopItem.listPicUrl" mode=""></image> <view class="shopInfo"> <text>{{shopItem.name}}</text> <text class="price">¥{{shopItem.retailPrice}}</text> </view> </view> <!-- 控制数量 --> <view class="countCtrl"> <text class="add" @click="changeShopItemCount(true,index)"> + </text> <text class="count"> {{shopItem.count}} </text> <text class="del" @click="changeShopItemCount(false,index)"> - </text> </view> </view> </view>
5.2, 映射cart.js中vuex中mutations,将参数传过去
import {mapState, mapMutations} from 'vuex'
methods:{ changeShopItemCount(type,index){ this.changeShopItemCountMutation({type,index}) }, ...mapMutations(['changeShopItemCountMutation'])
5.3,在cart.js的vuex中对cartlist数组操作,记住,传入到这里的参数要是个对象,解构下
changeShopItemCountMutation(state,{type,index}){ /*需求:当用户点击+/-按钮时,去修改对应商品的数量 拆解需求: 1.绑定监听->监视用户点击操作 2.区分点的到底是+还是-,+就是数量+1,-就是数量-1 3.通过点击计数器找到对应的商品,并修改数量 1)+没有限制 2)-的情况,如果-1之后,不小于1,正常-- 如果-1之后,小于1,直接从购物车中删除 */ // 通过传过来的下标,找到对应的商品 let shopItem=state.cartList[index]; if(type){ // 点击的加号 shopItem.count++; }else{ // 点击的减号 if(shopItem.count>1){ shopItem.count--; }else{ // 小于等于1,删除该商品 state.cartList.splice(index,1); } } // console.log('changeShopItemCountMutation',state,type,index) },
5.4,在购物车页,如果是有数据,展示购物车数据,如果,没有数据,展示空的购物车页,需要用到v-if,v-else
block标签只是展示而且,没有实际意义
<template> <view class="cartContainer"> <view class="title">购物车</view> <!-- 未登录时候的购物车 --> <block v-if="cartList.length===0"> <view class="header"> <text>30天无忧退货</text> <text>48小时快速退货</text> <text>满99元免邮费</text> </view> <view class="contentContainer"> <image class="cartImg" src="http://yanxuan-static.nosdn.127.net/hxm/yanxuan-wap/p/20161201/style/img/icon-normal/noCart-d6193bd6e4.png?imageView&type=webp" mode=""></image> <view class="addMore">去添加点什么吧</view> </view> </block> <block v-else> <!-- 登陆之后的购物车 --> <view class="cartList"> <view class="cartItem" v-for="(shopItem,index) in cartList" :key="shopItem.id"> <text class='iconfont icon-xuanzhong selected'></text> <view class="shopItem"> <image class="shopImg" :src="shopItem.listPicUrl" mode=""></image> <view class="shopInfo"> <text>{{shopItem.name}}</text> <text class="price">¥{{shopItem.retailPrice}}</text> </view> </view> <!-- 控制数量 --> <view class="countCtrl"> <text class="add" @click="changeShopItemCount(true,index)"> + </text> <text class="count"> {{shopItem.count}}</text> <text class="del" @click="changeShopItemCount(false,index)"> - </text> </view> </view> </view> <!-- 底部下单 --> <view class="cartFooter"> <text class='iconfont icon-xuanzhong selected'></text> <text class="allSelected">已选 3</text> <view class="right"> <text class="totalPrice">合计: ¥1000</text> <text class="preOrder">下单</text> </view> </view> </block> </view> </template>
六,购物车模块,商品选中状态逻辑
6.1,selected类是选中的状态,点击选中框,切换状态,将参数传给cart.js中的vuex,cartlist数据逻辑整理
<view class="cartItem" v-for="(shopItem,index) in cartList" :key="shopItem.id"> <text class='iconfont icon-xuanzhong ' @click="changeShopItemSelected(!shopItem.selected,index)" :class="shopItem.selected?'selected':''"></text> <view class="shopItem"> <image class="shopImg" :src="shopItem.listPicUrl" mode=""></image> <view class="shopInfo"> <text>{{shopItem.name}}</text> <text class="price">¥{{shopItem.retailPrice}}</text> </view> </view>
js
changeShopItemSelected(selected,index){ this.changeShopItemSelectedMutation({selected,index}); }, ...mapMutations(['changeShopItemCountMutation','changeShopItemSelectedMutation'])
在cart.js的vuex,此时selected的属性也不是响应式的,需要在添加购物车商品的时候一起添加
changeShopItemSelectedMutation(state,{selected,index}){ /* 单次点击需求:当用户点击选中按钮,将对应商品的选中状态进行修改 全选按钮需求: */ let shopItem = state.cartList[index]; shopItem.selected=selected; // console.log(selected,index) },
addShopItemMutation(state,good){ // console.log(good); //查询购物车列表中是否已存在准备新增的商品 let shopItem=state.cartList.find(shopItem=>shopItem.id===good.id); if(shopItem){ //如果购物车中已存在,只需要将他的数量+1 shopItem.count++; }else{ //如果购物车中不存在,给商品对象添加属性count,设置初始值为1,并添加至购物车列表中 // good.count=1; // 给新增的商品对象,添加响应式属性count,初始值为1 Vue.set(good,"count",1); Vue.set(good,"selected",true); state.cartList.push(good); } // console.log('已添加至购物车') },
七,全选按钮状态处理逻辑
7.1,当单选都选中,全选按钮才选中
在cart.js中的getters中计算全选按钮状态
const getters={ isSelectedAll(state){ /* 需求: 1)当购物车中没有商品,全选按钮应该是未选中状态 2)当购物车中所有商品都是选中状态,全选按钮应该也是选中状态->true 3)当购物车中有部分商品未选中,全选按钮应该是未选中状态->false 4)返回值类型:布尔值 5)书写位置:getter every->所有的元素都满足条件,返回值就是true,只要有一个不满足,结果就是false some->只要有一个元素满足条件,返回值就是true,反之,则是false */ let {cartList} =state; if(cartList.length===0) return false; let result = cartList.every(item=>item.selected); return result; // console.log(result) } }
在购物页映射getters
import {mapState,mapMutations,mapGetters} from 'vuex'
computed:{ ...mapState({ cartList:state=>state.cart.cartList }), ...mapGetters(["isSelectedAll"]) },
<!-- 底部下单 --> <view class="cartFooter"> <text class='iconfont icon-xuanzhong ' :class="isSelectedAll?'selected':''"></text> <text class="allSelected">已选 3</text> <view class="right"> <text class="totalPrice">合计: ¥1000</text> <text class="preOrder">下单</text> </view> </view>
八,点击全选按钮,单选按钮全部勾选上
<view class="cartFooter"> <text class='iconfont icon-xuanzhong ' @click="changeSelectedAll(!isSelectedAll)" :class="isSelectedAll?'selected':''"></text> <text class="allSelected">已选 3</text> <view class="right"> <text class="totalPrice">合计: ¥1000</text> <text class="preOrder">下单</text> </view> </view>
js,将全选状态穿到cart.js的vuex中,
changeSelectedAll(selected){ this.changeSelectedAllMutation(selected); }, ...mapMutations(['changeShopItemCountMutation','changeShopItemSelectedMutation', 'changeSelectedAllMutation'])
cart.js中vuex中处理状态
changeSelectedAllMutation(state,selected){ /* 需求:当用户点击全选按钮时,改变所有商品选中状态(根据当前全选按钮的情况取反) 拆分需求: 1)当用户点击->监听 2)获取到当前全选按钮的状态,并且取反(得到状态a) 3)将所有商品的状态设置为a */ state.cartList.forEach(item=>item.selected=selected) // console.log(result) }
九,获取用户唯一标识openId
购物车模块(重点:获取用户唯一标识openId) 1)无论是点击open-type为getUserInfo的button组件,还是使用wx.getUserInfo方法获取到的用户信息中,都没有用户的唯一标识 2)想要获取用户的唯一标识需要用到wx.login接口并配合服务器进行 流程: 1.客户端通过wx.login()获取到用户的临时登录凭证code 2.将code发送给我们自己的服务器 3.服务器将code+appid+appsecret发送给微信官方服务器 4.微信官方服务器返回session_key以及用户唯一标识openId 5.在服务器端对openId进行加密,最后返回给客户端 6.客户端接收到加密之后的openId,并存储到Storage中
将临时登录凭证吗code, appid,AppSecret(小程序密钥)收集起来,在node服务端发送请求,通过微信自己的接口,返回唯一凭证openId
调用 wx.login() 获取 临时登录凭证code ,并回传到开发者服务器。
调用 auth.code2Session 接口,换取 用户唯一标识 OpenID 和 会话密钥 session_key。
GET https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code
之后开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份。
注意:
会话密钥 session_key 是对用户数据进行 加密签名 的密钥。为了应用自身的数据安全,开发者服务器不应该把会话密钥下发到小程序,也不应该对外提供这个密钥。
临时登录凭证 code 只能使用一次
9.1,我们在app.vue中的onShow函数中,获取登录临时凭证,并且发送请求,将code传递过去
onShow: function() { wx.login({ // 解构 async success({code}){ // console.log(code) let openId = await request('/getOpenId',{code}); uni.setStorage({ key:"openId", data:openId }) } }) },
9.2,在node服务器中临时登录凭证吗code, appid,AppSecret(小程序密钥)收集起来,发送请求,通过微信小程序自己的接口,获取到唯一标识openId
注;fly.js,请求库 npm i flyio
一个支持所有JavaScript运行环境的基于Promise的、支持请求转发、强大的http请求库。可以让您在多个端上尽可能大限度的实现代码复用。
例子
//query参数通过对象传递 fly.get('/user', { id: 133 }) .then(function (response) { console.log(response); }) .catch(function (error) { console.log(error); });
引入fly
const Fly=require("flyio/src/node") const fly=new Fly;
// 用于获取用户唯一标识OpenId router.get('/getOpenId',async (ctx,next)=>{ // console.log(ctx.query.code) let {code} = ctx.query; let appid="wx7ab0b2d5ff1862"; // AppSecret(小程序密钥) let appSecret="35d565a8f9984aa0d9816e9aaf509de"; let data; // 微信小程序提供的接口 let url= `https://api.weixin.qq.com/sns/jscode2session?appid=${appid}&secret=${appSecret}&js_code=${code}&grant_type=authorization_code` // 利用fly.js库发送请求 let result = await fly.get(url) // 转换成字符串 data=JSON.parse(result.data); // 获取openid,session_key let {openid,session_key} =data; ctx.body=openid; })
onShow: function() { // console.log('App Show') wx.login({ // 解构 async success({code}){ // console.log(code) let openId = await request('/getOpenId',{code}); uni.setStorage({ key:"openId", data:openId }) } }) },
export default function(url,data={},method="GET"){ // console.log(uni.getSystemInfoSync()); // console.log(process.env.NODE_ENV) return new Promise((resolve,reject)=>{ uni.request({ url:baseURL+url, data, method, header:{ "token":uni.getStorageSync('openId') }, success(res){ resolve(res.data) // console.log(res) }, fail(error){ console.log(error) // reject(error) resolve(false) } }) }) }
十,在node中对唯一标识openId进行加密
购物车模块(对openId进行加密操作) 1)下载jwt加密包,npm install jsonwebtoken 2)使用方法: 加密:使用jwt.sign(需要加密的数据,提高解密难度的数据),可以获得加密之后的token 解密:使用jwt.verify(token,提高解密难度的数据),可以获得加密前的openId 3)将服务器的openId加密成token,返回给小程序客户端 4)小程序客户端获得token之后,将token存储至Storage中
jwt,github上搜索jsonwebtoken,数据加密 npm install jsonwebtoken
引入
const jwt = require('jsonwebtoken');
// 用于获取用户唯一标识OpenId router.get('/getOpenId',async (ctx,next)=>{ // console.log(ctx.query.code) let {code} = ctx.query; let appid="wx7ab0b2d5ff1862"; // AppSecret(小程序密钥) let appSecret="35d565a8f9984aa0d9816e9aaf509de"; let data; // 微信小程序提供的接口 let url= `https://api.weixin.qq.com/sns/jscode2session?appid=${appid}&secret=${appSecret}&js_code=${code}&grant_type=authorization_code` // 利用fly.js库发送请求 let result = await fly.get(url) // 转换成字符串 data=JSON.parse(result.data); // 获取openid,session_key let {openid,session_key} =data; // console.log(result) // 加密 jwt.sign(需要加密的数据,salt(盐)) // 盐->提高解密的难度(暴利破解的难度) // console.log(openid) let salt = "aotemannihao"; let token = jwt.sign(openid,salt); // console.log(token) //尝试解密token // 解密 jwt.verify(token,salt(盐)) let newOpenId = jwt.verify(token,salt); console.log(openid,newOpenId) ctx.body=token; })