一,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;
})
9.3,在app.vue中的onShow函数中,获取到登录唯一凭证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;
})