登录注册表单

前言
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、在邮箱中 &it;&it;头像->设互->反垃圾->白名单->设苣部件地址白名单</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')