从零开始,搭建一个简单的购物平台(十七)前端商城部分:
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值。下一篇将介绍用户信息修改,及后续将实现订单的生成及查看功能