Vue2积分商城项目
-
views
下面的文件只保留Home.vue
,其余删除,删除components/HelloWorld.vue
,并且Home.vue
中不再引入HelloWorld
组件。 -
-
将
router/index.js
中about
的路由注释掉。 -
删除
App.vue
中的less样式 -
VsCode用户片段提供
请在VScode中设置如下代码片段,以用于后面每一个案例的创建
{ "demo": { "prefix": "vue", "body": [ "<template>", "\t<div>", "\t\t$0", "\t</div>", "</template>", "", "<script>", "export default {", "\tdata () {", "\t\treturn {\n", " ", "\t\t}", "\t}", "}", "</script>", " ", "<style lang = \"less\" scoped>", "\t", "</style>" ], "description": "自定义的一个vue代码段" } }
-
“@/”路径提示配置
安装 Path Intellisense插件
打开设置 - 首选项 - 搜索
Path Intellisense
- 打开settings.json
,添加:
"path-intellisense.mappings": { "@": "${workspaceRoot}/src" }
在项目 package.json
所在同级目录下创建文件 jsconfig.json
:
{ "compilerOptions": { "target": "ES6", "module": "commonjs", "allowSyntheticDefaultImports": true, "baseUrl": "./", "paths": { "@/*": ["src/*"] } }, "exclude": [ "node_modules" ] }
最后重启打开即可
二、样式初始化
发现页面会有自带的间距,这是浏览器本身的默认样式,所以我们需要进行样式初始化,清除浏览器默认样式。
安装初始化样式库reset-css:
npm i reset-css 或者 yarn add reset-css
安装成功后在main.js中引入即可:
import "reset-css"
三、网站结构布置
在App.vue中设置好头部,导航和尾部组件:
<template> <div id="app"> <Tabbar></Tabbar> <Header></Header> <router-view/> <Footer></Footer> </div> </template> <script> import Header from '@/components/Header' import Tabbar from '@/components/Tabbar' import Footer from '@/components/Footer' export default { components:{ Header,Tabbar,Footer }, } </script>
在@/components目录下新建Header,Tabbar,Footer三个组件即可
四、网站数据请求模块
4.1、安装axios
作为一个网站前端,数据请求模块少不了。我们需要安装axios模块:
npm i axios
4.2、代理配置
我们对 vue.config.js
进行配置:
module.exports = { devServer: { port: 8080, proxy: { '/api': { target: "http://192.168.113.249:8081/cms", pathRewrite: { '^/api': '' } } } } }
由于配置文件修改了,这里一定要记得重新 npm run serve
!!
4.3、API与Request封装
在 src
下新建 request
目录 ,
在request
目录下新建 request.js
request.js
中:
// 书写请求拦截器 和 响应拦截器 import axios from "axios"; import store from "@/store" // 创建axios实例 (instance) const instance = axios.create({ baseURL: "/api", timeout: 5000 }) // 请求拦截器 instance.interceptors.request.use(config => { // 什么时候执行这个函数? 发送请求之前 // config是什么? 记录了本地请求的相关信息的一个对象 // 这个函数能用来做什么? 做一些请求之前的操作(例如:添加请求头,例如token) let token = localStorage.getItem("x-auth-token") if (token){ config.headers["x-auth-token"] = token } // console.log("执行了 请求拦截器的代码", config); return config }, err => { return Promise.reject(err) }); // 响应拦截器 instance.interceptors.response.use(res => { // 什么时候执行这个函数? 在接收到后端服务器的响应之后,进入到组件内部的then方法之前执行这里的代码 // res是什么? res是axios封装好的一个响应对象 // res.data就是后端服务器返回给我们的数据 // 这个函数能用来做什么? 做一些数据的统一处理 //统一处理响应码不为0的情况 let res_data = res.data let arr=[0,400,407] if (!arr.includes(res_data.code)) { //提示用户,操作失败 store.dispatch("toastStatus/asyncChanIsShowToast", { msg: res_data.message, type: "danger", }) return false } // console.log("执行了 响应拦截器 的代码", res); return res_data // return 后面的这个值被组件中的请求的then方法的res接收 }, err => { // 相当于在组件中,发送请求之后的catch方法 store.dispatch("toastStatus/asyncChanIsShowToast", { msg: err, type: "danger", }) return Promise.reject(err) }); export default instance
为了更好地管理我们的这些接口,我们把所有请求都抽取出来在一个api.js中
在
request目录下新建
api.js,api.js
中:
import request from './request' // 请求精品推荐数据 export const JingpinAPI = () => request.get('/products/recommend')
4.4、发起请求和获取数据
App.vue 中:
import {JingpinAPI} from "@/request/api" async created() { let jingPinRes=await JingPinAPI(); this.jingpinArr=jingPinRes.data.data.records; },
五、创建公共样式less文件
在assets在新建css目录,新建global.less文件
.wrap{ width: 1200px; margin: 0 auto; }
在main.js中全局引入
import "@/assets/css/global.less"
六、路由配置及导航项当前样式
在router/index.js中配置重定向,及几个导航路由:
import Vue from 'vue' import VueRouter from 'vue-router' import HomeView from '../views/HomeView.vue' import store from "@/store" const originalPush = VueRouter.prototype.push; VueRouter.prototype.push = function (location) { return originalPush.call(this, location).catch(err => { }) }; Vue.use(VueRouter) const routes = [ { path:"/", redirect:"/home" }, { path: '/home', name: 'home', component: HomeView }, // { // path: '/about', // name: 'about', // component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue') // } { path: '/goods', name: 'goods', component: () => import(/* webpackChunkName: "goods" */ '../views/GoodsView.vue') }, { path: '/user', name: 'user', redirect:"/user/cart", component: () => import(/* webpackChunkName: "user" */ '../views/UserView.vue'), children:[ { path: '/user/cart', name: 'cart', component: () => import(/* webpackChunkName: "cart" */ '../components/user/Cart.vue') }, ] }, { path: '/order', name: 'order', component: () => import(/* webpackChunkName: "order" */ '../views/OrderView.vue') }, { path: '/free', name: 'free', component: () => import(/* webpackChunkName: "free" */ '../views/FreeView.vue') }, { path: '/details', name: 'details', component: () => import(/* webpackChunkName: "details" */ '../views/DetailsView.vue') }, { path:"*", name: 'error', component: () => import(/* webpackChunkName: "error" */ '../views/ErrorView.vue') } ] const router = new VueRouter({ mode: 'history', base: process.env.BASE_URL, routes }) // 全局路由守卫 // router.beforeEach((to, from, next)=>{ // // // 什么执行这个函数? 路由跳转之前就会执行这里的代码 // // console.log("to为: ", to); // 要去到的路由对象 // // console.log("from为: ", from); //从哪个路由出发的路由对象 // // // 需求: 在进入到/user之前, 判断有没有token 有就放行, 没有就提示请先登录, 不放行 // if(to.path=="/user"){ // let token = localStorage.getItem("x-auth-token"); // if(token){ // // 有就放行 // next() // }else{ // // 提示 // store.dispatch("toastStatus/asyncChanIsShowToast",{ // msg:"请先登录!", // type:"warning" // }); // } // return // 需要写return 防止上面的next()执行完,还继续执行下面的next() // } // next() // 放行 // }) export default router
在View目录中新建对应组件。
处理导航项当前样式,在Tabbar.vue中
<div class="c"> <ul> <li :class="$route.path==='/home'?'active':''">首页</li> <li :class="$route.path==='/goods'?'active':''">全部商品</li> <li :class="$route.path==='/user'?'active':''">个人中心</li> <li :class="$route.path==='/order'?'active':''">我的订单</li> <li :class="$route.path==='/free'?'active':''">专属福利</li> </ul> </div> <style> ... .c ul{ width: 500px; display: flex; justify-content: space-between; color:#242B39; font-size: 16px; font-family: SourceHanSansSC-Medium; font-weight: 500; .active{ color:#0A328E; } } </style>
并且设置点击跳转路由:
<ul> <li @click="$router.push('/home')" :class="$route.path==='/home'?'active':''">首页</li> <li @click="$router.push('/goods')" :class="$route.path==='/goods'?'active':''">全部商品</li> <li @click="$router.push('/user')" :class="$route.path==='/user'?'active':''">个人中心</li> <li @click="$router.push('/order')" :class="$route.path==='/order'?'active':''">我的订单</li> <li @click="$router.push('/free')" :class="$route.path==='/free'?'active':''">专属福利</li> </ul>
七、登录模块(登录即自动注册)
7.1、模态窗口的书写
点击登录按钮,弹出模态窗口,如下图:
我们在cpmonents目录下新建一个Login.vue组件
然后在App.vue组件中引入,注册,使用。
<div id="app"> <Header></Header> <Nav></Nav> <router-view/> <Footer></Footer> <Login></Login> </div>
7.2、设置点击展示模态窗口
因为项目中可能有在各个组件中触发这个模态窗口的展示,所以控制模态框展示的变量可以放在Vuex中
在store中新建showModal目录,并在其中新建index.js:
export default{ namespaced:true,//记得加上命名空间 state: { isShowLoginModal:false // 用来表示是否展示登录模态窗口 }, mutations: { // 修改是否展示的值 chanIsShowLoginModal(state,payload){ state.isShowLoginModal = payload } }, actions: { }, }
在store.js中引入:
import Vue from 'vue' import Vuex from 'vuex' import showModal from "./showModal" Vue.use(Vuex) export default new Vuex.Store({ modules: { showModal } })
Login.vue组件中:
<template> <div class="modal" v-show="isShowLoginModal"> <div class="mask"></div> <div class="login-box"> </div> </div> </template> <script> import {mapState} from "vuex" export default { computed:{ ...mapState({ isShowLoginModal:state=>state.showModal.isShowLoginModal }) } } </script>
Header.vue组件中:
<template> <div class="header"> <div class="wrap header-wrap"> ... <div class="r"> <ul> ... <li class="login-btn" @click="chanIsShowLoginModal(true)">登录</li> </ul> </div> </div> </div> </template> <script> import {mapMutations} from "vuex" export default { data () {}, methods:{ ...mapMutations({ chanIsShowLoginModal:"showModal/chanIsShowLoginModal" }) } } </script>
7.3、点击关闭模态窗口
Login.vue中:
<template> <div class="modal" v-show="isShowLoginModal"> <!--点击遮罩层也可以关闭模态窗口--> <div class="mask" @click="chanIsShowLoginModal(false)"></div> <div class="login-box"> <div class="close" @click="chanIsShowLoginModal(false)"></div> </div> </div> </template> <script> import {mapState,mapMutations} from "vuex" export default { ... methods:{ ...mapMutations({ chanIsShowLoginModal:"showModal/chanIsShowLoginModal" }) } } </script> <style lang = "less" scoped> .modal{ ... .login-box{ ... .close{ width: 22px; height: 22px; background: url("../assets/img/close.png"); position: absolute; right: 60px; top: 16px; cursor: pointer; } } } </style>
7.4、点击标题栏的切换效果
Login.vue中:
<ul class="title"> <li @click="isShowForm=true" :class="{active:isShowForm}">手机号码登录</li> <li style="margin:0 10px">|</li> <li @click="isShowForm=false" :class="{active:!isShowForm}">微信扫码登录</li> </ul> <div class="body"> <div class="form" v-show="isShowForm"> 表单 </div> <div class="qrcode" v-show="!isShowForm"> 二维码 </div> </div> <script> export default { data () { return { isShowForm:true } } ... } </script> <style lang = "less" scoped> .modal{ ... .login-box{ ... .title{ display: flex; justify-content: center; width: 100%; padding-top: 50px; font-size:20px; color:#999; .active{ color:#333; } } .body{ width: 355px; margin:20px auto 0; height:200px; } } } </style>
八、拼图验证滑块
插件参考:https://gitee.com/monoplasty/vue-monoplasty-slide-verify
8.1、安装插件
npm install --save vue-monoplasty-slide-verify
或者
yarn add vue-monoplasty-slide-verify
8.2、main.js入口文件引中入
import SlideVerify from 'vue-monoplasty-slide-verify' // 拼图验证码 Vue.use(SlideVerify)
8.3、在组件中使用
<template> <slide-verify :l="42" :r="20" :w="362" :h="140" @success="onSuccess" @fail="onFail" @refresh="onRefresh" :style="{ width: '100%' }" class="slide-box" ref="slideBlock" :slider-text="msg"></slide-verify> </template> <script> export default { data() { return { msg: "向右滑动" }; }, methods: { // 拼图成功 onSuccess(times) { let ms = (times / 1000).toFixed(1); this.msg = "login success, 耗时 " + ms + "s"; }, // 拼图失败 onFail() { this.onRefresh(); // 重新刷新拼图 }, // 拼图刷新 onRefresh() { this.msg = "再试一次"; }, }, }; </script> <style lang="less" scoped> /deep/.slide-box { width: 100%; position: relative; box-sizing: border-box; canvas { position: absolute; left: 0; top: -120px; display: none; width: 100%; box-sizing: border-box; } .slide-verify-block{ width: 85px; height: 136px; } .slide-verify-refresh-icon { top: -120px; display: none; } &:hover { canvas { display: block; } .slide-verify-refresh-icon { display: block; } } } </style>
8.4、点击登录按钮,判断是否有进行拼图滑块验证
登录之前,我们需要验证用户是否有拼图验证过,有拼图验证过才可以登录。
我们以msg文字内容来判断是否有进行拼图滑块验证
<div class="mb20 btn" @click="submitFn"> 登录 </div> ... <script> ... methods:{ ... // 点击登录按钮 submitFn() { // 以msg文字内容来判断是否有进行拼图滑块验证 if (this.msg == "再试一次" || this.msg == "向右滑动") { alert("请滑动拼图"); return } alert("拼图滑块验证通过,可以执行登录了") }, } </script>
九、点击获取验证码按钮的逻辑
9.1、逻辑分析
可以正常获取验证码的前提是:手机号格式正确
所以,点击获取验证码的逻辑如下:
1、如果校验手机号格式不正确,则return
2、滑块拼图验证不通过,则return
3、验证成功后,发起请求,获取验证码成功,则进行倒计时
【百度】结合运营商之后的手机号码的正则:
/^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$/
9.2、点击获取验证码判断手机号格式
<div class="btn checkcode-btn" @click="getCode">获取验证码</div> ... <script> getCode(){ // 1、验证手机号是否正确 if(!/^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$/.test(this.phoneNum)){ alert("请输入正确的手机号"); this.$refs.phone.focus(); return } alert("手机号格式正确"); // 2、进行滑块验证 // 3、验证成功后,发起请求,获取验证码成功,则进行倒计时,并展示秒数 }, </script>
9.3、倒计时及其展示
<div class="btn checkcode-btn" @click="getCode"> <span v-show="!isShowCount">获取验证码</span> <span v-show="isShowCount">{{count}} s</span> </div> <script> methods:{ countdown(){ // 计时的方法 // 倒计时,实际上就是每隔1秒,count减去1 // 每次点击先让count为60 this.count=60; let timer = null; timer = setInterval(()=>{ this.count-- if(this.count===0){ // 清除定时器 clearInterval(timer) } },1000); }, getCode(){ // 1、验证手机号是否正确 /* if(!/^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$/.test(this.phoneNum)){ alert("请输入正确的手机号"); this.$refs.phone.focus(); return } */ // 2、进行滑块验证 if (this.msg == "再试一次" || this.msg == "向右滑动") { alert("请先进行滑块验证"); return } // 3、验证成功后,发起请求,获取验证码成功,则进行倒计时,并展示秒数 // 这里先展示秒数 this.countdown(); this.isShowCount=true; }, } </script>
9.4、连续点击倒计时bug
此时连续点击倒计时会有bug,数字越跳越快,主要是重复开启倒计时造成的。
其实我们只需要把事件给到 "获取验证码" 所在的span,就可以解决
<div class="btn checkcode-btn"> <span v-show="!isShowCount" @click="getCode">获取验证码</span> <span v-show="isShowCount">{{count}} s</span> </div> <script> data () { return { // 是否展示表单的布尔值 isShowForm:true, // 拼图滑块的文字 msg: "向右滑动", // 用户手机号 phoneNum:"", // 最大的计时时间 countMax:60, // 倒计时时间,每秒变化的那个数字 count:0, // 是否展示秒数所在盒子 isShowCount:false } }, ... ... countdown(){ // 计时的方法 // 倒计时,实际上就是每隔1秒,count减去1 let timer = null; this.count = this.countMax; timer = setInterval(()=>{ this.count-- if(this.count===0){ // 清除定时器 clearInterval(timer); } },1000); } </script>
9.5、抽取工具函数(优化)
事实上,验证手机号本身是一个工具函数,和本身逻辑有关但是具体代码不需要在组件中体现。真实项目场景往往是放在工具函数的文件中。
src目录下新建utils文件夹,在里面新建index.js文件:
export const validateTelephoneNumber = value =>{ let reg = /^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$/ return reg.test(value) }
在Login.vue组件中:
import {validateTelephoneNumber} from "@/utils" ... ... getCode(){ // 1、验证手机号是否正确 if(!validateTelephoneNumber(this.phoneNum)){ alert("请输入正确的手机号"); this.$refs.phone.focus(); return } // 2、进行滑块验证 ... // 3、验证成功后,发起请求,获取验证码成功,则进行倒计时,并展示秒数 ... },
9.6、发起获取验证码请求
接口: /sendSMS
api.js文件中
// 发送短信验证码请求 export const SendSMSAPI = params => request.post("/sendSMS",params);
Login.vue组件中:
// 3、验证成功后,发起请求,获取验证码成功,则进行倒计时,并展示秒数 SendSMSAPI({ phone:this.phoneNum.trim() }).then(res=>{ this.countdown(); this.isShowCount=true; console.log(res); })
发现返回的数据报如下错误:
其实接口上有需要我们修改请求头Content-Type字段,并使用qs.stringnify进行格式转换:
所以我们需要在请求拦截器加上:
instance.interceptors.request.use(config=>{ if (config.url === "/sendSMS" || config.url === "/wechatUsers/PCLogin") { config.headers["Content-Type"] = "application/x-www-form-urlencoded"; } return config },err=>{ return Promise.reject(err) })
并且安装qs模块:
npm i qs
api.js中:
import qs from "qs" // 发送短信验证码请求 export const SendSMSAPI = params => request.post("/sendSMS",qs.stringify(params));
再次到浏览器进行测试
看见如下响应则为发送成功。
后端对每一个手机号做了限制,如果看到如下图,说明已经达到最大可发送短信条数,请隔数个小时后再尝试:
9.7、请求成功回调函数的完善
// 3、验证成功后,发起请求,获取验证码成功,则进行倒计时,并展示秒数 SendSMSAPI({ phone:this.phoneNum.trim() }).then(res=>{ if(res.code===0){ this.countdown(); this.isShowCount=true; console.log(res); }else{ //获取短信验证码失败 alert(res.message) } }).catch(err=>{ // 发送请求失败 alert("请重新发送") })
十、手机号码登录逻辑分析
手机号码登录逻辑分析:
1、手机号码格式是否正确
2、拼图滑块验证通过
3、验证码是否为空(注意,这里我们前端并没有拿到发送的手机验证码,所以只能判空)
4、发起登录请求
10.1、抽取前两个验证的代码
前两个验证我们前面已经做了,可以直接封装成函数
Login.vue中:
toVerify(){ // 1、验证手机号是否正确 if(!validateTelephoneNumber(this.phoneNum)){ alert("请输入正确的手机号"); this.$refs.phone.focus(); return } // 2、进行滑块验证 if (this.msg == "再试一次" || this.msg == "向右滑动") { alert("请先进行滑块验证"); return } }, getCode(){ this.toVerify(); //3、发起请求 ... }
如果只是按照上面进行抽取,则会有验证不通过还发起请求的bug
所以,this.toVerify();应换成:
toVerify(){ // 1、验证手机号是否正确 if(!validateTelephoneNumber(this.phoneNum)){ alert("请输入正确的手机号"); this.$refs.phone.focus(); return } // 2、进行滑块验证 if (this.msg == "再试一次" || this.msg == "向右滑动") { alert("请先进行滑块验证"); return } // 【!!!!!!!!】 return true // 【!!!!!!!!】 }, getCode(){ if(!this.toVerify()){ return }; //3、验证成功后,发起请求 ... }
10.2、发起登录请求
api.js中:
// 手机号登录请求 export const PhoneLoginAPI = params => request.post("/phoneRegin",qs.stringify(params));
Login.vue中:
import { SendSMSAPI, PhoneLoginAPI } from "@/request/api"; ... // 点击登录按钮 submitFn() { if(!this.toVerify()){ return }; // 验证码是否为空 if (this.code.trim() === "") { alert("请输入验证码再进行登录"); return; } // 发起登录请求 PhoneLoginAPI({ // 先根据后端给到的测试账号和密码做登录。 // 但如果真正场景要传的是短信验证码和手机号 phone:"13800138001", password:"qwerty567" }).then(res=>{ // 登录成功 console.log(res); }) },
看到上图就表示登录成功。
10.3、登录成功后的逻辑
登录成功后,我们需要做什么?
1、提示登录成功
2、保存token值到localStorage
3、隐藏登录模态窗口
【!!!】4、登录状态的切换
所以在回调函数中:
// 发起登录请求 PhoneLoginAPI({ phone:"13800138001", password:"qwerty567" }).then(res=>{ if(res.code===0){ // 1、提示登录成功 alert("登录成功"); // 2、存储token localStorage.setItem("x-auth-token", res["x-auth-token"]); // 3、隐藏登录模态窗口 this.chanIsShowLoginModal(false); // 【!!!】4、登录状态的切换 } })
10.4、购物车按钮展示(登录状态)分析
因为该购物车按钮需要依靠 有没有登录这个状态的值 来进行展示,并且,这个值会在Login.vue这个文件中进行修改,所以,我们把有没有登录这个状态值放在Vuex中:
store目录中新建文件夹loginStatus,中新建index.js:
export default{ namespaced:true, state: { isLogined:localStorage.getItem("x-auth-token")?true:false // 用来表示是否登录的登录状态值 }, mutations: { chanIsLogined(state,payload){ console.log("执行了chanIsLogined"); state.isLogined = payload } }, actions: { }, }
记得在store/index.js中引入。
在TopBar.vue中
<div class="cart_btn" v-if="isLogined"> <img src="../assets/img/cart.png" alt="" width="20" /> <span>购物车</span> <b>{{cartTotal}}</b> </div> <li class="login-btn" v-else @click="chanIsShowLoginModal(true)">登录</li> ... <script> import {mapMutations,mapState} from "vuex" ... computed:{ ...mapState({ isLogined:state=>state.loginStatus.isLogined }) }, </script>
最后在登录的回调函数中进行修改这个值。即在Login.vue中:
...mapMutations({ chanIsShowLoginModal:"showModal/chanIsShowLoginModal", chanIsLogined:"loginStatus/chanIsLogined" }), ... ... PhoneLoginAPI({ ... }).then(res=>{ if(res.code===0){ // 提示登录成功 ... // 存储token ... // 隐藏登录模态窗口 ... // 登录状态值的切换 this.chanIsLogined(true) } })
十一、提示组件的封装
11.1、icon图标的使用
11.1.1、在全局中引入
效果图中icon的链接:
https://at.alicdn.com/t/font_2730880_ylrio3ahhx.css
11.1.2、具体图标名称
图标名称 | 图标类名 |
---|---|
YDUI-复选框(选中) | icon-yduifuxuankuangxuanzhong |
YDUI-复选框 | icon-yduifuxuankuang |
loading | icon-loading |
toast-失败_画板 1 | icon-toast-shibai_huaban |
toast-警告 | icon-toast-jinggao |
toast _成功 | icon-toast_chenggong |
11.1.3、在组件中使用
<i class="iconfont icon-loading"></i>
11.2、Toast组件的初步封装与使用
https://at.alicdn.com/t/font_2730880_ylrio3ahhx.css
iconfont的样式链接内容粘贴到src/assets/css/public.less中,
components目录下新建Toast.vue
<template> <div class="toast"> <i class="iconfont icon-toast-shibai_huaban"></i> <span>提示内容</span> </div> </template> <script> export default { data () { return { } } } </script> <style lang = "less" scoped> .toast{ position: fixed; padding: 10px 20px; display: flex; justify-content: center; align-items: center; background: #fff; left: 50%; top: 0; transform: translateX(-50%); border-radius: 10px; .iconfont{ margin-right: 10px; } .icon-toast-shibai_huaban{ color: red; } .icon-toast_chenggong{ color: green; } .icon-toast-jinggao{ color: orange; } } </style>
App.vue中引入注册使用即可 。
11.3、Toast组件展示
Toast组件展示与否最终可以在各个组件中调用,所以放在Vuex中。
然后在App中:
<Toast v-show="isShowToast"></Toast> <script> ... import {mapState} from "vuex" export default { ... computed:{ ...mapState({ isShowToast:state=>state.showToast.isShowToast }) } } </script>
接下来尝试写个事件看是否能触发显示和隐藏切换
在TopBar组件中,先找头像尝试点击触发展示:
<img @click="showToastFn" src="../assets/img/userImg.f8bbec5e.png" width="26" alt=""> <script> methods:{ ...mapMutations({ chanIsShowLoginModal:"showModal/chanIsShowLoginModal", chanIsShowToast:"showToast/chanIsShowToast" }), showToastFn(){ this.chanIsShowToast(true) } } </script>
11.4、Toast组件的进场离场效果
Vue提供了transition组件,配合css3可以用来做进场离场效果:https://cn.vuejs.org/v2/guide/transitions.html
App.vue中添加:
<template> <div id="app"> <transition name="slide"> <Toast v-show="isShowToast"></Toast> </transition> ... ... ... </div> </template> <style lang="less"> /* 入场的起始状态 = 离场的结束状态 */ .slide-enter, .slide-leave-to{ opacity: 0; } .slide-enter-active, .slide-leave-active{ transition: opacity .3s linear; } .slide-enter-to, .slide-leave{ opacity: 1; } </style>
在TobBar.vue组件中:
methods:{ ...mapMutations({ chanIsShowLoginModal:"showModal/chanIsShowLoginModal", chanIsShowToast:"showToast/chanIsShowToast" }), showToastFn(){ this.chanIsShowToast(true); setTimeout(()=>{ this.chanIsShowToast(false); },1500) } }
记得把Toast组件中的opacity属性去掉
11.5、封装Toast的属性
一个完整的Toast组件最好需要有展示、颜色、类型三种属性:
在vuex中,补充:
export default{ namespaced:true, state: { // 表示是否展示提示 isShowToast:false, // toast的内容 toastMsg: "默认内容", // toast的类型(success, danger, info) toastType: "success" }, mutations: { chanIsShowToast(state,payload){ console.log(payload); state.isShowToast = payload.isShow; if(payload.isShow){ state.toastMsg = payload.msg; state.toastType = payload.type; } } }, actions: { }, }
Toast.vue组件:
<template> <div class="toast"> <!-- <i class="iconfont icon-toast-shibai_huaban"></i> <span>提示内容</span> --> <i :class="toastType=='success' ? 'iconfont icon-toast_chenggong' : (toastType=='danger' ? 'iconfont icon-toast-shibai_huaban' : 'iconfont icon-toast-jinggao')" ></i> <span>{{toastMsg}}</span> </div> </template> <script> import {mapState} from "vuex" export default { data () { return { } }, computed:{ ...mapState({ toastMsg:state=>state.showToast.toastMsg, toastType:state=>state.showToast.toastType, }) } } </script>
最后在TopBar.vue组件中,调用的时候传入对象:
showToastFn(){ this.chanIsShowToast({ isShow:true, msg:"请先登录", type:"danger" }); setTimeout(()=>{ this.chanIsShowToast({ isShow:false, }); },1500) }
11.6、Toast组件自动关闭的处理
Toast组件应该具备自动关闭的功能,而不是每次调用都要写一段定时器代码来关闭。
即TopBar.vue组件中去掉setTimeout方法,在TopBar.vue中:
import {mapMutations,mapState,mapActions} from "vuex" methods:{ ...mapMutations({ chanIsShowLoginModal:"showModal/chanIsShowLoginModal" }), ...mapActions({ asyncIsShowToast:"showToast/asyncIsShowToast" }), showToastFn(){ this.asyncIsShowToast({ isShow:true, msg:"请先登录", type:"danger" }); /* setTimeout(()=>{ this.chanIsShowToast({ isShow:false, }); },1500) */ } }
在Vuex中书写actions:
actions: { asyncIsShowToast(context,payload){ context.commit("chanIsShowToast",payload) setTimeout(()=>{ context.commit("chanIsShowToast",{ isShow:false }) },2000) } },
十二、微信登录二维码的获取与展示
12.1、获取微信二维码
在 public/ndex.html 的head标签中引入:
<script src="https://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js"></script>
把Login.vue文件中展示二维码图片的盒子上添加 id=“weixin” :
<div id="weixin" class="qrcode" v-show="!isShowForm">
二维码
</div>
在api.js中:
// 微信登录(这个接口必须用qs对数据进行格式化) export const WeixinLoginApi = (params) => request.post(`/wechatUsers/PCLogin`, qs.stringify(params));
在点击切换到微信登录的函数中:
weixinClick(){// 点击切换微信扫码登录这一项,并向微信扫码登录 this.isShowForm=false; // 申请微信登录二维码 let _this = this; new WxLogin({ id: "weixin", appid: "wx67cfaf9e3ad31a0d", // 这个appid要填死 scope: "snsapi_login", // 扫码成功后重定向的接口 redirect_uri: "https://sc.wolfcode.cn/cms/wechatUsers/shop/PC", // state填写编码后的url state: encodeURIComponent(window.btoa("http://127.0.0.1:8080" + _this.$route.path)), // 调用样式文件 href: "", }); },
用户扫码完成后,当前页面刷新,并且页面url会拼接微信后台发给我们的微信code.
在TopBar.vue中:(拿页面url上面的code向后端发起请求换取token)
created() { // 扫码的时候做两件事情: 浏览器刷新(created()执行), 浏览器拼接上 ?code= // 重载执行了created setTimeout(async () => { // console.log(this.$route.query.code); let mycode = this.$route.query.code; if (mycode) { // 发起微信扫码登录请求 let res = await WeixinLoginAPI({ code: mycode, }); console.log(res); if (res.code == 0) { // 登录成功的逻辑 this.asyncChanIsShowToast({ msg: "登录成功!", type: "success", }); // 保存token到localStorage localStorage.setItem("x-auth-token", res["x-auth-token"]); // 登录状态的切换 (用户的头像,昵称,购物车按钮,数量等) this.chanIsLogined(true); // 清除浏览器上的code this.$router.push(this.$route.path); //!!! 登录成功也要获取用户信息 this.asyncChanUserInfo() } else if (res.code == 400) { // code过期的逻辑: // 1 提示用户进行 请重新进行扫码登录 this.asyncChanIsShowToast({ msg: "请重新扫码登录!", type: "warning", }); // 2 弹出登录框(打开登录的模态窗口) this.chanIsShowLoginModal(true); } else if (res.code == 407) { // 返回407说明: 用户扫码的这个微信, 在这个网站上没有绑定手机号 // 我们需要: 让用户绑定手机到扫码的这个微信上 // 1 弹出提示框, 提示用户, 手机号登录进行绑定微信号 this.asyncChanIsShowToast({ msg: "请进行手机号登录绑定扫码的微信!", type: "danger", }); // 2 弹出登录框(打开登录的模态窗口) this.chanIsShowLoginModal(true); // 3 保存uuid(待会用户输入手机点击登录按钮的时候,发送登录请求需要携带uuid) localStorage.setItem("uuid", res.uuid); } } else { // 没有code的情况 // 判断有没有token,来改变用户的登录状态 let mytoken = localStorage.getItem("x-auth-token"); // if(mytoken){ // this.chanIsLogined(true); // }else{ // this.chanIsLogined(false); // } this.chanIsLogined(Boolean(mytoken)); if(mytoken){ // 请求登录的用户的信息 this.asyncChanUserInfo() }else{ // 初始化用户信息 // 修改vuex中的cartTotal和userInfo属性变回初始值 this.initUserInfo() } } }, 100); },
12.2、微信二维码样式调整
把今天其他文件夹中的wxLoginStyle文件夹放到utils文件夹中,然后在这个wxLoginStyle目录下用node执行js文件:
node data-url.js
得到:
data:text/css;base64,Lyogd3hsb2dpbi5jc3MgKi8NCi5pbXBvd2VyQm94IC50aXRsZSwgLmltcG93ZXJCb3ggLmluZm97DQogIGRpc3BsYXk6IG5vbmU7DQp9DQoNCi5pbXBvd2VyQm94IC5xcmNvZGV7DQogIG1hcmdpbi10b3A6IDIwcHg7DQp9
把它填到上面的href属性,隐藏头部尾部。
最后调整页面iframe外层盒子的样式,使二维码居中
#weixin{ /* background-color: #fcf; */ display: flex; justify-content: center; margin-top: -20px; }
十三、组件重载
之前做的利用路由监听完成切换路由就更新用户登录状态值。
这个功能,也可以使用组件重载的方式来完成。即每次切换路由,我们都让TopBar.vue重新加载一次。
首先,现在TopBar的created函数中补充更新用户状态值的代码(此时注释掉前面的watch):
if(mycode){ ... }else{ // !!!如果不是微信登录进来的,就会执行这里的代码 let mytoken = localStorage.getItem("x-auth-token"); this.chanIsLogined(Boolean(mytoken)); }
如何进行组件重载?利用key属性。
先删除TopBar.vue中的watch。
然后来到App.vue组件,给调用TopBar.vue的地方添加key属性:
<template> ... <!-- 顶部 --> <TopBar :key="topBarKeyValue"></TopBar> ... </template> <script> data() { return { topBarKeyValue:1 }; }, ... watch: { // 监听路由的变化 "$route.path": { handler(newVal, oldVal){ // console.log(newVal, oldVal) if(newVal !== oldVal){ // key属性的值一变化,就会做组件重载,从而执行created函数 console.log("组件重载!!!!"); this.topBarKeyValue++; } } } }, </script>
十四、获取登录用户信息,请求头携带token
14.1、请求头携带token
在登录功能中,目前我们已经完成了保存token到本地存储中。
在真正项目中,只要我们本地存储中有token,在请求的时候都会带上这个token值在每一个请求头中,不管这个请求需不需token,都会带上。
接下来,我们就需要在请求拦截器中判断token,携带token:
src/request/request.js中:
instance.interceptors.request.use(config => { const token = localStorage.getItem("x-auth-token"); if (token) { // 判断是否存在token,如果存在的话,则每个请求的请求头上都加上token config.headers["x-auth-token"] = token; } return config }, err => { return Promise.reject(err) })
14.2、获取用户登录信息
接口文档:http://www.docway.net/project/1h9xcTeAZzV/1hG2hFlipBQ?st=1iUU09vKhMm&sid=1iUU09vKhMm
在api.js中:
// 获取登录用户信息 export const UserProfilesAPI = () => request.get("/shop/userProfiles");
在TopBar组件中:
import {UserProfilesAPI} from "@/request/api" setUserLoginstatus(){ // 微信登录第二步:临时票据code换取token let mycode = this.$route.query.code; if (mycode) { ... }else{ // 没有code,说明没有扫码, 或者 用户已经登录了,不用扫码了 // 判断有没有token,设置登录状态 (因为TopBar上面的信息,是需要靠 有没有登录来去展示的) let mytoken = localStorage.getItem("x-auth-token"); this.chanIsLogined(Boolean(mytoken)); if(mytoken){ // 请求并渲染用户信息 UserProfilesAPI().then(res=>{ // 打印用户信息 console.log(res); }); }else{ // 设置回默认的用户信息 } } },
十五、用户信息渲染
但是在项目中,任意组件都可以获取这个用户信息,比如:TopBar.vue,Login.vue等,所以用户信息可以放在vuex中:
store中新建userInfo文件夹,新建index.js
import {UserProfilesAPI} from "@/request/api" export default { namespaced: true, state: { // 购物车数量 cartTotal: 0, // 用户信息 userInfo:{ headImg:require("../../assets/img/userImg.f8bbec5e.png"), nickName:"--", coin:"--" } }, mutations: { updateUserInfo(state,payload){ console.log("payload为:",payload); state.cartTotal = payload.cartTotal; state.userInfo = payload.userInfo; } }, actions: { asyncUpdateUserInfo(context){ UserProfilesAPI().then(res=>{ // 打印用户信息 context.commit("updateUserInfo",res.data) }); } }, }
在TopBar.vue组件中:
<template> ... <li> <img @click="clickAvatar" class="avatar" width="26" :src="userInfo.headImg" alt="" />用户名:{{userInfo.nickName}} </li> <li>我的鸡腿:{{userInfo.coin}}</li> <li>获取鸡腿</li> <li>叩丁狼官网</li> <li class="cart-btn btn" v-show="isLogined"> <img src="../assets/img/cart.png" alt="" /> <span>购物车</span> <b>{{ cartTotal }}</b> </li> ... </template> <script> setUserLoginstatus(){ setUserLoginstatus(){ // 微信登录第二步:临时票据code换取token let mycode = this.$route.query.code; if (mycode) { // 有code才去换取token WeixinLoginApi({ code: mycode, }).then((res) => { console.log(res); if (res.code === 0) { // 登录成功 // 1、提示用户登录成功 // 2、保存token值 // 3、改变登录状态 // 4、清除浏览器地址栏上的code // 5、获取登录用户信息 this.asyncUpdateUserInfo(); }else{ this.asyncChanToastState({ msg:res.message, type:"danger" }); } }) }else{ // 如果没有code,需要更新用户登录状态 // 没有code说明,用户没扫码 // 说明用户已经登录的不用扫码,或者用户没有登录没扫码, // 判断用户能否拿到token来做出登录状态的更新 let mytoken = localStorage.getItem("x-auth-token"); this.chanIsLogined(Boolean(mytoken)); // 获取登录用户信息 if(mytoken){ this.asyncUpdateUserInfo(); } } }, } </script>
在Login.vue组件中登录成功也是异步更新用户信息:
import { mapMutations,mapActions } from "vuex"; ... ...mapActions({ asyncUpdateUserInfo:"userInfo/asyncUpdateUserInfo" }), toLogin() { // 1&&2、前两个验证已经完成 // 3 判断验证码是否为空 // 去做登录 LoginAPI({ ... }).then((res) => { console.log(res); if(res.code==0){ // 1、提示登录成功 // 2、保存token值,本地存储 // 3、隐藏登录框 // 【!!】4登录状态的切换 // 5、更新用户信息 this.asyncUpdateUserInfo(); } }); },
注:如果此时用户头像没有出来,在public文件夹中的index.html文件的head标签里,添加这个标签即可
<!-- 防止服务器检查防盗用链接 --> <meta name="referrer" content="no-referrer" />
十六、删除token后的用户信息初始化
在删除token后,切换路由的时候,用户信息需要还原为默认值
TopBar.vue中:
setUserLoginState() { if(mycode){ ... }else { ... if (mytoken) { ... } else { // 初始化用户数据 this.initUserInfo(); } }
十七、重复点击同一个路由出现的报错问题解决
在现有版本的vue-router中,重复点击同一个路由会出现报错。
解决方案如下:
方案1、vue-router降级处理(但不推荐)
npm i vue-router@3.0.7
方案2、直接在push方法最后添加异常捕获,例如:
<van-search v-model="SearchVal" shape="round" placeholder="请输入搜索关键词" disabled @click="$router.push('/home/searchPopup').catch(err=>{})"/>
方案3、直接修改原型方法push(推荐)
// 把这段代码直接粘贴到router/index.js中的Vue.use(VueRouter)之前 const originalPush = VueRouter.prototype.push; VueRouter.prototype.push = function(location) { return originalPush.call(this, location).catch(err => {}) };
十八、抽取图片基本路径
通过 Vue.prototype.变量名=值
,定义的变量,在组件内部可以直接 this.变量名
来获取。
有人称它为全局变量,但不是真正意义上的全局变量。跟Vuex也不一样。
它相当于在每一个组件中都定义一个组件内部变量
在main.js中:
// 定义“全局变量”(相当于在每一个组件中都定义一个组件内部变量imgBaseUrl) Vue.prototype.imgBaseUrl = "https://sc.wolfcode.cn";
在Details.vue和List.vue组件中,图片基本路径的处理均调整为:
<img :src="imgBaseUrl+item.img" alt="">
十九、导航守卫(导航拦截、路由拦截)
文档地址:
路由拦截(导航守卫:前置导航守卫和后置导航守卫) 前置导航守卫有三个参数 to 表示即将进入的路由 from 表示即将离开的路由 next() 表示执行进入这个路由
19.1、全局导航守卫
在router/index.js中:
// 全局导航守卫 router.beforeEach((to, from, next)=>{ // 有token就表示已经登录 // 想要进入个人中心页面,必须有登录标识token // console.log('to:', to) // console.log('from:', from) if(to.path=='/user'){ let token = localStorage.getItem('x-auth-token') // 此时必须要有token if(token){ next(); // next()去到to所对应的路由界面 }else{ // 提示没有登录 store.dispatch("showToast/asyncChanIsShowToast",{ msg: "你还没有登录!", type: "danger", }) } return; // 需要些return,防止执行完上面的next(),还继续执行下面的next() } // 如果不是去往个人中心的路由,则直接通过守卫,去到to所对应的路由界面 next() }) export default router
19.2、组件内部导航守卫
全局导航守卫是每一次改变路由都会触发的,然而,目前我们只需要到个人中心页面才触发,所以我们在组件内部书写导航守卫:
User.vue组件中
import store from "@/store" ... beforeRouteEnter(to, from, next) { // 在渲染该组件的对应路由被 confirm 前调用 // 不!能!获取组件实例 `this` // 因为当守卫执行前,组件实例还没被创建 let token = localStorage.getItem("x-auth-token"); if(token){ next() }else{ // 提示没有登录 store.dispatch("showToast/asyncChanIsShowToast",{ msg: "你还没有登录!", type: "danger", }); } },
二十、404处理
新建Error.vue组件:
<template> <div class="wrap"><img width="100%" src="../assets/img/404.94e7c552.jpg" alt=""></div> </template>
在路由表中配置剩余地址对应Error组件:
, { path: '*', name: 'Error', component: () => import(/* webpackChunkName: "error" */ '../views/Error.vue') }
二十一、全部商品中,滚动到底部加载更多
Goods.vue中:
<p style="text-align: center; margin-top: 20px">
正在加载... ...
</p>
把三个函数(带兼容性的写法)写在工具类utils文件夹index.js中:
//获取滚动条当前的位置 function getScrollTop() { var scrollTop = 0; if(document.documentElement && document.documentElement.scrollTop) { scrollTop = document.documentElement.scrollTop; } else if(document.body) { scrollTop = document.body.scrollTop; } return scrollTop; } //获取当前可视范围的高度 function getClientHeight() { var clientHeight = 0; if(document.body.clientHeight && document.documentElement.clientHeight) { clientHeight = Math.min(document.body.clientHeight, document.documentElement.clientHeight); } else { clientHeight = Math.max(document.body.clientHeight, document.documentElement.clientHeight); } return clientHeight; } //获取文档完整的高度 function getScrollHeight() { return Math.max(document.body.scrollHeight, document.documentElement.scrollHeight); }
Goods.vue中:
<script> import {getScrollTop,getClientHeight,getScrollHeight} from "@/utils" ... data(){ return{ // 产品列表 goodsList: [], // 用来展示的产品列表 goodsListShow: [], // 默认展示第一页 page:1, // 每页8条 size:8, // false 表示没有正在加载 isLoading: false, //是否已经到低 isReachBottom:false, } }, ... getGoodList() { GoodListAPI({ ... }).then((res) => { ... // 在请求到数据之后,先展示前8条 this.goodsListShow=this.goodsList.filter((item,index)=>index<8) }); }, scrollFn() { // 滚动就执行这里的代码频繁触发事件 console.log(getClientHeight() + getScrollTop() == getScrollHeight() + 1); // console.log("页面正在滚动"); // 如果滚动到底部的时候, // if (到底部了) { // if (窗口高度+scrollTop>=页面文档高度-20) { if (getClientHeight() + getScrollTop() >= getScrollHeight() - 20) { // 需要this.isLoading为false才能进行加载 if (!this.isLoading) { // this.isLoading避免了重复触发这个到底了加载数据事件 this.page++; this.isLoading = true; setTimeout(() => { // 往goodsListShow这个数组去push下一页的数据 // 从goodsList数组中去 this.page页的数据 push到goodsListShow for (var i = this.size * (this.page - 1);i < this.size * this.page;i++) { //this.goodsList[i]必须有这个值,才能push到展示的数组里面去 this.goodsList[i] ? this.goodsListShow.push(this.goodsList[i]) : ""; } this.isLoading = false; }, 500); } } }, }, mounted() { // 监听滚动 window.addEventListener("scroll", this.scrollFn); }, beforeDestroy() { // 取消监听 window.removeEventListener("scroll", this.scrollFn); }, </script>
处理是否 已经到底部 isReachBottom
结构完善:
<p style="text-align: center; margin-top: 20px"> {{ isReachBottom ? "已经没有数据了" : "正在加载... ..." }} </p> <script> ... // 定义是不是已经没有数据了 isReachBottom: false, ... if (getClientHeight() + getScrollTop() >= getScrollHeight() - 20) { if (this.goodsListShow.length >= this.goodsList.length) { // 没有数据了 this.isReachBottom = true; return; } </script>
最后解决选项切换时候的bug
async goodsSearch(){ ... ... // 初始化数据 this.isReachBottom = false; this.page = 1; // 判断是不是已经没有数据了 if (this.goodsListShow.length >= this.goodsList.length) { // 每次请求到数据,把数据把页数和是否到底部初始化一下 this.isReachBottom = true; } }
二十二、跨域配置
我们对 vue.config.js
进行配置:
module.exports = { devServer: { proxy: { '/api': { target: "http://kumanxuan1.f3322.net:8881/cms", pathRewrite: { '^/api': '' } } } } }
request.js中:
const instance = axios.create({ baseURL: "/api", timeout: 5000 })
记得配置完需要重启服务器!!
浏览器请求案例:
二十三、项目环境变量配置
项目目录下新建两个文件,分别是开发环境和生产环境下的两个不同配置
.env.dev
NODE_ENV=development VUE_APP_BASE_URL=http://172.16.xxxxx VUE_APP_STATE_URL=http://127.0.0.1:8080
.env.prod
NODE_ENV = production VUE_APP_BASE_URL = http://xxxxxx VUE_APP_STATE_URL="后端给的地址"
在package.json中修改启动命令:
"serve": "vue-cli-service serve --open --mode dev",
"servepro": "vue-cli-service serve --open --mode prod",
在vue.config.js中换成:
'/api': { target: process.env.VUE_APP_BASE_URL, pathRewrite: { '^/api': '' } }
二十四、项目总结
24.1、项目介绍
《叩丁严选》是一个由vue-cli搭建的PC端SPA商城,该商城主要涉及登录注册、商品列表、商品详情、个人中心、购物车及商品检索等主体功能。该项目主要用于平台用户参与积分兑换商品,是一个中大型的PC端商城项目。
24.2、项目技术点
-
使用vue-cli搭建项目,并结合蓝湖+PS进行页面切图,实现对设计稿的高保真还原;
-
使用axios进行数据请求,并对其进行请求拦截器响应拦截器封装;
-
封装所有请求的api,统一管理项目所有的请求路径
-
鉴权,认证机制采用手机+验证码、手机+密码及微信扫码登录认证,其中微信扫码登录结合环境变量,调用后端接口实现平台切换验证;
-
使用localStorage对token进行存储;
-
使用原生JS在组件mounted中监听滚动,并实现向下滚动加载更多;
-
使用组件内导航守卫对每个进入个人中心页的路由进行拦截,判断路径后保证有token方能进入该路由;
-
使用路由监听解决路由跳转而页面不跳转的问题;
-