前言
2025-9 原版链接
仓库链接
一些东西按照自己的理解写了一下, 最后实现的功能暂时没什么问题
组件库 : element
基础表单封装
作用 : 表单的壳子, 实现标题和最后 “登录/注册/重置表单”的渲染部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
| <script setup>
const props = defineProps({ show: { type: Boolean, default: true, }, title: { type: String, default: '标题', }, showClose: { type: Boolean, default: true, }, width: { type: String, default: '30%' }, top: { type: String, default: '50%' }, buttons: { type: Array, }, showCancle: { type: Boolean, default: true },
})
//与父组件的函数相传递, 共同实现关闭(把父组件的show变为false) const emit = defineEmits(['close']) const close = () => { console.log('guabi') emit('close') } </script>
<template> <div> <el-dialog :model-value="show" :show-close="showClose" :draggable="true" :close-on-click-modal="false" :title="title" :width="width" :top="top" class="cust-dialog" @close="close" > <div class="dialog-body"> <slot></slot> </div> <template v-if="(buttons && buttons.length > 0 || showCancle)"> <div class="dialog-footer"> <el-button link @click="close()" v-if="showCancle">取消</el-button> <el-button v-for="btn in buttons" :type="btn.type" @click="btn.click()"> {{ btn.text }} </el-button> </div> </template> </el-dialog> </div> </template>
<style scoped lang="scss"> .cust-dialog { margin: 0px auto !important; width: 200px; .el-dialog__body { padding: 0px; } .dialog-body { border-top: 1px solid #ddd; border-bottom: 1px solid #ddd; padding: 15px; min-height: 100px; max-height: 900px; overflow: auto; } .dialog-footer { text-align: center; padding: 5px 20px; } } </style>
|
实现效果
登录
![[Pasted image 20250914211609.png]]
注册
![[Pasted image 20250914211627.png]]
![[Pasted image 20250914211639.png]]
重置表单
![[Pasted image 20250914211647.png]]
实现逻辑
基础信息
库
1 2 3 4 5 6 7
| //导入的库 import {ref, reactive, proxyRefs, nextTick} from 'vue' import { getCurrentInstance } from 'vue'; import { errorMessages } from 'vue/compiler-sfc'; import md5 from 'js-md5'
const { proxy } = getCurrentInstance();
|
md5:
//用于生成一段数据的固定长度的摘要(即哈希值)。这个过程是单向的
//在用户注册或修改密码时,不对明文密码进行存储
//而是将密码通过 MD5 等哈希算法加密后存入数据库。
// 当用户登录时,再次对输入的密码进行加密,然后与数据库中的哈希值进行比对
api
1 2 3 4 5 6 7
| const api = { checkCode: 'api/checkCode', sendMailCode: '/sendEmailCode', register: '/register', login: '/login', resetPwd: '/resetPwd', }
|
页面显示控制
1 2 3 4 5 6 7 8 9
| //此时显示的是那些页面, showPanel更改显示的内容 //0注册, 1登录, 2找回密码 const onType = ref()
const showPanel = (type)=>{ onType.value = type resetForm() } defineExpose({showPanel,})
|
数据存储
表单信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| const dialogConfig4SendEmailCode = ref({ show: false, title: '发送邮箱验证码', buttons:[{ type: "primary", text: "发送验证码", click: ()=>{ sendEmailCode(); } }] })
const dialogConfig = ref({ show: false, title: '标题', })
|
用户输入信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| const formData = ref({}) const formDataRef = ref() const rules = { email: [ {required: true, message: '请输入邮箱'}, {validator: proxy.Verify.email, message: '请输入正确的邮箱'} ], password: [ {required: true, message: "请输入密码"} ], emailCode: [ {required: true, message: "请输入邮箱验证码"} ], nickName: [ {required: true, message: "请输入昵称"} ], registerPassword: [ {required: true, message: "请输入密码"}, { validator: proxy.Verify.password, message: "密码只能是数字, 字母, 特殊字符 要求8-18位"} ], reRegisterPassword: [ {required: true, message: "请再次输入密码"}, { validator: checkRePassword, message: "两次输入的密码不一致" } ], checkCode: [ {required: true, message: "请输入图片验证码"} ], }
|
验证码
1 2
| const checkCodeUrl = ref(api.checkCode) //最后登录注册前需要填写的验证码 const checkCodeUrl4SendEmailCode = ref(api.checkCode) //注册时的弹窗验证码
|
邮箱验证码弹窗
1 2 3 4 5 6 7 8 9 10
| //邮箱弹窗数据 const formData4SendEmailCode = ref({}) const formData4SendEmailCodeRef = ref()
// 为发送邮箱验证码弹窗创建单独的 rules const rules4SendEmailCode = { checkCode: [ {required: true, message: "请输入图片验证码"} ], }
|
接口封装
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
| import axios from 'axios' import {ElLoading} from 'element-plus' import Message from './Message'
const contentTypeForm = "application/x-www-form-urlencoded;charset=UTF-8" const contentTypeJson = "application/json"
const instance = axios.create({ baseURL: "/api", timeout: 10 * 1000, })
let loading = null //请求前的过滤器 instance.interceptors.request.use( (config)=>{ if(config.showLoading) { loading = ElLoading.service({ lock: true, text: "加载中......", background:"rgb(0, 0, 0, 0.7)" }) } return config }, (error)=>{ if(error.config.showLoading && loading) { loading.close() } Message.error("请求发送失败") return Promise.reject("请求发送失败") });
//请求后的过滤器 instance.interceptors.response.use( (response) => { const {showLoading, errorCallback, showError = true} = response.config if(showLoading && loading) { loading.close() } const responseData = response.data if(responseData.code == 200) { //code状态码微200, 请求成功 return responseData } else if (responseData.code == 901) { //超时 return Promise.reject({showError: false, msg: '登录超时'}) } else{ //其他错误 if(errorCallback) { errorCallback(responseData) } return Promise.reject({showError: showError, msg: responseData.info}) }
}, (error) => { if(error.config.showLoading && loading) { loading.close() } return Promise.reject({showError: true, msg: "网络异常"}) } );
const request = (config)=>{ const {url, params, dataType, errorCallback, showLoading = true, showError = true} = config let contentType = contentTypeForm let formData = new FormData() for (let key in params) { formData.append(key, params[key] == undefined ? "" : params[key]) }
if(dataType != null && dataType === "json") { contentType = contentTypeJson } let headers = { 'Content-type': contentType, 'X-Requested-With': 'XMLHttpRequest', }
return instance.post(url, formData, { headers: headers, showLoading: showLoading, errorCallback: errorCallback, showError: showError, }) .catch ( error => { if (error.showError) { Message.error(error.msg) } return null }) }
export default request;
|
表单验证
表单渲染
详细属性请看 element的Form表单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204
| <div> <Dialog :show="dialogConfig.show" :title="dialogConfig.title" :buttons="dialogConfig.buttons" width="400px" :showCancel="false" @close="dialogConfig.show = false" > <!-- 邮箱 --> <el-form class="login-register" :model="formData" :rules="rules" ref="formDataRef"> <el-form-item prop="email"> <el-input size="large" clearable placeholder="请输入邮箱" v-model="formData.email" maxLength="150" > <template #prefix> <span class="iconfont icon-account"></span> </template> </el-input> </el-form-item>
<!-- 验证码 --> <el-form-item prop="emailCode" v-if="onType != 1"> <div class="email-code-panel"> <el-input size="large" placeholder="请输入验证码" v-model="formData.emailCode" > <template #prefix> <span class="iconfont icon-checkcode"></span> </template> </el-input> <el-button class="email-code" type="primary" size="large" @click="getEmailCode" >获取验证码</el-button> </div> <el-popover placement="left" :width="490" trigger="click"> <div> <p>1、在垃圾箱中查找邮箱验证码</p> <p>2、在邮箱中 ⁢⁢头像->设互->反垃圾->白名单->设苣部件地址白名单</p> <p>3、将邮箱【laoluo@wuhancoder.com】添加到白名单,不知道怎么设置?</p> </div> <template #reference> <span class="a-link" :style="{'fontsize': '14px'}">未收到邮箱验证码?</span> </template> </el-popover> </el-form-item> <!-- 昵称 --> <el-form-item prop="nickName" v-if="onType == 0"> <el-input size="large" clearable placeholder="请输入昵称" v-model="formData.nickName" maxLength="20" > <template #prefix> <span class="iconfont icon-account"></span> </template> </el-input> </el-form-item> <!-- 登录密码 --> <el-form-item prop="registerPassword"> <el-input :type="passwordEyeType.registerPasswordEye?'text':'password'" size="large" clearable placeholder="请输入密码" v-model="formData.registerPassword" > <template #prefix> <span class="iconfont icon-password"></span> </template> <template #suffix> <span @click="eyeChange('registerPasswordEye')" :class="[ 'iconfont', passwordEyeType.registerPasswordEye ?'icon-eye' : 'icon-close-eye' ]"></span> </template> </el-input> </el-form-item> <!-- 确认密码 --> <el-form-item prop="reRegisterPassword" v-if="onType != 1"> <el-input :type="passwordEyeType.reRegisterPasswordEye?'text':'password'" size="large" clearable placeholder="请再次输入密码" v-model="formData.reRegisterPassword" > <template #prefix> <span class="iconfont icon-password"></span> </template> <template #suffix> <span @click="eyeChange('reRegisterPasswordEye')" :class="[ 'iconfont', passwordEyeType.reRegisterPasswordEye ?'icon-eye' : 'icon-close-eye' ]"></span> </template> </el-input> </el-form-item> <!-- 验证码 --> <el-form-item prop="checkCode"> <div class="check-code-panel"> <el-input size="large" placeholder="请输入验证码" v-model="formData.checkCode" > <template #prefix> <span class="iconfont icon-checkcode"></span> </template> </el-input> <img :src="checkCodeUrl" class="check-code" @click="changeCheckCode(0)" alt=""> </div> </el-form-item> <el-form-item v-if="onType == 1"> <div class="remember-panel"> <el-checkbox v-model="formData.rememberMe">记住我</el-checkbox> </div> <div class="no-account"> <a href="javascript:void(0)" class="a-link" @click="showPanel(2)">忘记密码?</a> <a href="javascript:void(0)" class="a-link" @click="showPanel(0)">没有账号</a> </div> </el-form-item> <el-form-item v-if="onType == 0"> <a href="javascript:void(0)" class="a-link" @click="showPanel(1)">已有账号?</a> </el-form-item> <el-form-item v-if="onType == 2"> <a href="javascript:void(0)" class="a-link" @click="showPanel(1)">去登录?</a> </el-form-item> <el-button class="op-btn" type="primary" @click="doSubmit">{{ dialogConfig.title }}</el-button> </el-form> </Dialog>
<!-- 发送邮箱验证码 --> <Dialog :show="dialogConfig4SendEmailCode.show" :title="dialogConfig4SendEmailCode.title" :buttons="dialogConfig4SendEmailCode.buttons" width="500px" :showCancel="false" @close="dialogConfig4SendEmailCode.show = false " > <el-form :model="formData4SendEmailCode" :rules="rules4SendEmailCode" ref="formData4SendEmailCodeRef" label-width="80px" > <!-- 邮箱验证码弹窗 --> <el-form-item label="邮箱"> {{ formData.email }} </el-form-item> <!-- 输入 --> <el-form-item label="验证码" prop="checkCode"> <div class="check-code-panel"> <el-input size="large" clearable placeholder="请输入验证码" v-model="formData4SendEmailCode.checkCode" > <template #prefix> <span class="iconfont icon-checkcode"></span> </template> </el-input> <img :src="checkCodeUrl4SendEmailCode" class="check-code" @click="changeCheckCode(1)" alt=""> </div> </el-form-item> </el-form> </Dialog> </div>
|
逻辑
登录/ 注册/ 验证码消息的发送, 成功与失败的提示信息
Message.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| import {ElMessage} from 'element-plus' // import {de} from 'element-plus/es/locale'
const showMessage = (msg, callback, type)=>{ ElMessage({ type: type, message: msg, duration: 2000, onClose:() => { if(callback){ callback() } }, }) }
const message = { error: (msg, callback)=>{ showMessage(msg, callback, "error") }, success: (msg, callback)=>{ showMessage(msg, callback, "success") }, warning:(msg, callback)=>{ showMessage(msg, callback, "warning") }, }
export default message;
|
通过正则表达式来校验输入框中的信息
与用户输入信息的规则配合使用
Verify.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| const regs = { email: /^\w+([\.\w+]*)\w+@([\w-]+\.)+\w+$/, number: /^(0|[1-9][0-9]*)$/, password: /^(?=.*\d)(?=.*[a-zA-Z])[\da-zA-Z~!@#$%^&*]{8,18}$/, } const verify = (rule, value,reg, callback)=>{ if(value) { if (reg.test(value)) { callback() } else { callback(new Error(rule, message)) } } else { callback() } }
export default { email: (rule, value, callback) => { return verify(rule, value, regs.email, callback) }, number: (rule, value, callback) => { return verify(rule, value, regs.number, callback) }, password: (rule, value, callback) => { return verify(rule, value, regs.password, callback) } }
|
第二次输入的密码验证, 直接判断是否相等就好了
1 2 3 4 5 6 7 8 9
| const checkRePassword = (rule, value, callback)=>{
if (value !== formData.value.registerPassword) { callback(new Error(rule.message)) } else { callback() }
}
|
通过字体图标显示密码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <el-input :type="passwordEyeType.reRegisterPasswordEye?'text':'password'" size="large" clearable placeholder="请再次输入密码" v-model="formData.reRegisterPassword" > <template #prefix> <span class="iconfont icon-password"></span> </template> <template #suffix> <span @click="eyeChange('reRegisterPasswordEye')" :class="[ 'iconfont', passwordEyeType.reRegisterPasswordEye ?'icon-eye' : 'icon-close-eye' ]"></span> </template> </el-input>
|
1 2 3 4 5 6 7 8 9 10 11
| //密码显示隐藏操作
const passwordEyeType = reactive({
passwordEye:false,//图标
registerPasswordEye:false, //注册密码
reRegisterPasswordEye:false, //确认密码
})
|
1 2 3 4 5
| const eyeChange = (type)=>{
passwordEyeType[type] = !passwordEyeType[type]
}
|
重置表单
dialogConfig表示表单最外层的展示信息(例如: 登录/ 注册/ 重置密码)
nextTick : 主要用于在下一次 DOM 更新循环结束之后执行延迟回调函数。
工作步骤: Vue.js 在更新 DOM 时,是异步批量处理的。这意味着当你修改了响应式数据时,Vue.js 并不会立即更新 DOM,而是会将这些更新操作放入一个队列中,等到同一个事件循环(event loop)中的所有同步代码执行完毕后,才会统一批量更新 DOM。
因此,如果想在修改数据后立即尝试访问或操作 DOM,获取到的可能还是更新前的旧 DOM 状态。
`nextTick` 就是用来解决这个问题的。它会把你的回调函数推迟到 Vue.js 完成 DOM 更新之后再执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| const resetForm = () => { dialogConfig.value.show = true
if(onType.value === 0) { dialogConfig.value.title = '注册' } else if (onType.value === 1) { dialogConfig.value.title = '登录' } else { //2 dialogConfig.value.title = '重置密码' } nextTick(()=>{ changeCheckCode(0) formDataRef.value.resetFields() formData.value = {}
//登录 if(onType.value == 1) { const cookieLoginInfo = proxy.VueCookies.get("loginInfo") if(cookieLoginInfo) { //"记住我"功能 formData.value = cookieLoginInfo } } }) }
|
发送验证码
切换验证码
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| //验证码 const checkCodeUrl = ref(api.checkCode) const checkCodeUrl4SendEmailCode = ref(api.checkCode)
const changeCheckCode = (type)=> { if (type == 0) { checkCodeUrl.value = api.checkCode + "?type=" + type + "&time=" + new Date().getTime()
} else { checkCodeUrl4SendEmailCode.value = api.checkCode + "?type=" + type + "&time=" + new Date().getTime() } }
|
邮箱验证码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| //获取邮箱验证码 const getEmailCode = async ()=> {
formDataRef.value.validateField("email" ,(valid) => {
if (!valid) { return; } //当邮箱存在时, 才会弹出获取邮箱验证码的弹窗 dialogConfig4SendEmailCode.value.show = true
nextTick(() => { changeCheckCode(1) formData4SendEmailCodeRef.value.resetFields(); formData4SendEmailCode.value = { email: formData.value.email } }) }) }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| //发送邮件 const sendEmailCode = () => { formData4SendEmailCodeRef.value.validate( async (valid) => { if(!valid) { return } const params = Object.assign({}, formData4SendEmailCode.value) params.type = onType.value == 0 ? 0 : 1 let result = await proxy.Request({ url: api.sendMailCode, params: params, errorCallback:() => { changeCheckCode(1) } }) if (!result) { return ; } //发送成功 proxy.Message.success("验证码发送成功, 请登录邮箱查看") dialogConfig4SendEmailCode.value.show = false }) }
|
存储用户信息, “记住我”功能pinia
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import { defineStore } from "pinia"; import {ref, computed} from 'vue'
//原版视频用的vuex, 这里为了练习,采用的pinia export const useUserStore = defineStore('user', () => {
const loginUserInfo =ref() function updateLoginUserInfo(value) { loginUserInfo.value = value } return { loginUserInfo, updateLoginUserInfo, } })
|
提交表单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| const doSubmit = () => { formDataRef.value.validate(async (valid) => { if (!valid) { return } let params = {} Object.assign(params, formData.value)
//注册或者重置密码都需要把密码改成现有的 if (onType.value == 0 || onType.value == 2) { //储存注册的密码 params.password = params.registerPassword delete params.registerPassword delete params.reRegisterPassword } else if(onType.value == 1) { let cookieLoginInfo = proxy.VueCookies.get("loginInfo") let cookiePassword = cookieLoginInfo == null ? null : cookieLoginInfo.password if(params.password !== cookiePassword) { params.password = md5(params.registerPassword || "") } }
let url = null if (onType.value == 0) { url = api.register } else if (onType.value == 1) { url = api.login } else if (onType.value == 2){ url = api.resetPwd } let result = await proxy.Request({ url: url, params: params, errorCallback: () => { changeCheckCode(0)//刷新验证码 } })
if (!result) { return } //注册返回 if (onType.value == 0) { proxy.Message.success("注册成功, 请登录") showPanel(1) //跳转到登录界面 } else if (onType.value == 1) { //登录返回 //登录 if (params.rememberMe) { //对应表单的"记住我" const loginInfo = { email: params.email, password: params.password, rememberMe: params.rememberMe } proxy.VueCookies.set("loginInfo", loginInfo, "7d") } else { proxy.VueCookies.remove("loginInfo") } dialogConfig.value.show = false proxy.Message.success("登录成功") } else if (onType.value == 2) { proxy.Message.success("重置密码成功, 请登录") //重置密码 showPanel(1) } }) }
|
main.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| import {createApp} from 'vue' import App from './App.vue' import router from './router/index.ts'
//引入pinia import {createPinia} from 'pinia' //引入cookies import VueCookies from 'vue-cookies' //vue-cookies提供了一个简洁、方便的方式来在 Vue 组件中设置、获取和删除浏览器 Cookie。 //引入element plus import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' //我们使用sass 所以这里将base.css 改成base.scss // import './assets/base.scss' import './assets/style.css' //图标 图标在附件中 import './assets/icon/iconfont.css' import Dialog from './components/Dialog.vue' import Verify from './utils/Verify.js' import Message from './utils/Message.js' import Request from './utils/Request.js' import Avatar from './components/Avatar.vue'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia) app.use(router) app.use(ElementPlus);
app.config.globalProperties.VueCookies = VueCookies; app.config.globalProperties.globalInfo = { bodyWidth: 1300, avatarUrl: "/api/file/getAvatar/" }
app.config.globalProperties.Verify = Verify app.config.globalProperties.Message = Message app.config.globalProperties.Request = Request
app.component('Dialog', Dialog ) app.component('Avatar', Avatar)
app.mount('#app')
|