从零开始,搭建一个简单的购物平台(十八)前端商城部分
本文最后更新于:1 个月前
从零开始,搭建一个简单的购物平台(十七)前端商城部分:
https://blog.csdn.net/time_____/article/details/108893925
项目源码(持续更新):https://gitee.com/DieHunter/myCode/tree/master/shopping
上篇文章对购物车进行了简单的介绍,商城的所有基础功能已经全部实现,这篇文章开始将介绍一下用户信息和订单相关的功能实现,用户信息登录在后端管理中已经实现,我们需要实现一个注册功能,和邮箱验证功能,具体实现可参照之前的一篇博客或是这篇文章
实现效果如下,分别是用户名密码登录,邮箱验证登录,注册功能
登录部分
账号密码登录与后台管理系统的登录一样,若用户输入用户名密码登录成功,则将用户部分信息加密成token传至前端并保存至本地,每次通过将token值发送后端进行继续操作
邮箱验证
在服务端的config文件中新建EmailTransporter静态变量,用来配置发送邮箱使用到的nodemailer模块
EmailTransporter: {
// service: "qq", // 运营商 qq邮箱 网易 若使用QQ邮箱,则只需配置service:qq
host: "smtp.163.com",// 若使用网易邮箱,则只需配置host:smtp.163.com
port: 465, //端口
auth: {
user: "132*****516@163.com", //发送方的邮箱
pass: "WAQM******WQEFKB", // pop3 授权码
},
},
然后新建邮箱验证码配置EmailConfig
EmailConfig: {
time: 5,//验证码有效期,单位分钟
codeLength: 4,//验证码长度
sendTime: 1 * 60 * 1000,//后端验证码允许再次发送时间间隔,单位毫秒
targetTime: 5 * 60 * 1000,//验证码有效期,单位毫秒
title: "零食商贩",//验证码标题
},
接着在Utils中写一个生成随机验证码函数
/* 生成随机
* @method codeLength 函数名
* @for Utils 附属于哪个类
* @param {number/string} len 随机数长度
* @return {string} _count 生成len个随机数
*/
static codeLength(len) {
let _count = "";
for (let i = 0; i < len; i++) {
_count += Math.floor(Math.random() * 10); //生成len个随机数
}
return _count;
}
在Utils生成时间戳函数,记录验证码及验证码发送时间和有效时间
/* 生成时间戳
* @method randomCode
* @for Utils
* @param
* @return {object} _count 生成len个随机数
*/
static randomCode() {
return {
code: this.codeLength(EmailConfig.codeLength), //生成的随机数
sendTime: new Date().getTime() + EmailConfig.sendTime, //发送时间
targetTime: new Date().getTime() + EmailConfig.targetTime, //截止时间
};
}
在Utils文件夹下新建SendMail.js并新建发送邮件模块,在utils中调用
const nodemailer = require("nodemailer");
const Config = require("../config/config");
module.exports = class SendMail {
static transporter = nodemailer.createTransport(Config.EmailTransporter); //邮箱配置项
static mailOptions = null; //邮箱配置
/* 发送邮件模块
* @method sendEmail
* @for SendMail
* @param {String} mail 用户邮箱号
* @param {String} title 邮件标题
* @param {String} content 邮件内容
* @return {Boolean} 是否发送成功
*/
static async sendEmail(mail, title, content) {
this.mailOptions = {
from: '"邮箱验证" <' + Config.EmailTransporter.auth.user + ">",
to: mail,
subject: title,
text: content,
};
try {
let result = await this.transporter.sendMail(this.mailOptions);
console.log("发送成功");
return true;
} catch (error) {
console.log(error);
console.log("发送失败");
return false;
}
}
};
在utils中新建生成邮件内容的函数
/* 生成邮件内容
* @method sendEmailCode
* @for Utils
* @param {String} code 验证码内容
* @param {String} email 用户邮箱
*/
static async sendEmailCode(code, email) {
return await SendMail.sendEmail(
email,
EmailConfig.title,
`您的验证码为:${code},${EmailConfig.time}分钟内有效`
);
}
最后在utils编写一个异步发送邮箱的函数
/* 异步发送邮箱验证
* @method createEmailCode
* @for Utils
* @param {Object} codeList 邮箱验证码列表
* @param {String} email 用户邮箱
* @param {Object} findRes 数据库搜寻到的用户信息
* @return {Boolean} isSuccess 是否发送成功
*/
static async createEmailCode(codeList, email, findRes) {
if (!codeList[email] || new Date().getTime() > codeList[email].sendTime) {
//已过1分钟,防止多次请求邮箱
codeList[email] = this.randomCode();
codeList[email].info = findRes;
return await this.sendEmailCode(codeList[email].code, email);
} else {
//未过1分钟
return false;
}
}
一个发送邮件的完整模块就实现完成,下一步要做的是验证码的验证功能
/* 核对验证码
* @method checkEmailCode
* @for Utils
* @param {Object} codeList 用户验证码列表
* @param {String} key 用户邮箱
* @param {Object} _data 用户提交的表单信息
* @return {Object} res 请求响应返回值
*/
static checkEmailCode(codeList, key, _data) {
if (!codeList[key]) {
//未发送验证码
return {
result: 0,
msg: "验证码错误",
};
} else if (
new Date().getTime() < codeList[key].targetTime &&
_data.mailcode == codeList[key].code
) {
//验证码校对成功
let _obj = {
result: 1,
token: Utils.createToken(
codeList[key].info.userType || "",
codeList[key].info.username || "",
_data.remember || ""
),
msg: "操作成功",
};
codeList[key] = null;
return _obj;
} else if (new Date().getTime() > codeList[key].targetTime) {
//验证码超时
return {
result: 0,
msg: "验证码超时",
};
} else {
return {
result: 0,
msg: "验证失败",
};
}
}
到这一步,关于验证码的所有准备工作已全部实现,下一步将实现注册登录功能,其中登录有两个途径,注册时邮箱为必填值,所以可以使用邮箱验证的方式进行登录
服务端获取验证码接口,通过一个codeType区分用户登录获取验证码和注册获取验证码
router.get(Config.ServerApi.getMailCode, async (_req, res) => {
//用户邮箱验证
let _data = Util.getCrypto(Util.parseUrl(_req, res).crypto);//解密参数
//查询用户是否存在,若未找到用户,则返回错误响应值,否则异步发送邮件验证码
let findRes = await findData(Mod, {
mailaddress: _data.username.split('@')[0],
mailurl: '@' + _data.username.split('@')[1],
});
if ((!findRes.length || !findRes) && _data.codeType !== 'reg') {//过滤区分用户注册登录
res.send({
result: 0,
msg: "用户未注册"
});
return;
}
await Util.createEmailCode(userCodeList, _data.username, findRes[0] || {}) ? res.send({
result: 1,
msg: "发送成功",
}) : res.send({
result: 0,
msg: "发送失败"
});
});
在实现注册部分之前,我们要写一个工具方法,用于验证码倒计时,在此期间用户无法再次点击发送请求
import Vue from "vue";
import Config from "../config/config";
const { GetCodeTime, CodeText } = Config;
class TimeTick {
static timer = GetCodeTime / 1000;//倒计时时间
static _timeTick = null;//定时器
static timeTick(fn) {
if (!TimeTick._timeTick) {
TimeTick._timeTick = setInterval(() => {
if (TimeTick.timer-- <= 1) {
// 重置倒计时和发送邮箱验证开关
TimeTick.clearTick();
fn({ content: CodeText, res: 1 });//倒计时归零
} else {
fn({ content: TimeTick.timer + "S", res: 0 });//倒计时中,阻止用户重复点击
}
}, 1000);
}
}
static clearTick() {
//清除定时器
clearInterval(TimeTick._timeTick);
TimeTick._timeTick = null;
}
}
Vue.prototype.$timeTick = TimeTick;
用户注册界面
<template>
<div class="login">
<div>
<mt-field
placeholder="请输入用户名"
:state="userInfo.username.length ? 'success' : 'error'"
v-model="userInfo.username"
></mt-field>
<mt-field
placeholder="请输入密码"
:state="userInfo.password.length ? 'success' : 'error'"
v-model="userInfo.password"
type="password"
></mt-field>
<mt-field
placeholder="请重复输入密码"
:state="
userInfo.repassword.length && userInfo.password == userInfo.repassword
? 'success'
: 'error'
"
v-model="userInfo.repassword"
type="password"
></mt-field>
<mt-field
placeholder="请输入邮箱"
v-model="userInfo.mailaddress"
:state="userInfo.mailaddress.length ? 'success' : 'error'"
>
<mt-button class="btn" @click="selectMail">{{
userInfo.mailurl
}}</mt-button>
</mt-field>
<mt-field
placeholder="请输入验证码"
:state="userInfo.mailcode.length == 4 ? 'success' : 'error'"
v-model="userInfo.mailcode"
type="number"
>
<mt-button class="btn" :disabled="canGetCode" @click="getCode">{{
codeTime
}}</mt-button>
</mt-field>
<mt-button class="btn" type="primary" @click="submit">注册</mt-button>
<div class="shopPicker"></div>
<ShopPicker :ShopMaxCount="address" pickerTitle="邮箱类型"></ShopPicker>
</div>
</div>
</template>
<script>
import Config from "../../config/config";
import Mail from "../../config/mail";
import ShopPicker from "../shopPicker/shopPicker";
import { Field, Button, Picker, Popup } from "mint-ui";
import RegBussiness from "./bussiness";
const { GetCodeTime, EventName, CodeText } = Config;
const { address } = Mail;
export default {
components: {
ShopPicker,
},
data() {
return {
codeTime: CodeText, //获取验证码按钮显示值
address, //邮箱默认地址
canGetCode: false, //防止重复点击开关
userInfo: {
//注册表单默认数据
username: "",
password: "",
repassword: "",
mailurl: address[0],
mailaddress: "",
mailcode: "",
},
};
},
created() {
this.regBussiness = new RegBussiness(this);
this.$events.onEvent(EventName.ChangeCount, (_count) => {
this.userInfo.mailurl = _count; //切换邮箱地址
});
},
destroyed() {
this.$events.offEvent(EventName.ChangeCount);
},
methods: {
selectMail() {
this.$events.emitEvent(EventName.ShowPicker);
},
getCode() {
if (this.canGetCode) {
//是否允许发送邮箱验证
return;
}
this.regBussiness.sendCode().then((res) => {
this.canGetCode = true;//关闭点击开关
this.$timeTick.timeTick((state) => {
this.codeTime = state.content;
switch (state.res) {
case 0:
this.canGetCode = false;//允许用户点击
break;
}
});
});
},
submit() {
this.regBussiness.submitData();
},
},
};
</script>
<style lang="less" scoped>
@import "../../style/init.less";
.login {
.btn {
.f_s(34);
width: 100%;
.h(100);
}
}
</style>
注册业务逻辑部分,bussiness.js
import Vue from 'vue'
import {
Toast
} from "mint-ui";
import config from "../../config/config"
const {
ServerApi,
StorageName,
EventName
} = config
export default class LoginBussiness extends Vue {
constructor(_vueComponent) {
super()
this.vueComponent = _vueComponent
}
sendCode() {
return new Promise((resolve, reject) => {
if (!this.vueComponent.userInfo.mailaddress.length) {//过滤邮箱长度为0
Toast('请填写正确的邮箱');
return
}
this.$axios
.get(ServerApi.user.getMailCode, {
params: {
crypto: this.$crypto.setCrypto({
codeType: "reg",//区分注册登录类型
username: this.vueComponent.userInfo.mailaddress + this.vueComponent.userInfo.mailurl
})
},
}).then(res => {
switch (res.result) {
case 1:
Toast(res.msg);
resolve(res)
break;
default:
reject(res)
break;
}
}).catch(err => {
reject(err)
})
})
}
submitData() {
for (const key in this.vueComponent.userInfo) {
if (this.vueComponent.userInfo.hasOwnProperty(key) && !this.vueComponent.userInfo[key].length) {//过滤表单项长度为0
Toast('请填写完整的信息');
return
}
}
this.$axios
.post(ServerApi.user.userReg, {
crypto: this.$crypto.setCrypto({
...this.vueComponent.userInfo
})
}).then(res => {
//成功后重置用户信息
this.vueComponent.userInfo.password = "";
this.vueComponent.userInfo.repassword = "";
this.vueComponent.userInfo.mailcode = "";
switch (res.result) {
case 1:
Toast(res.msg);
history.go(-1)//返回登录页面
break;
default:
break;
}
})
}
}
注册部分完成,登录与注册功能类似,这里只介绍一下服务端token的生成
在服务端user.js中修改接口,和管理系统登录一样,新增邮箱验证登录,区分管理员和用户登录
router.post(Config.ServerApi.userLogin, async (req, res) => {
let _data = Util.getCrypto(Util.parseUrl(req, res).crypto); //解密前端入参
switch (_data.loginType) {
case "code"://验证码登录,验证邮箱验证码
res.send(Util.checkEmailCode(userCodeList, _data.username, _data));
break;
case "psd"://密码登录
default:
let findRes = await findData(Mod, {
$or: [
{
mailaddress: _data.username.split("@")[0],
mailurl: "@" + _data.username.split("@")[1],
},
{
username: _data.username,
},
{
phoneNum: _data.username,
},
],
});
if (findRes && findRes.length > 0) {
Util.checkBcrypt(_data.password, findRes[0].password)
? res.send({
result: 1,
token: Util.createToken(//生成前端token
findRes[0].userType,
findRes[0].username,
_data.remember
),
msg: "登录成功",
})
: res.send({
result: 0,
msg: "密码错误",
});
return;
}
res.send({
result: 0,
msg: "用户不存在",
});
break;
}
});
总结
本篇文章将用户的注册登录邮箱验证功能基本实现,主要功能参照之前的邮箱验证登录注册的博客,文章中的重点是邮箱验证功能模块,与注册登录配合使用,注册则新增用户,登录则更新token值。下一篇将介绍用户信息修改,及后续将实现订单的生成及查看功能
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!