459 lines
11 KiB
Vue
459 lines
11 KiB
Vue
|
<script lang="ts" setup>
|
|||
|
import {useToast} from "#imports";
|
|||
|
|
|||
|
import Vcode from "vue3-puzzle-vcode";
|
|||
|
|
|||
|
definePageMeta({
|
|||
|
layout: 'login',
|
|||
|
})
|
|||
|
import * as Yup from 'yup';
|
|||
|
import {Minus} from 'lucide-vue-next'
|
|||
|
import type {HttpType} from "~/types/baseType";
|
|||
|
|
|||
|
const toast = useToast()
|
|||
|
const form = reactive({
|
|||
|
username: '',
|
|||
|
nickname: '',
|
|||
|
email: '',
|
|||
|
phoneNumber: '',
|
|||
|
password: '',
|
|||
|
confirmPassword: '',
|
|||
|
remember: false
|
|||
|
});
|
|||
|
type FormErrorMessages = {
|
|||
|
[K in keyof typeof form]?: string;
|
|||
|
};
|
|||
|
const EmailDialog = ref(false);
|
|||
|
const isShow = ref(false);
|
|||
|
const errors = ref<FormErrorMessages>({});
|
|||
|
const code = ref<string>();
|
|||
|
const schema = Yup.object().shape({
|
|||
|
username: Yup.string()
|
|||
|
.required('用户名是必填项')
|
|||
|
.min(3, '用户名至少需要3个字符')
|
|||
|
.max(15, '用户名不能超过15个字符'),
|
|||
|
nickname: Yup.string()
|
|||
|
.max(15, '昵称不能超过15个字符')
|
|||
|
.required('昵称是必填项'),
|
|||
|
email: Yup.string()
|
|||
|
.email('邮箱地址格式不正确')
|
|||
|
.required('邮箱地址是必填项'),
|
|||
|
phoneNumber: Yup.string()
|
|||
|
.matches(/^1[3-9]\d{9}$/, '手机号码格式不正确'), // 这里使用了正则表达式匹配中国大陆的手机号码
|
|||
|
password: Yup.string()
|
|||
|
.required('密码是必填项')
|
|||
|
.min(6, '密码至少需要6个字符')
|
|||
|
.matches(/(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[!@#$%^&*()_+])[0-9a-zA-Z!@#$%^&*()_+]{6,}/, '密码必须包含字母、数字和至少一个特殊字符'),
|
|||
|
confirmPassword: Yup.string()
|
|||
|
.oneOf([Yup.ref('password')], '两次输入的密码必须一致')
|
|||
|
.required('请再次输入密码'),
|
|||
|
remember: Yup.boolean().oneOf([true], '请同意用户协议')
|
|||
|
});
|
|||
|
|
|||
|
const handleSubmit = async () => {
|
|||
|
try {
|
|||
|
// 验证表单
|
|||
|
await schema.validate(form, {abortEarly: false});
|
|||
|
errors.value = {}; // 清空错误信息
|
|||
|
isShow.value = true;
|
|||
|
|
|||
|
} catch (error) {
|
|||
|
// 处理验证错误
|
|||
|
if (error instanceof Yup.ValidationError) {
|
|||
|
const errorMessages: FormErrorMessages = {};
|
|||
|
error.inner.forEach((e) => {
|
|||
|
// 确保e.path是form对象的有效键
|
|||
|
if (e.path) {
|
|||
|
errorMessages[e.path as keyof typeof form] = e.message;
|
|||
|
}
|
|||
|
});
|
|||
|
errors.value = errorMessages;
|
|||
|
}
|
|||
|
}
|
|||
|
};
|
|||
|
const onSuccess = () => {
|
|||
|
// 表单验证通过,处理登录逻辑
|
|||
|
code.value = "";
|
|||
|
//请求验证码发送程序
|
|||
|
isShow.value = false;
|
|||
|
$fetch('/Api/Account/VerifyEmailName', {
|
|||
|
method: 'POST',
|
|||
|
body: {
|
|||
|
email: form.email,
|
|||
|
username: form.username
|
|||
|
},
|
|||
|
baseURL: useRuntimeConfig().public.baseUrl
|
|||
|
}).then(res => {
|
|||
|
const data = res as HttpType
|
|||
|
if (data.code === 200) {
|
|||
|
$fetch('/Api/Account/SendVerificationCode', {
|
|||
|
method: 'POST',
|
|||
|
body: {
|
|||
|
email: form.email
|
|||
|
},
|
|||
|
baseURL: useRuntimeConfig().public.baseUrl
|
|||
|
}).then(res => {
|
|||
|
toast.add({
|
|||
|
severity: 'success',
|
|||
|
summary: '验证码发送成功',
|
|||
|
detail: '请查收邮箱',
|
|||
|
life: 3000
|
|||
|
})
|
|||
|
EmailDialog.value = true;
|
|||
|
})
|
|||
|
} else {
|
|||
|
toast.add({
|
|||
|
severity: 'error',
|
|||
|
summary: data.message,
|
|||
|
detail: '错误',
|
|||
|
life: 3000
|
|||
|
})
|
|||
|
}
|
|||
|
})
|
|||
|
|
|||
|
|
|||
|
};
|
|||
|
const submitCode = () => {
|
|||
|
$fetch('/Api/Account/Register', {
|
|||
|
method: 'POST',
|
|||
|
body: {
|
|||
|
username: form.username,
|
|||
|
nickname: form.nickname,
|
|||
|
email: form.email,
|
|||
|
phone: form.phoneNumber,
|
|||
|
password: form.password,
|
|||
|
code: code.value
|
|||
|
},
|
|||
|
baseURL: useRuntimeConfig().public.baseUrl,
|
|||
|
}).then(res => {
|
|||
|
const data = res as HttpType<any>
|
|||
|
if (data.code === 200) {
|
|||
|
toast.add({
|
|||
|
severity: 'success',
|
|||
|
summary: '注册成功',
|
|||
|
detail: '即将跳转登录页面',
|
|||
|
life: 3000
|
|||
|
})
|
|||
|
setTimeout(() => {
|
|||
|
navigateTo('/SignIn')
|
|||
|
}, 1000)
|
|||
|
}
|
|||
|
})
|
|||
|
}
|
|||
|
</script>
|
|||
|
|
|||
|
<template>
|
|||
|
<div class="SignUp-Box">
|
|||
|
<Vcode :show="isShow" @success="onSuccess"/>
|
|||
|
<Dialog v-model:visible="EmailDialog" :style="{ width: '25rem' }" header="验证你的账户" modal>
|
|||
|
<div class="Email-Box">
|
|||
|
<h2>验证您的帐户</h2>
|
|||
|
<p>请输入发送到您邮箱的代码。</p>
|
|||
|
<div>
|
|||
|
<InputOtp v-model="code" :length="6" style="gap: 0">
|
|||
|
<template #default="{ attrs, events, index }">
|
|||
|
<input class="custom-otp-input" type="text" v-bind="attrs" v-on="events"/>
|
|||
|
<div v-if="index === 3" style="padding-inline: 5px">
|
|||
|
<Minus/>
|
|||
|
</div>
|
|||
|
</template>
|
|||
|
</InputOtp>
|
|||
|
</div>
|
|||
|
<div class="Email-Box-Action">
|
|||
|
<Button label="重新发送" link></Button>
|
|||
|
<Button label="提交验证码" @click.prevent="submitCode"></Button>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</Dialog>
|
|||
|
<div class="SignUp-Box-Header">
|
|||
|
<h1>注册</h1>
|
|||
|
<h3>创建你的帐户</h3>
|
|||
|
</div>
|
|||
|
<div class="SignUp-Box-Form">
|
|||
|
<form @submit.prevent="handleSubmit">
|
|||
|
<div class="From-Item">
|
|||
|
<div class="From-Group">
|
|||
|
<label for="username">用户名</label>
|
|||
|
<InputText id="username" v-model="form.username" aria-describedby="username-help"/>
|
|||
|
<small id="username-help" class="p-error">{{ errors.username }}</small>
|
|||
|
</div>
|
|||
|
<div class="From-Group">
|
|||
|
<label for="nickname">昵称</label>
|
|||
|
<InputText id="nickname" v-model="form.nickname" aria-describedby="username-help"/>
|
|||
|
<small id="username-help" class="p-error">{{ errors.nickname }}</small>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
<div class="From-Item">
|
|||
|
<div class="From-Group">
|
|||
|
<label for="email">邮箱</label>
|
|||
|
<InputText id="email" v-model="form.email" aria-describedby="username-help"/>
|
|||
|
<small id="username-help" class="p-error">{{ errors.email }}</small>
|
|||
|
</div>
|
|||
|
<div class="From-Group">
|
|||
|
<label for="phoneNumber">手机号</label>
|
|||
|
<InputText id="phoneNumber" v-model="form.phoneNumber" aria-describedby="username-help"/>
|
|||
|
<small id="username-help" class="p-error">{{ errors.phoneNumber }}</small>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
<div class="From-Item">
|
|||
|
<div class="From-Group">
|
|||
|
<label for="password">密码</label>
|
|||
|
<Password id="password" v-model="form.password" toggleMask>
|
|||
|
<template #header>
|
|||
|
<h6>检查你的密码</h6>
|
|||
|
</template>
|
|||
|
<template #footer>
|
|||
|
<Divider/>
|
|||
|
<p>需求</p>
|
|||
|
<ul>
|
|||
|
<li>至少一个特殊字符</li>
|
|||
|
<li>至少一个字母</li>
|
|||
|
<li>至少一个数字</li>
|
|||
|
<li>至少6个字符</li>
|
|||
|
</ul>
|
|||
|
</template>
|
|||
|
</Password>
|
|||
|
<small id="username-help" class="p-error">{{ errors.password }}</small>
|
|||
|
</div>
|
|||
|
<div class="From-Group">
|
|||
|
<label for="confirmPassword">确认密码</label>
|
|||
|
<Password id="confirmPassword" v-model="form.confirmPassword" :feedback="false" aria-describedby="username-help"
|
|||
|
toggleMask/>
|
|||
|
<small id="username-help" class="p-error">{{ errors.confirmPassword }}</small>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
<div class="From-Check">
|
|||
|
<div>
|
|||
|
<Checkbox v-model="form.remember" :binary="true" inputId="remember" name="remember"/>
|
|||
|
<label class="ml-2" for="remember"> 我同意使用条款</label>
|
|||
|
<small id="username-help" class="p-error">{{ errors.remember }}</small>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
<div class="From-Action">
|
|||
|
<Button label="注册" type="submit"/>
|
|||
|
</div>
|
|||
|
</form>
|
|||
|
</div>
|
|||
|
<div class="SignIn-Box-Bottom">
|
|||
|
<p>还是使用其他帐户登录?</p>
|
|||
|
<div>
|
|||
|
<NuxtImg height="40" src="/Gmail.svg" width="40"/>
|
|||
|
<NuxtImg height="40" src="/Facebook.svg" width="40"/>
|
|||
|
<NuxtImg height="40" src="/Instagram.svg" width="40"/>
|
|||
|
<NuxtImg height="40" src="/Linkedin.svg" width="40"/>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</template>
|
|||
|
|
|||
|
<style lang="scss" scoped>
|
|||
|
@import "base";
|
|||
|
|
|||
|
.SignUp-Box {
|
|||
|
display: flex;
|
|||
|
flex-direction: column;
|
|||
|
justify-content: center;
|
|||
|
gap: $gap*2;
|
|||
|
|
|||
|
> * {
|
|||
|
transition: all 0.3s ease;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
.SignUp-Box-Header {
|
|||
|
display: flex;
|
|||
|
justify-content: center;
|
|||
|
flex-direction: column;
|
|||
|
align-items: center;
|
|||
|
gap: $gap*2;
|
|||
|
|
|||
|
h3 {
|
|||
|
color: $unfocused-color;
|
|||
|
font-size: 16px;
|
|||
|
font-style: normal;
|
|||
|
font-weight: 400;
|
|||
|
line-height: 175%; /* 28px */
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
.SignUp-Box-Form {
|
|||
|
display: flex;
|
|||
|
align-items: center;
|
|||
|
|
|||
|
form {
|
|||
|
width: 100%;
|
|||
|
display: flex;
|
|||
|
gap: $gap*2;
|
|||
|
flex-direction: column;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
.Email-Box {
|
|||
|
display: flex;
|
|||
|
flex-direction: column;
|
|||
|
gap: $gap*2;
|
|||
|
justify-content: center;
|
|||
|
align-items: center;
|
|||
|
|
|||
|
> p {
|
|||
|
color: $light-unfocused-color;
|
|||
|
font-size: 16px;
|
|||
|
margin-top: -10px;
|
|||
|
}
|
|||
|
|
|||
|
.Email-Box-Action {
|
|||
|
display: flex;
|
|||
|
gap: $gap;
|
|||
|
width: 100%;
|
|||
|
|
|||
|
> button {
|
|||
|
width: 100%;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
.From-Item {
|
|||
|
display: flex;
|
|||
|
align-items: center;
|
|||
|
gap: $gap;
|
|||
|
justify-content: space-between;
|
|||
|
}
|
|||
|
|
|||
|
.From-Group {
|
|||
|
display: flex;
|
|||
|
flex-direction: column;
|
|||
|
gap: $gap;
|
|||
|
|
|||
|
small {
|
|||
|
max-height: 5px;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
.From-Group {
|
|||
|
display: flex;
|
|||
|
flex-direction: column;
|
|||
|
gap: $gap;
|
|||
|
width: 240px;
|
|||
|
|
|||
|
label {
|
|||
|
color: $unfocused-color;
|
|||
|
font-size: 16px;
|
|||
|
font-style: normal;
|
|||
|
font-weight: 400;
|
|||
|
line-height: 175%; /* 28px */
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
.From-Check {
|
|||
|
display: flex;
|
|||
|
justify-content: center;
|
|||
|
color: $unfocused-color;
|
|||
|
}
|
|||
|
|
|||
|
.From-Action {
|
|||
|
display: flex;
|
|||
|
justify-content: center;
|
|||
|
margin-top: $padding*.5;
|
|||
|
|
|||
|
.p-button {
|
|||
|
display: flex;
|
|||
|
width: 100%;
|
|||
|
padding: $padding*.75;
|
|||
|
justify-content: center;
|
|||
|
align-items: center;
|
|||
|
border-radius: $radius;
|
|||
|
background: $primary-color;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
.SignIn-Box-Bottom {
|
|||
|
display: flex;
|
|||
|
gap: $gap*2;
|
|||
|
flex-direction: column;
|
|||
|
align-items: center;
|
|||
|
justify-content: center;
|
|||
|
|
|||
|
> p {
|
|||
|
color: $light-text-color;
|
|||
|
font-size: 16px;
|
|||
|
}
|
|||
|
|
|||
|
> div {
|
|||
|
display: flex;
|
|||
|
height: 30px;
|
|||
|
overflow: hidden;
|
|||
|
gap: 24px;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
.custom-otp-input {
|
|||
|
width: 48px;
|
|||
|
height: 48px;
|
|||
|
font-size: 24px;
|
|||
|
appearance: none;
|
|||
|
text-align: center;
|
|||
|
border-radius: 0;
|
|||
|
border: 1px solid var(--surface-400);
|
|||
|
background: transparent;
|
|||
|
outline-offset: -2px;
|
|||
|
outline-color: transparent;
|
|||
|
border-right: 0 none;
|
|||
|
transition: outline-color 0.3s;
|
|||
|
color: var(--text-color);
|
|||
|
}
|
|||
|
|
|||
|
.custom-otp-input:focus {
|
|||
|
outline: 2px solid var(--primary-color);
|
|||
|
}
|
|||
|
|
|||
|
.custom-otp-input:first-child,
|
|||
|
.custom-otp-input:nth-child(5) {
|
|||
|
border-top-left-radius: 12px;
|
|||
|
border-bottom-left-radius: 12px;
|
|||
|
}
|
|||
|
|
|||
|
.custom-otp-input:nth-child(3),
|
|||
|
.custom-otp-input:last-child {
|
|||
|
border-top-right-radius: 12px;
|
|||
|
border-bottom-right-radius: 12px;
|
|||
|
border-right-width: 1px;
|
|||
|
border-right-style: solid;
|
|||
|
|
|||
|
border-color: var(--surface-400);
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
:deep(.p-input-icon) {
|
|||
|
top: 30%
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
//primeVue
|
|||
|
:deep(.p-inputtext) {
|
|||
|
padding: $padding*.5 $padding;
|
|||
|
border-radius: $radius;
|
|||
|
width: 100%;
|
|||
|
border: 1px solid $primary-color;
|
|||
|
|
|||
|
&:enabled:hover {
|
|||
|
border: 1px solid $primary-color;
|
|||
|
box-shadow: 0 0 0 2px $primary-color;
|
|||
|
}
|
|||
|
|
|||
|
&:enabled:focus {
|
|||
|
outline: 2px solid $primary-color;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
:deep(.p-password-panel) {
|
|||
|
padding: $padding;
|
|||
|
background: red;
|
|||
|
}
|
|||
|
|
|||
|
.dark-mode {
|
|||
|
h1, h2, h3, p {
|
|||
|
color: $dark-text-color;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
</style>
|