LoongPanel-Asp/web/pages/SignUp.vue

459 lines
11 KiB
Vue
Raw Normal View History

2024-06-22 10:54:02 +08:00
<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>