feat: 新增邮箱注册

This commit is contained in:
何嘉悦 2025-06-03 17:52:42 +08:00
parent 67f6bad76b
commit cf1ee9cbb0
9 changed files with 305 additions and 22 deletions

View File

@ -1,4 +1,10 @@
import type { LoginDTO, LoginVO } from './types'; import type { EmailCodeDTO, LoginDTO, LoginVO, RegisterDTO } from './types';
import { post } from '@/utils/request'; import { post } from '@/utils/request';
export const login = (data: LoginDTO) => post<LoginVO>('/auth/login', data); export const login = (data: LoginDTO) => post<LoginVO>('/auth/login', data);
// 邮箱验证码
export const emailCode = (data: EmailCodeDTO) => post('/resource/email/code', data);
// 注册账号
export const register = (data: RegisterDTO) => post('/auth/register', data);

View File

@ -1,6 +1,9 @@
export interface LoginDTO { export interface LoginDTO {
username: string; username: string;
password: string; password: string;
code?: string;
// 二次确认密码
confirmPassword?: string;
} }
export interface LoginVO { export interface LoginVO {
@ -116,3 +119,28 @@ export interface RoleDTO {
*/ */
roleName?: string; roleName?: string;
} }
// 邮箱验证码
export interface EmailCodeDTO {
username?: string;
}
// 邮箱注册
export interface RegisterDTO {
/**
*
*/
username: string;
/**
*
*/
password: string;
/**
*
*/
code: string;
/**
*
*/
confirmPassword?: string;
}

View File

@ -6,10 +6,12 @@ import { reactive, ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { login } from '@/api'; import { login } from '@/api';
import { useUserStore } from '@/stores'; import { useUserStore } from '@/stores';
import { useLoginFromStore } from '@/stores/modules/loginFrom';
import { useSessionStore } from '@/stores/modules/session'; import { useSessionStore } from '@/stores/modules/session';
const userStore = useUserStore(); const userStore = useUserStore();
const sessionStore = useSessionStore(); const sessionStore = useSessionStore();
const loginFromStore = useLoginFromStore();
const formRef = ref<FormInstance>(); const formRef = ref<FormInstance>();
@ -45,9 +47,15 @@ async function handleSubmit() {
<template> <template>
<div class="custom-form"> <div class="custom-form">
<el-form ref="formRef" :model="formModel" :rules="rules" style="width: 230px"> <el-form
ref="formRef"
:model="formModel"
:rules="rules"
style="width: 230px"
@submit.prevent="handleSubmit"
>
<el-form-item prop="username"> <el-form-item prop="username">
<el-input v-model="formModel.username" placeholder="请输入用户名" clearable> <el-input v-model="formModel.username" placeholder="请输入用户名">
<template #prefix> <template #prefix>
<el-icon> <el-icon>
<User /> <User />
@ -59,7 +67,6 @@ async function handleSubmit() {
<el-input <el-input
v-model="formModel.password" v-model="formModel.password"
placeholder="请输入密码" placeholder="请输入密码"
clearable
type="password" type="password"
show-password show-password
> >
@ -71,11 +78,22 @@ async function handleSubmit() {
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" style="width: 100%" @click="handleSubmit"> <el-button type="primary" style="width: 100%" native-type="submit">
登录 登录
</el-button> </el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
<!-- 注册登录 -->
<div class="form-tip font-size-12px flex items-center">
<span>没有账号</span>
<span
class="c-[var(--el-color-primar,#409eff)] cursor-pointer"
@click="loginFromStore.setLoginFormType('RegistrationForm')"
>
立即注册
</span>
</div>
</div> </div>
</template> </template>

View File

@ -0,0 +1,200 @@
<!-- 注册表单 -->
<script lang="ts" setup>
import type { FormInstance, FormRules } from 'element-plus';
import type { RegisterDTO } from '@/api/auth/types';
import { useCountdown } from '@vueuse/core';
import { reactive, ref } from 'vue';
import { emailCode, register } from '@/api';
import { useLoginFromStore } from '@/stores/modules/loginFrom';
const loginFromStore = useLoginFromStore();
const countdown = shallowRef(60);
const { start, stop, resume } = useCountdown(countdown, {
onComplete() {
resume();
},
onTick() {
countdown.value--;
},
});
const formRef = ref<FormInstance>();
const formModel = ref<RegisterDTO>({
username: '',
password: '',
code: '',
confirmPassword: '',
});
const rules = reactive<FormRules<RegisterDTO>>({
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
confirmPassword: [
{ required: true, message: '请输入确认密码', trigger: 'blur' },
{
validator: (_, value) => {
if (value !== formModel.value.password) {
return new Error('两次输入的密码不一致');
}
return true;
},
trigger: 'change',
},
],
username: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{
validator: (_, value) => {
if (!isEmail(value)) {
return new Error('请输入正确的邮箱');
}
return true;
},
trigger: 'blur',
},
],
});
function isEmail(email: string) {
const emailRegex = /^[\w.-]+@[a-z0-9.-]+\.[a-z]{2,4}$/i;
return emailRegex.test(email);
}
async function handleSubmit() {
try {
await formRef.value?.validate();
const params: RegisterDTO = {
username: formModel.value.username,
password: formModel.value.password,
code: formModel.value.code,
};
await register(params);
ElMessage.success('注册成功');
formRef.value?.resetFields();
resume();
}
catch (error) {
console.error('请求错误:', error);
}
}
//
async function getEmailCode() {
if (formModel.value.username === '') {
ElMessage.error('请输入邮箱');
return;
}
if (!isEmail(formModel.value.username)) {
return;
}
if (countdown.value > 0 && countdown.value < 60) {
return;
}
try {
start();
await emailCode({ username: formModel.value.username });
ElMessage.success('验证码发送成功');
}
catch (error) {
console.error('请求错误:', error);
stop();
}
}
</script>
<template>
<div class="custom-form">
<el-form
ref="formRef"
:model="formModel"
:rules="rules"
style="width: 230px"
@submit.prevent="handleSubmit"
>
<el-form-item prop="username">
<el-input v-model="formModel.username" placeholder="请输入邮箱" autocomplete="off">
<template #prefix>
<el-icon>
<Message />
</el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item prop="code">
<el-input v-model="formModel.code" placeholder="请输入验证码" autocomplete="off">
<template #prefix>
<el-icon>
<Bell />
</el-icon>
</template>
<template #suffix>
<div class="font-size-14px cursor-pointer bg-[var(0,0,0,0.4)]" @click="getEmailCode">
{{ countdown === 0 || countdown === 60 ? "获取验证码" : `${countdown} s` }}
</div>
</template>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input v-model="formModel.password" placeholder="请输入密码" autocomplete="off">
<template #prefix>
<el-icon>
<Unlock />
</el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item prop="confirmPassword">
<el-input v-model="formModel.confirmPassword" placeholder="请确认密码" autocomplete="off">
<template #prefix>
<el-icon>
<Lock />
</el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" style="width: 100%" native-type="submit">
注册
</el-button>
</el-form-item>
</el-form>
<!-- 返回登录 -->
<div class="form-tip font-size-12px flex items-center">
<span>已有账号</span>
<span
class="c-[var(--el-color-primar,#409eff)] cursor-pointer"
@click="loginFromStore.setLoginFormType('AccountPassword')"
>
返回登录
</span>
</div>
</div>
</template>
<style scoped lang="scss">
.custom-form {
display: flex;
flex-direction: column;
gap: 16px;
}
.form-group {
display: flex;
align-items: center;
gap: 8px;
}
.login-btn {
margin-top: 24px;
padding: 12px;
background: #409eff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>

View File

@ -3,10 +3,15 @@ import { ref, watch } from 'vue';
import logoPng from '@/assets/images/logo.png'; import logoPng from '@/assets/images/logo.png';
import SvgIcon from '@/components/SvgIcon/index.vue'; import SvgIcon from '@/components/SvgIcon/index.vue';
import { useUserStore } from '@/stores'; import { useUserStore } from '@/stores';
import { useLoginFromStore } from '@/stores/modules/loginFrom';
import AccountPassword from './components/FormLogin/AccountPassword.vue'; import AccountPassword from './components/FormLogin/AccountPassword.vue';
import RegistrationForm from './components/FormLogin/RegistrationForm.vue';
import QrCodeLogin from './components/QrCodeLogin/index.vue'; import QrCodeLogin from './components/QrCodeLogin/index.vue';
const userStore = useUserStore(); const userStore = useUserStore();
const loginFromStore = useLoginFromStore();
const loginFormType = computed(() => loginFromStore.LoginFormType);
// 使 defineModel visible Vue 3.4+ // 使 defineModel visible Vue 3.4+
const visible = defineModel<boolean>('visible'); const visible = defineModel<boolean>('visible');
@ -27,7 +32,7 @@ watch(
{ immediate: true }, { immediate: true },
); );
// //
function toggleLoginMode() { function toggleLoginMode() {
isQrMode.value = !isQrMode.value; isQrMode.value = !isQrMode.value;
} }
@ -56,7 +61,7 @@ function onAfterLeave() {
<div class="left-section"> <div class="left-section">
<div class="logo-wrap"> <div class="logo-wrap">
<img :src="logoPng" class="logo-img"> <img :src="logoPng" class="logo-img">
<span class="logo-text"> Element Plus X</span> <span class="logo-text">Element Plus X</span>
</div> </div>
<div class="ad-banner"> <div class="ad-banner">
<SvgIcon name="p-bangong" class-name="animate-up-down" /> <SvgIcon name="p-bangong" class-name="animate-up-down" />
@ -68,18 +73,29 @@ function onAfterLeave() {
<SvgIcon v-else name="zhanghaodenglu" /> <SvgIcon v-else name="zhanghaodenglu" />
</div> </div>
<div class="content-wrapper"> <div class="content-wrapper">
<span class="content-title"> 登录后免费使用完整功能 </span>
<div v-if="!isQrMode" class="form-box"> <div v-if="!isQrMode" class="form-box">
<!-- 表单容器父组件可以自定定义表单插槽 --> <!-- 表单容器父组件可以自定定义表单插槽 -->
<slot name="form"> <slot name="form">
<!-- 父组件不用插槽则显示默认表单 默认账号密码登录 --> <!-- 父组件不用插槽则显示默认表单 默认使用 AccountPassword 组件 -->
<div class="form-container"> <div v-if="loginFormType === 'AccountPassword'" class="form-container">
<span class="content-title"> 登录后免费使用完整功能 </span>
<el-divider content-position="center"> <el-divider content-position="center">
账号密码登录 账号密码登录
</el-divider> </el-divider>
<AccountPassword /> <AccountPassword />
</div> </div>
<div v-if="loginFormType === 'RegistrationForm'" class="form-container">
<span class="content-title"> 登录后免费使用完整功能 </span>
<el-divider content-position="center">
邮箱注册账号
</el-divider>
<RegistrationForm />
</div>
</slot> </slot>
</div> </div>
<div v-else class="qr-container"> <div v-else class="qr-container">
@ -216,14 +232,12 @@ function onAfterLeave() {
} }
.right-section .content-title { .right-section .content-title {
position: absolute; display: flex;
width: 100%; width: 100%;
font-size: 20px; font-size: 20px;
font-weight: 700; font-weight: 700;
display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
top: 55px;
} }
.right-section .mode-toggle { .right-section .mode-toggle {

View File

@ -33,18 +33,18 @@ export function useWindowWidthObserver(
case 'alwaysExpanded': case 'alwaysExpanded':
designStore.setCollapse(false); designStore.setCollapse(false);
if (isAbove) { if (isAbove) {
// 大于的时候执行关闭动画 // 大于的时候执行关闭动画 (豆包是有的,第一版本暂未添加)
console.log('执行关闭动画'); console.log('执行关闭动画');
} }
else { else {
// 小于的时候执行打开动画 // 小于的时候执行打开动画 (豆包是有的,第一版本暂未添加)
console.log('小于的时候执行打开动画'); console.log('小于的时候执行打开动画');
} }
break; break;
case 'narrowExpandWideCollapse': case 'narrowExpandWideCollapse':
designStore.setCollapse(isAbove); designStore.setCollapse(isAbove);
} }
console.log('最终的折叠状态:', designStore.isCollapse); // console.log('最终的折叠状态:', designStore.isCollapse);
if (!designStore.isCollapse) { if (!designStore.isCollapse) {
document.documentElement.style.setProperty( document.documentElement.style.setProperty(

View File

@ -20,10 +20,6 @@ const router = createRouter({
scrollBehavior: () => ({ left: 0, top: 0 }), scrollBehavior: () => ({ left: 0, top: 0 }),
}); });
console.group('Routes');
console.log('routes', router.getRoutes());
console.groupEnd();
// 路由前置守卫 // 路由前置守卫
router.beforeEach( router.beforeEach(
async ( async (

View File

@ -0,0 +1,18 @@
// 登录表单状态管理
import { defineStore } from 'pinia';
type LoginFormType = 'AccountPassword' | 'VerificationCode' | 'RegistrationForm';
export const useLoginFromStore = defineStore('loginFrom', () => {
const LoginFormType = ref<LoginFormType>('AccountPassword');
// 设置登录表单类型
const setLoginFormType = (type: LoginFormType) => {
LoginFormType.value = type;
};
return {
LoginFormType,
setLoginFormType,
};
});

View File

@ -3,7 +3,7 @@
// Generated by unplugin-vue-components // Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399 // Read more: https://github.com/vuejs/core/pull/3399
// biome-ignore lint: disable // biome-ignore lint: disable
export {} export {};
/* prettier-ignore */ /* prettier-ignore */
declare module 'vue' { declare module 'vue' {
@ -11,6 +11,7 @@ declare module 'vue' {
AccountPassword: typeof import('./../src/components/LoginDialog/components/FormLogin/AccountPassword.vue')['default'] AccountPassword: typeof import('./../src/components/LoginDialog/components/FormLogin/AccountPassword.vue')['default']
DeepThinking: typeof import('./../src/components/DeepThinking/index.vue')['default'] DeepThinking: typeof import('./../src/components/DeepThinking/index.vue')['default']
ElAvatar: typeof import('element-plus/es')['ElAvatar'] ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElButtom: typeof import('element-plus/es')['ElButtom']
ElButton: typeof import('element-plus/es')['ElButton'] ElButton: typeof import('element-plus/es')['ElButton']
ElContainer: typeof import('element-plus/es')['ElContainer'] ElContainer: typeof import('element-plus/es')['ElContainer']
ElDivider: typeof import('element-plus/es')['ElDivider'] ElDivider: typeof import('element-plus/es')['ElDivider']
@ -21,6 +22,7 @@ declare module 'vue' {
ElIcon: typeof import('element-plus/es')['ElIcon'] ElIcon: typeof import('element-plus/es')['ElIcon']
ElImage: typeof import('element-plus/es')['ElImage'] ElImage: typeof import('element-plus/es')['ElImage']
ElInput: typeof import('element-plus/es')['ElInput'] ElInput: typeof import('element-plus/es')['ElInput']
ElLink: typeof import('element-plus/es')['ElLink']
ElMain: typeof import('element-plus/es')['ElMain'] ElMain: typeof import('element-plus/es')['ElMain']
FilesSelect: typeof import('./../src/components/FilesSelect/index.vue')['default'] FilesSelect: typeof import('./../src/components/FilesSelect/index.vue')['default']
IconSelect: typeof import('./../src/components/IconSelect/index.vue')['default'] IconSelect: typeof import('./../src/components/IconSelect/index.vue')['default']
@ -28,6 +30,7 @@ declare module 'vue' {
ModelSelect: typeof import('./../src/components/ModelSelect/index.vue')['default'] ModelSelect: typeof import('./../src/components/ModelSelect/index.vue')['default']
Popover: typeof import('./../src/components/Popover/index.vue')['default'] Popover: typeof import('./../src/components/Popover/index.vue')['default']
QrCodeLogin: typeof import('./../src/components/LoginDialog/components/QrCodeLogin/index.vue')['default'] QrCodeLogin: typeof import('./../src/components/LoginDialog/components/QrCodeLogin/index.vue')['default']
RegistrationForm: typeof import('./../src/components/LoginDialog/components/FormLogin/RegistrationForm.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
SvgIcon: typeof import('./../src/components/SvgIcon/index.vue')['default'] SvgIcon: typeof import('./../src/components/SvgIcon/index.vue')['default']