这里写目录标题
- 1. 注册页面
- 1.1 注册/登录页面——接口请求
- 1.2 Vue开发中Element UI的样式穿透
- 1.2.1 ::v-deep的使用
- 1.2.2 elementUI Dialog内容区域显示滚动条
- 1.3 注册页面——步骤条和表单联动 steps+form
- 1.4 注册页面——滑动拼图验证
- 1.5 注册页面——element-ui组件Popover 弹出框 条件控制显示和隐藏 滑块验证码
- 1.6 注册页面——获取手机验证码,进行手机号校验、验证码CD60秒
- 1.7 注册页面——element-ui组件el-autocomplete带输入建议 自动补全后缀(邮箱地址)
- 1.8 注册页面——用户、密码强弱校验+密码自定义规则校验、提交验证
- 2. 登录页面
- 2.1 登录页面——接口
- 2.2 登录——token
- 2.3 登录页面——退出登录
- 2.4 登录页面——全局导航守卫
- 2.5 路由独享守卫
- 2.6 组件导航守卫
- 2.7 全局封装API
- 3. 二维码生成
- 4. 二级路由拆分(个人中心)
- 5. 图片懒加载
- 6. 路由懒加载
- 7. 项目上线
- 7.1 打包
- 7.2 购买云服务器
1. 注册页面
1.1 注册/登录页面——接口请求
api/index.js
/*--------- 用户注册登录 ---------*/
//获取验证码
export const reqGetCode = (phone) => requests({url: `/user/passport/sendCode/${phone}`, method: 'get'})
//用户注册
export const reqUserRegister = (data) => requests({url: '/user/passport/register', method: 'post', data: data})
store/user.js
import { reqGetCode, reqUserRegister } from "@/api"
//home模块的Vuex模块
const state = {
//state中数据默认初始值别瞎写,根据接口的返回值进行初始化
code: '',
}
const mutations = {
GET_CODE(state, code) {
state.code = code
},
}
const actions = {
//获取验证码
async getCode({commit}, phone){
//获取验证码的这个接口,把验证码返回,正常情况下,后台把验证码发到用户手机上
let result = await reqGetCode(phone)
// console.log("result",result)
if(result.code == 200){
commit('GET_CODE',result.data)
return 'ok'
} else {
return Promise.reject(new Error("fail"))
}
},
//用户注册
async userRegister({commit}, userFrom) {
let result = await reqUserRegister(userFrom)
if(result.code == 200) {
return 'ok'
} else {
return Promise.reject(new Error(result.message))
}
},
}
export default{
state,
getters,
mutations,
actions
}
1.2 Vue开发中Element UI的样式穿透
1.2.1 ::v-deep的使用
参考::v-deep的使用
在 vue 项目的开发过程,使用了 ElementUI 组件且样式 style 使用了 scoped 属性,当想要修改组件样式,发现直接修改不了,需去掉 scoped 属性或者使用深度选择器才能修改成功。去掉scoped的话又会影响全局样式,针对这种情况,可以使用深度作用选择器(即样式穿透)
1、当项目中使用的 css 原生样式 ,需要使用 >>> 深度选择器来修改 外用第三方组件的样式
<style lang="css" scoped>
.el-button >>> span{
color: '#f00'
}
</style>
2、当项目中使用的 css 扩展语言是 less, 需要使用 /deep/ 或者 ::v-deep 深度选择器来修改 外用第三方组件的样式
<style lang="less" scoped>
/deep/.el-button{
span{
color: '#f00'
}
}
.el-button::v-deep{
span{
color: '#f00'
}
}
</style>
3、当项目中使用的 css 扩展语言是 node-sass, 需要使用 /deep/ 或者 ::v-deep 深度选择器来修改 外用第三方组件的样式
<style lang="scss" scoped>
.el-button::v-deep{
span{
color: '#f00'
}
}
/deep/.el-button{
span{
color: '#f00'
}
}
</style>
4、当项目中使用的 css 扩展语言是 dart-sass, 需要使用 ::v-deep 深度选择器来修改 外用第三方组件的样式,dart-sass不支持 /deep/ 和 >>> 的写法
<style lang="scss" scoped>
.el-button::v-deep{
span{
color: '#f00'
}
}
</style>
注意:
① 操作符 >>> 可能会因为无法编译而报错,可以使用 /deep/
② vue3.0 中使用 /deep/ 会报错,更推荐使用 ::v-deep
③ 对于使用了 css 预处理器(scss 、sass、 less)时,深度选择器 ::v-deep 比较通用
1.2.2 elementUI Dialog内容区域显示滚动条
所以在项目,我要使对话框的内容区域显示滚动条,同时去掉对话框原有的外部滚动条,使用样式穿透,代码如下:
<el-dialog title="Dialog" class="roll-dialog"> </el-dialog>
.rolling-dialog {
overflow: hidden;
::v-deep .el-dialog .el-dialog__body {
overflow-y: scroll;
height: 400px;
}
}
实际开发中还有对话框内容区域高度自适应的要求,可以阅读这篇 Element UI 弹窗(Dialog)改成自适应高度,仅body内容部分滚动
效果
1.3 注册页面——步骤条和表单联动 steps+form
element的步骤条整合表单(steps+form)
场景
在vue开发中,注册页面填写的信息过多,如果全部一起呈现,效果不是很好,这时候就可以用到步骤条来分步注册,这里用到了ElementUI里的steps
组件和form
页面代码如下:
<template>
//第一步:定义出4个步骤
<el-steps :active="active" finish-status="success" align-center :space="200" class="steps">
<el-step title="验证手机号"></el-step>
<el-step title="填写帐号信息"></el-step>
<el-step title="注册成功" status="success"></el-step>
</el-steps>
//第二步:定义form表单
<el-form
ref="registerForm"
:model="registerForm"
:rules="rules"
class="form-body">
//第三步:定义3个盒子对象active =>0 到 2
<div v-show="active == 0">
//第四步:放置表单项
//...
<el-form-item class="form-item" prop="phoneNum">
<el-input clearable placeholder="建议使用常用手机号" v-model="registerForm.phoneNum">
</el-form-item>
</div>
<div v-show="active == 1"></div>
<div v-show="active == 2"></div>
</el-form>
//第五步:设置上一步和下一步的按钮
<el-button v-if="active < 3" style="margin-top: 12px" @click="next">下一步</el-button>
<el-button v-if="active > 1" style="margin-top: 12px" @click="pre">上一步</el-button>
</template>
对应的属性和方法
data() {
return {
//默认第一步
active: 0,
}
},
methods: {
// 步骤条下一步的方法
next() {
if (this.active++ > 2) this.active = 0
},
// 步骤条上一步的方法
pre() {
if (this.active-- < 0) this.active = 0
},
}
1.4 注册页面——滑动拼图验证
Vue实现滑块拼图验证,这里使用了vue-monoplasty-slide-verify插件
参考 vue实现登录滑动拼图验证的两种方法,纯前端组件验证以及前后端同时验证
1.5 注册页面——element-ui组件Popover 弹出框 条件控制显示和隐藏 滑块验证码
场景:输入手机号码,手机格式正确,点击按钮验证才可显示弹出框
手动控制el-popver
弹窗的显示与隐藏,给el-popver
层绑定一个v-model
,值为true
或是false
,这是官网上给的Attributes。
<el-popover v-model="showPopover">
而且显示根本不用控制,el-popover
有一个trigger
属性,trigger
可以为click/focus/hover/manual
,默认值是click
,所以单击就能触发,主要是弹窗的隐藏问题。
我们使用manul
来控制显示
<el-popover trigger="manual">
实际代码:
<el-popover placement="top" width="320" trigger="manual" v-model="showSliderVerify">
<div class="popper-title">
<span>完成拼图验证</span>
<i class="el-icon-close close-icon" @click="showSliderVerify=false"></i>
</div>
<slide-verify :l="42" :r="10" :w="310":h="155"
slider-text="向右滑动"
@success="onSuccess"
@fail="onFail"
@refresh="onRefresh">
</slide-verify>
<div style="margin-top: 15px">{{ msg }}</div>
<!--点击控制弹窗的显示-->
<el-button slot="reference" style="width: 400px" @click="openSliderVerify">
点击按钮进行验证
</el-button>
</el-popover>
<script>
export default {
data() {
return {
showSliderVerify:false,//v-model默认值是false, click激活变成true
msg: ""
}
},
methods: {
//滑块按钮点击
openSliderVerify() {
//只有手机号码通过才能显示滑块验证码
this.$refs.registerForm.validateField("phoneNum", async (valid) => {
if (!valid) {
//手机号码格式正确,才可以显示滑块验证码
this.showSliderVerify = true;
} else {
return false;
}
});
},
//滑块验证通过
onSuccess(times) {
this.msg = `success, 耗时${(times / 1000).toFixed(1)}s`;
this.showSliderVerify = false;
this.showCode = true;
//验证码一通过,就自动获取验证码
this.getCode();
},
//滑块验证失败
onFail() {
this.msg = "验证不通过";
},
//滑块验证刷新
onRefresh() {
this.msg = "";
console.log("点击了刷新小图标");
},
//滑块验证刷新完成
onFulfilled() {
this.msg = "重新验证";
},
}
}
</script>
代码中我还设置了样式,但是样式在当前vue文件下,怎么样调整都不动。
后来看来这个blog
原来是要在App.vue
下写css样式,
<style lang="less">
.popper-title {
margin-bottom: 10px;
display: flex;
justify-content: space-between;
font-size: 16px;
.close-icon {
cursor: pointer;
font-size: 20px;
color: #4f4f4f;
}
}
</style>
原因可以看下面这张图,你会发现 app 和 el-popover 是平级,又因为我们每个组件的style标签都写有 scoped 属性,所以在组件里写样式不起效
1.6 注册页面——获取手机验证码,进行手机号校验、验证码CD60秒
参考blog
场景
手机输入不能为空且必须为正确格式
点击下一步,如果未完成验证,则不可以进行下一步注册步骤
完成滑块拼图验证后,立即自动发送验证码,60s有效期,
由于注册页面用到了前面的提到的steps
,所以手机及验证码的输入框是在 <div v-show="active == 0"></div>
内,且只有手机号码输入正确,才可进行滑块拼图验证,滑块拼图验证通过之后,立即发送验证码
页面代码如下
<el-steps :active="active" finish-status="success" align-center :space="200" class="steps" >
<el-step title="验证手机号"></el-step>
<el-step title="填写帐号信息"></el-step>
<el-step title="注册成功"></el-step>
</el-steps>
<el-form ref="registerForm" :model="registerForm" :rules="rules" class="form-body">
<!-- 步骤条 active==0 -->
<div v-show="active == 0">
<el-form-item class="form-item" prop="phoneNum">
<el-input clearable placeholder="建议使用常用手机号" v-model="registerForm.phoneNum">
<el-select
v-model="registerForm.select"
placeholder="中国+86"
slot="prepend"
style="width: 120px"
>
<el-option label="中国+86" value="中国+86"></el-option>
<el-option label="+40" value="+40"></el-option>
<el-option label="+111" value="+111"></el-option>
</el-select>
</el-input>
</el-form-item>
<el-form-item class="form-item" v-show="!showCode">
<el-popover placement="top" width="320" trigger="manual" v-model="showSliderVerify">
<div class="popper-title">
<span>完成拼图验证</span>
<i class="el-icon-close close-icon" @click="showSliderVerify = false"></i>
</div>
<slide-verify :l="42" :r="10" :w="310" :h="155" :imgs="bgimgs"
slider-text="向右滑动"
@success="onSuccess"
@fail="onFail"
@refresh="onRefresh"
></slide-verify>
<div style="margin-top: 15px">{{ msg }}</div>
<el-button slot="reference" class="wd400" @click="openSliderVerify">
点击按钮进行验证
</el-button>
</el-popover>
<div v-show="validateCode" class="el-form-item__error">请完成验证</div>
</el-form-item>
<el-form-item prop="code" class="form-item" v-show="showCode">
<el-input clearable v-model="registerForm.code" placeholder="请输入验证码">
<template slot="prepend">手机验证码</template>
<el-button slot="append" :disabled="codeCd" size="samll" @click="getCode">
<span v-if="codeCd">{{ long }}后重新获取</span>
<span v-else>获取验证码</span>
</el-button>
</el-input>
</el-form-item>
<el-form-item class="form-item">
<el-button class="wd400" @click="next">下一步</el-button>
</el-form-item>
</div>
</el-form>
对应的逻辑代码如下
export default {
name: "Register",
data() {
//验证手机号
const validatePhone = (rule, value, callback) => {
if (!value) {
callback(new Error("手机号码不能为空"));
}
// 使用正则表达式验证手机号码
if (!/^1[3456789]\d{9}$/.test(value)) {
callback(new Error("手机号码格式不正确"));
}
//自定义校验规则,需要调用callback()函数
callback();
};
return {
registerForm: {
phoneNum: null,
code: "",
},
rules: {
phoneNum: [
{
required: true,
validator: validatePhone,
trigger: "blur",
},
],
code: [
{
required: true,
message: "验证码不能为空!",
trigger: "blur",
},
],
},
//验证码秒数倒计时
long: 60,
//验证码是否等候
codeCd: false,
//滑块拼图验证码msg
msg: "",
//滑块验证码背景图
bgimgs: [],
//步骤条的active
active: 0,
//是否显示滑块拼图验证
showSliderVerify: false,
//是否显示验证码
showCode: false,
//验证是否完成拼图滑块
validateCode: false
};
},
methods: {
//滑块按钮点击
openSliderVerify() {
//只有手机号码通过才能显示滑块验证码
this.$refs.registerForm.validateField("phoneNum", async (valid) => {
if (!valid) {
//手机号码格式正确,才可以显示滑块验证码
this.showSliderVerify = true;
} else {
return false;
}
});
},
//步骤条下一步
next() {
//当前表格是否进行验证
const { phoneNum, code } = this.registerForm;
const { showCode } = this;
//验证手机号
if (!phoneNum) {
this.$refs.registerForm.validateField("phoneNum");
return;
}
//验证滑块拼图验证码
if (!showCode) {
//展示提示信息
this.validateCode = true
return;
} //完成滑块验证后,验证是否填写验证码
else if (showCode && !code) {
this.$refs.registerForm.validateField("code");
return;
}
//以上都通过才进入下一步
if (this.active++ > 2) this.active = 0;
},
//获取手机验证码
getCode() {
this.$refs.registerForm.validateField("phoneNum", async (valid) => {
// valid是验证手机号码是否通过
if (!valid) {
// 获取验证码
try {
//发送验证码
this.$store.dispatch("getCode", this.registerForm.phoneNum);
//开始计时,60秒倒计时
this.codeCd = true;
const timer = setInterval(() => {
this.long--;
if (this.long <= 0) {
this.long = 60;
this.codeCd = false;
clearInterval(timer);
}
}, 1000);
//假设手动输入验证码
this.registerForm.code = this.$store.state.user.code;
} catch (error) {
alert(error.message);
}
} else {
return false;
}
});
},
//滑块验证通过
onSuccess(times) {
this.msg = `success, 耗时${(times / 1000).toFixed(1)}s`;
this.showSliderVerify = false;
this.showCode = true;
//验证码一通过,就自动获取验证码
this.getCode();
},
//滑块验证失败
onFail() {
this.msg = "验证不通过";
},
//滑块验证刷新
onRefresh() {
this.msg = "";
//console.log("点击了刷新小图标");
},
//滑块验证刷新完成
onFulfilled() {
this.msg = "重新验证";
},
- 进行手机号校验关键在对单个手机号输入框进行校验,需要使用到
validateField
对部分表单字段进行校验,valid是校验完的提示信息,当valid为空时代表校验成功 - 读秒和设置禁用,在校验成功时设置一个60s计时器,读秒过程禁用按钮,用了
element-ui
的按钮组件,在读秒过程中给按钮增加disabled
属性;读秒过程结束,解除按钮禁用
1.7 注册页面——element-ui组件el-autocomplete带输入建议 自动补全后缀(邮箱地址)
效果:实现输入数字,自动补齐邮箱后缀
autocomplete
是一个可带输入建议的输入框组件,fetch-suggestions
是一个返回输入建议的方法属性,如 querySearch(queryString, cb)
,在该方法中你可以在你的输入建议数据准备好时通过 cb(data)
返回到 autocomplete
组件中。
<el-autocomplete
clearable
v-model="registerForm.email"
:fetch-suggestions="emailSuffix"
:trigger-on-focus = 'false'
@select="selectEmailSuffix"
class="wd400"
placeholder="请输入邮箱"
>
<template slot="prepend">邮箱验证</template>
</el-autocomplete>
data(){
return {
suffix: []
}
},
mounted() {
this.suffix = this.loadAll()
},
methods: {
//邮箱后缀输入建议
emailSuffix(queryString, callback) {
console.log(queryString);
let suffix = this.suffix
let results = JSON.parse(JSON.stringify(suffix))
for(let item in results) {
results[item].value = queryString + '' + suffix[item].value
}
callback(results)
},
//选择的哪个值
selectEmailSuffix(item) {
console.log(item);
},
loadAll() {
return [
{"value": "@qq.com"},
{"value": "@126.com"},
{"value": "@163.com"},
{"value": "@sohu.com"},
{"value": "@Gmail.com"},
{"value": "@Sina.com"}
]
},
}
1.8 注册页面——用户、密码强弱校验+密码自定义规则校验、提交验证
参考vue3+ts+element-plus密码强弱校验+密码自定义规则校验
博客中用的是Vue3,自己项目中用的Vue2,去掉密码规则中的"是否包含3个及以上键盘连续字符;(横向、斜向都包括)"这一项,其他要求都差不多。
修修改改,实现效果如下:
页面代码
<el-form ref="registerForm" :model="registerForm" :rules="rules" class="form-body">
<div v-show="active ==0 ">
<!-- 验证手机号.... -->
</div>
<div v-show="active == 1">
<el-form-item class="form-item" prop="username">
<el-input clearable v-model="registerForm.username" placeholder="账户唯一识别,可用来登录">
<template slot="prepend">账号名</template>
</el-input>
</el-form-item>
<el-form-item class="form-item" prop="password">
<el-input
clearable
v-model="registerForm.password"
show-password
placeholder="请输入包含英文字母大小写、数字和特殊符号的 8-16 位组合"
>
<template slot="prepend">设置密码</template>
</el-input>
<div class="barbox"
v-if="registerForm.password !== '' && registerForm.password !== undefined">
<div class="strength" :style="{ color: barColor }">{{ strength }} </div>
<div class="bar" :style="{ background: barColor, width: width + '%' }"></div>
</div>
</el-form-item>
<el-form-item class="form-item" prop="repassword">
<el-input
clearable
v-model="registerForm.repassword"
show-password
placeholder="请再次输入密码"
>
<template slot="prepend">确认密码</template>
</el-input>
</el-form-item>
<el-form-item class="form-item" prop="email">
<el-autocomplete
clearable
v-model="registerForm.email"
:fetch-suggestions="emailSuffix"
:trigger-on-focus="false"
@select="selectEmailSuffix"
class="wd400"
placeholder="请输入邮箱"
>
<template slot="prepend">邮箱验证</template>
</el-autocomplete>
</el-form-item>
<el-form-item class="form-item" prop="emailCode">
<el-input
clearable
v-model="registerForm.emailCode"
placeholder="请输入邮箱验证码"
>
<template slot="prepend">邮箱验证码</template>
<el-button
slot="append"
:disabled="emailCodeCd"
size="samll"
@click="getEmailCode"
>
<span v-if="emailCodeCd">{{ emaillong }}后重新获取</span>
<span v-else>获取验证码</span>
</el-button>
</el-input>
</el-form-item>
<el-form-item class="form-item">
<el-button class="wd400" @click="submitForm">立即注册</el-button>
</el-form-item>
</div>
<div v-show="active == 2">
<div class="registerOk">
<i class="el-icon-time icon"></i>
<h1>恭喜您 {{ this.registerForm.username }}</h1>
<span>您已成功注册为京东用户,祝您购物愉快~</span>
<router-link class="btn" to="/home">去购物</router-link>
</div>
</div>
</el-form>
css代码
.barbox {
display: flex;
align-items: center;
height: 26px;
.strength {
font-size: 13px;
color: #271e25;
transition: 0.5s all ease;
margin-right: 5px;
flex-shrink: 0;
}
.bar {
height: 5px;
background: red;
transition: 0.5s all ease;
max-width: 400px;
}
}
.registerOk {
color: #333;
font-size: 14px;
display: flex;
flex-flow: column;
align-items: center;
.icon {
font-size: 40px;
color: green;
}
h1 {
font-size: 40px;
margin: 16px 0;
}
span {
margin-bottom: 16px;
}
.btn {
padding: 10px 20px;
background-color: #c81623;
color: #fff;
}
}
对应的逻辑代码
//引入验证方法
import { checkPasswordRule, level } from "./CheckPassword";
export default {
name: "Register",
data() {
//验证手机号
const validatePhone = (rule, value, callback) => {
if (!value) {
callback(new Error("手机号码不能为空"));
}
// 使用正则表达式验证手机号码
if (!/^1[3456789]\d{9}$/.test(value)) {
callback(new Error("手机号码格式不正确"));
}
//自定义校验规则,需要调用callback()函数
callback();
};
//密码验证
const passwordValidate = (rule, value, callback) => {
if (!value) {
callback(new Error("密码不能为空"));
} else {
let name =
this.registerForm.username === "" ? "" : this.registerForm.username;
const result = checkPasswordRule(value, name);
if (result === "校验通过") {
callback();
} else {
callback(new Error(result));
}
}
//该部分是只验证密码是否满足reg正则,而不进行强弱校验
// const reg = /^(?=.*[A-Za-z])(?=.*[0-9])[A-Za-z0-9]{8,16}$/g;
// if (!reg.test(value)) {
// callback(new Error("请输入包含英文字母、数字的 8-16 位组合"));
// } else {
// callback();
// }
};
//密码与确认密码不一样,一定要写在data里,但不是return里
const repeatValidate = (rule, value, callback) => {
if (!value) {
callback(new Error("请再次输入密码"));
} else if (value !== this.registerForm.password) {
callback(new Error("两次输入密码不一致"));
} else {
callback();
}
};
return {
registerForm: {
username: "",
phoneNum: null,
code: "",
email: "",
password: "",
repassword: "",
emailCode: "",
},
rules: {
phoneNum: [
{
required: true,
validator: validatePhone,
trigger: "blur",
},
],
code: [
{
required: true,
message: "验证码不能为空!",
trigger: "blur",
},
],
password: [
{
required: true,
validator: passwordValidate,
trigger: "blur",
},
],
repassword: [
{
required: true,
validator: repeatValidate,
trigger: "blur",
},
],
username: [
{
required: true,
message: "账户名不能为空",
trigger: "blur",
},
{
min: 4,
max: 30,
message: "账户名长度在4至30个字符之间",
trigger: "blur",
},
],
email: [
{
required: true,
message: "请输入邮箱地址",
trigger: "blur",
},
{
type: "email",
message: "请输入正确的邮箱地址",
trigger: ["blur", "change"],
},
],
emailCode: [
{
required: true,
message: "邮箱验证码不能为空!",
trigger: "blur",
},
],
},
//验证码秒数倒计时
long: 60,
emaillong: 300,
//密码强度背景色
barColor: "",
//密码强度长度
width: "",
//密码强度
strength: "",
//验证码是否等候
codeCd: false,
//邮箱验证码等候
emailCodeCd: false,
};
},
methods: {
//提交表单_用户注册
submitForm() {
this.$refs["registerForm"].validate(async(valid)=>{
if(valid) {
try {
const { phoneNum, code, password } = this.registerForm;
phoneNum && code && password && (await this.$store.dispatch("userRegister", {
phone: phoneNum,
code: code,
password:password,
}));
this.active++;
// this.$router.push("/login");
} catch (error) {
alert(error.message);
}
}else {
return false
}
})
},
},
watch: {
"registerForm.password"(newVal, oldVal) {
if (newVal !== "") {
const res = level(newVal);
this.strength = res;
switch (res) {
case "非常强":
this.barColor = "#1B8EF8";
this.width = "100";
break;
case "强":
this.barColor = "green";
this.width = "80";
break;
case "一般":
this.barColor = "orange";
this.width = "60";
break;
case "弱":
this.barColor = "#ee795c";
this.width = "40";
break;
case "非常弱":
this.barColor = "red";
this.width = "20";
break;
}
}
},
},
强弱校验、规则校验 CheckPassword.js:
// 数字
const REG_NUMBER = '.*\\d+.*'
//大写字母
const REG_UPPERCASE = '.*[A-Z].*'
//小写字母
const REG_LOWERCASE = '.*[a-z].*'
//特殊符号
const REG_SYMBOL = ".*[~!@#$%^&*()_+|<>,.?/:;'\\[\\]{}\"]+.*"
/**
* 校验密码是否符合条件
* @param password 密码
* @param username 用户名
*/
export const checkPasswordRule = (password, username) => {
if(password === '' ) {
return "密码不能为空"
}else if (password.length < 8 || password.length > 20) {
return "密码长度应大于8小于20"
}
if(username && password.indexOf(username) !== -1) {
return "请勿包含用户名"
}
if(isContinuousChar(password)) {
return "请勿包含3个及以上相同或连续的字符"
}
let i = 0
if(password.match(REG_NUMBER)) i++
if(password.match(REG_UPPERCASE)) i++
if(password.match(REG_LOWERCASE)) i++
if(password.match(REG_SYMBOL)) i++
if(i<2) {
return "数字、小写字母、大写字母、特殊字符,至少包含两种";
}
return "校验通过"
}
/**
* 是否包含3个及以上相同或字典连续字符
*/
const isContinuousChar = (password) => {
let chars = password.split('')
let charCode = []
for(let i=0; i<chars.length-2; i++) {
charCode[i] = chars[i].charCodeAt(0)
}
for(let i=0; i<chars.length-2; i++) {
let n1 = charCode[i]
let n2 = charCode[i+1]
let n3 = charCode[i+2]
//判断重复字符
if(n1 == n2 && n2 == n3) {
return true
}
//判断连续字符: 正序+倒序
if((n1 + 1 == n2 && n2 + 2 == n3) || (n1 - 1 == n2 && n2 - 2 == n3)) {
return true
}
}
return false
}
/**
* 密码强度校验
*/
/**
* 长度
* @param str
*/
const length = (str) => {
if(str.length<5){
return 5;
}else if(str.length<8){
return 10;
}else{
return 25;
}
}
/**
* 字母
* @param str
*/
const letters = (str)=> {
let count1 = 0, count2 = 0
for(let i=0; i<str.length; i++) {
if(str.charAt(i) >= 'a' && str.charAt(i) <= 'z'){
count1++
}
if(str.charAt(i) >= 'A' && str.charAt(i) <= 'Z'){
count2++
}
}
if(count1==0 && count2==0) {
return 0
}
if(count1!=0 && count2!=0) {
return 20
}
return 10
}
/**
* 数字
* @param str
*/
const numbers = (str)=> {
let count = 0
for(let i=0; i<str.length; i++) {
if(str.charAt(i) >= '0' && str.charAt(i) <= '9'){
count++
}
}
if(count==0) {
return 0
}
if(count==1) {
return 10
}
return 20
}
/**
* 符号
* @param str
*/
const symbols = (str)=> {
let count = 0
for(let i=0; i<str.length; i++) {
if(str.charCodeAt(i)>=0x21 && str.charCodeAt(i)<=0x2F ||
str.charCodeAt(i)>=0x3A && str.charCodeAt(i)<=0x40 ||
str.charCodeAt(i)>=0x5B && str.charCodeAt(i)<=0x60 ||
str.charCodeAt(i)>=0x7B && str.charCodeAt(i)<=0x7E ){
count++;
}
}
if(count==0) {
return 0
}
if(count==1) {
return 10
}
return 25
}
/**
* 得分机制
* @param str
*/
const rewards = (str) => {
let letter = letters(str)
let number = numbers(str)
let symbol = symbols(str)
if(letter>0 && number>0 && symbol==0){ //字母和数字
return 2;
}
if(letter==10 && number>0 && symbol>0){ //字母、数字和符号
return 3;
}
if(letter==20 && number>0 && symbol>0){ //大小写字母、数字和符号
return 5;
}
return 0;
}
/**
* 最终评分
* @param str
*/
export const level = (str) => {
let lengths=length(str);//长度
let letter=letters(str);//字母
let number=numbers(str);//数字
let symbol=symbols(str);//符号
let reward=rewards(str);//奖励
let sum = lengths+letter+number+symbol+reward
if(sum>=80) {
return '非常强'
}else if (sum>=60) {
return "强"
}else if(sum>=40) {
return '一般'
}else if(sum>=25) {
return '弱'
}else {
return "非常弱"
}
}
常用的密码校验正则和 Regex 正则表达式
包含英文字母大小写、数字和特殊符号的 8-16 位组合
/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[._~!@#$^&*])[A-Za-z0-9._~!@#$^&*]{8,16}$/g
2. 登录页面
2.1 登录页面——接口
api/index.js
/*--------- 用户登录 ---------*/
//登录
export const reqUserLogin = (data) => requests({url: '/user/passport/login', method: 'post', data: data})
//登陆后获取用户信息(需要带着用户的token向服务器要用户信息)
export const reqUserInfo = () => requests({url: '/user/passport/auth/getUserInfo',method: 'get'})
//退出登录
export const reqLogout = () => requests({url: '/user/passport/logout',method: 'get'})
store/user.js
import { reqUserLogin } from "@/api"
//home模块的Vuex模块
const state = {
//state中数据默认初始值别瞎写,根据接口的返回值进行初始化
token: localStorage.getItem("TOKEN"),
userInfo: {}
}
const mutations = {
USER_LOGIN(state, token){
state.token = token
},
USER_INFO(state, data) {
state.userInfo = data
},
USER_CLEAR(state) {
//清除本地数据
state.token = ""
state.userInfo = {}
localStorage.removeItem("TOKEN")
}
}
const actions = {
//用户登录
async userLogin({commit}, userFrom) {
let result = await reqUserLogin(userFrom)
//服务器下发token,用户唯一标识符(uuid)
//将来经常通过带token找服务器要用户信息进行展示
if(result.code == 200) {
//token存入vuex
commit("USER_LOGIN", result.data.token)
//持久化存储token
localStorage.setItem('TOKEN', result.data.token)
return 'ok'
} else {
return Promise.reject(new Error(result.message))
}
},
//获取用户信息
async getUserInfo({commit}) {
let result = await reqUserInfo()
if(result.code == 200) {
commit("USER_INFO", result.data)
return 'ok'
} else {
return Promise.reject(new Error(result.message))
}
},
//退出登录
async userLogout({commit}) {
let result = await reqLogout()
if(result.code == 200) {
commit("USER_CLEAR", result.data)
return 'ok'
} else {
return Promise.reject(new Error(result.message))
}
}
}
export default{
state,
getters,
mutations,
actions
}
2.2 登录——token
登录成功的时候,后台为了区分你这个用户是谁-服务器下发token[令牌:唯一标识符]
一般登录成功服务器会下发token,前台持久化存储token,[带着token找服务器要用户信息进行展示]
Vuex不是持久化存储,如果token存储在Vuex,页面一刷新token数据就没有了,所以使用localStorage
存储
登陆组件methods登陆函数userLogin
methods: {
async userLogin() {
try {
const {phone, password} = this
phone && password && await this.$store.dispatch('userLogin', {phone,password})
//登陆成功,有query参数,就跳到query参数指定的路由,无query参数,跳到home组件
//这个query参数是导航守卫设置的,next("/login?redirect="+toPath),toPath是原路由
let toPath = this.$route.query.redirect || '/home'
this.$router.push(toPath)
} catch (error) {
alert(error.message)
}
}
}
登录成功后跳转到指定路由或是首页,若是跳转到首页,在首页获取用户信息,向服务器请求用户信息,需要携带token
view/home/index.vue
mounted() {
// 触发vuex的异步action调用, 从mock接口请求数据到state中
this.$store.dispatch("getFloorList")
//获取用户信息在首页展示
this.$store.dispatch("getUserInfo")
},
api/request.js 下的请求拦截器,设置携带token
//配置请求拦截器
requests.interceptors.request.use(config => {
//config内主要是对请求头Header配置
//比如添加token
//1、先判断uuid_token是否为空
if(store.state.detail.uuid_token) {
//2、userTempId字段和后端统一
config.headers['userTempId'] = store.state.detail.uuid_token
}
//需要携带token带给服务器
if(store.state.user.token) {
config.headers.token = store.state.user.token
}
//开启进度条
nprogress.start()
return config;
})
登录接口返回的token
获取用户信息接口,携带token
2.3 登录页面——退出登录
methods: {
//退出登录
async logout() {
//1.发请求,通知服务器退出登录(清除数据,如token)
//2. 清楚项目当中的用户数据(userInfo)
try {
//退出成功
await this.$store.dispatch("userLogout")
//回到首页
this.$router.push('/home')
} catch (error) {
alert(error.message)
}
}
}
2.4 登录页面——全局导航守卫
存在的问题:
- 多个组件需要展示用户信息,需要在每一个组件的mounted中触发 获取用户信息接口函数
- 用户已经登录,不应该再回登陆页面
- 未登录,不允许跳转到购物车和订单
流程
为什么要判断name?
因为store中的token是通过localStorage获取的,token有存放在本地。当页面刷新时,本地token不会消失,所以store中的token也不会消失。但是,store中的其他数据(用户信息等)会清空,此时会出现用户信息不存在,但是有token,这种情况是不可以访问其他页面的,必须先去获取用户信息。由于用户信息是一个对象,所以我们通过它的一个属性name判断用户信息是否存在。
所以不仅要判断token,还要判断用户信息
router/index.js全局前置守卫代码
//设置全局导航前置守卫
router.beforeEach(async(to, from, next) => {
let token = store.state.user.token
let name = store.state.user.userInfo.name
//1、有token代表登录,全部页面放行
if(token){
//1.1登陆了,不允许前往登录页
if(to.path==='/login'){
next('/home')
} else{
//1.2、因为store中的token是通过localStorage获取的,token有存放在本地
// 当页面刷新时,token不会消失,但是store中的其他数据会清空,
// 所以不仅要判断token,还要判断用户信息
//1.2.1、判断仓库中是否有用户信息,有放行,没有派发actions获取信息
if(name)
next()
else{
//1.2.2、如果没有用户信息,则派发actions获取用户信息
try{
await store.dispatch('getUserInfo')
next()
}catch (error){
//1.2.3、获取用户信息失败,原因:token过期
//清除前后端token,跳转到登陆页面
await store.dispatch('logout')
next('/login')
}
}
}
}else{
//2、未登录,首页或者登录页可以正常访问
//2. 未登录,支付页面、订单页
let toPath = to.path
if(toPath.indexOf('/pay') !== -1 ||
toPath.indexOf('/trade')!==-1 ||
toPath.indexOf('/center')!==-1)
{
alert("请先登录")
//登录成功后,回到原页面,源地址存储在地址栏中
next("/login?redirect="+toPath)
} else {
// 其他可以正常访问
next()
}
}
})
2.5 路由独享守卫
全局导航守卫已经帮助我们限制未登录的用户不可以访问相关页面。但是还会有一个问题。
例如:
用户已经登陆,用户在home页直接通过地址栏访问trade结算页面,发现可以成功进入该页面,正常情况,用户只能通过在shopcart页面点击去结算按钮才可以到达trade页面。我们可以通过路由独享守卫解决该问题
路由独享的守卫:只针对一个路由的守卫,所以该守卫会定义在某个路由中。
以上面问题为例,我们可以通过路由独享的守卫解决。
在trade路由信息中加入路由独享守卫
//交易组件
{
name: 'Trade',
path: '/trade',
meta: {showFooter: true},
component: () => import('@/views/Trade'),
//路由独享首位
beforeEnter: (to, from, next) => {
//购物车页面才可进入交易页面
if(from.path === '/shopcart' ){
next()
}else{
next(false)
}
}
},
//支付组件
{
path: '/pay',
component: () => import('@/views/Pay'),
meta: {showFooter: true},
//路由独享守卫
beforeEnter: (to, from, next) => {
if(from.path === '/trade'){
next()
} else {
next(false)
}
}
},
上面的代码已经实现了trade路由只能从shopcart路由跳转。next(false)
指回到from路由。
但是,上面的代码还会有bug,就是当我们在shopcart页面通过地址栏访问trade时还是会成功。正常情况应该是只有当我们点击去结算按钮后才可以进入到trade页面。(这只是我个人观点)
解决办法:
在shopcart路由信息meta中加一个flag,初始值为false。当点击去结算按钮后,将flag置为true。在trade的独享路由守卫中判断一下flag是否为true,当flag为true时,代表是通过点击去结算按钮跳转的,所以就放行。
shopcart路由信息
//购物车
{
path: "/shopcart",
component: () => import('@/views/ShopCart'),
meta:{showFooter: true,flag: false},
},
shopcart组件去结算按钮触发事件
toTrade(){
this.$route.meta.flag = true
this.$router.push('/trade')
}
trade路由信息
{
name: 'Trade',
path: '/trade',
meta: {showFooter: true},
component: () => import('@/views/Trade'),
//路由独享首位
beforeEnter: (to, from, next) => {
//购物车页面才可进入交易页面
if(from.path === '/shopcart'&& from.meta.flag === true){
from.meta.flag = false
next()
}else{
next(false)
}
}
},
注意,判断通过后,在跳转之前一定要将flag置为false。
2.6 组件导航守卫
支付成功页面,设置只有通过支付页面后才能访问
<script>
export default {
name: 'PaySuccess',
//组件内守卫:通过路由规则,进入该组件时被调用
//不能获取组件实例——this,因为守卫执行前,组件实例还未被创建
beforeRouteEnter (to, from, next) {
if(from.path == '/pay') {
next()
}else {
next(false)
}
},
}
</script>
2.7 全局封装API
推荐:API封装的具体步骤
若是想在组件里调用请求接口,而不通过Vuex来调用请求接口,该如何统一配置api接口,一次调用即可,而不须一个个引入
api/index.js文件是请求接口
main.js
//统一接口api
import * as api from '@/api'
new Vue({
render: (h) => h(App),
beforeCreate() {
//全局事件总线
Vue.prototype.$bus = this;
Vue.prototype.$api = api;
},
})
组件内发请求
methods: {
//提交订单
submitOrder() {
console.log(this.$API.reqSubmitOrder());
}
}
3. 二维码生成
qrcode
import QRCode from "qrcode"
//立即支付弹出框
async openMsgBox(){
//生成二维码(地址)
let code = await QRCode.toDataURL(this.payInfo.codeUrl)
this.$alert(`<img src=${code} />`, '请微信支付', {
dangerouslyUseHTMLString: true,
center: true,
showCancelButton: true,
confirmButtonText: '已支付成功',
cancelButtonText: '支付遇见问题',
showClose: false
})
}
4. 二级路由拆分(个人中心)
菜单栏
<dt><i>·</i> 订单中心</dt>
<dd>
<router-link to="/center/myorder">我的订单</router-link>
</dd>
<dd>
<router-link to="/center/grouporder">团购订单</router-link>
</dd>
</dt>
个人中心路由
//个人中心
{
path: '/center',
component: () => import('@/views/Center'),
children: [
{
//二级路由要么不写/,要么写全:'/center/myorder'
path: 'myorder',
component: () => import('@/views/Center/MyOrder')
},
{
path: 'groupbuy',
component: () => import('@/views/Center/GroupOrder'),
},
//默认显示
{
path: '',
redirect: 'myorder'
}
]
}
{ path: '', redirect: 'myorder' }
表示当我们访问center路由时,center中的router-view部分默认显示myorder二级路由内容。
我们的子路由最好放在父路由文件夹下,如下所示。
注意:当某个路由有子级路由时,父级路由须要一个默认的路由,因此父级路由不能定义name属性
5. 图片懒加载
在网络不好时,每个图片都有一个基础的默认图片,即在请求服务器结束前 加载默认设置的图片
懒加载vue-lazyload插件官网
插件的使用直接参考官方教程,很简单。
npm i vue-lazyload
vue使用插件的步骤,main.js
import VueLazyload from "vue-lazyload
import loadingImg from '@/assets/images/loading.jpeg'
Vue.use(VueLazyload, {
//懒加载默认的图片
loading: loadingImg
})
使用懒加载
<img v-lazy="good.defaultImg" />
在使用中报错 如下图所示:
因为该 模块 版本问题, 可安装低版本的 vue-lazyload
来解决该问题:
# 先写在原有的安装
npm uninstall vue-lazyload --save
# 再安装低版本的
npm install vue-lazyload@1.3.3 --save
6. 路由懒加载
路由懒加载
当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就会更加高效。
// 将
// import Center from '@/views/Center'
// 替换成
const Center = () => import('./views/Center')
{
path: '/center',
component: Center,
meta: { showFooter: true },
}
component
(和 components
) 配置接收一个返回 Promise 组件的函数,Vue Router 只会在第一次进入页面时才会获取这个函数,然后使用缓存数据。
7. 项目上线
7.1 打包
npm run build
会生成dist打包文件。
dist就是我们打包好的项目文件
dist文件下的js文件存放我们所有的js文件,并且经过了加密,并且还会生成对应的map文件。
**map文件作用:**因为代码是经过加密的,如果运行时报错,输出的错误信息无法准确得知时那里的代码报错。有了map就可以像未加密的代码一样,准确的输出是哪一行那一列有错。
当然map文件也可以去除(map文件大小还是比较大的)
在vue.config.js
配置productionSourceMap: false
即可。
注意:vue.config.js
配置改变,需要重启项目
7.2 购买云服务器
阿里云、腾讯云
记得重置密码
设置安全组 开放端口
利用xshell等工具登录服务器
nginx
1、如何通过服务器IP地址直接访问到项目?
在服务器上部署dist文件地址:/root/project/dist
2、项目数据来自于哪个服务器
通过nginx从数据服务器拿数据
配置Nginx:在etc文件下
cd etc
ls
安装nginx:yum install nginx
cd nginx
存在文件nginx.conf
编辑vim nginx.conf
解决第一个问题:
location /{
root /root/project/dist;
index index.html;
try_files $uri/ /index.html;
}
解决第二个问题:
location /api{
proxy_pass http://39.983123.211;
启动nginx服务器:service nginx start