LoongPanel-Asp/web/pages/SignUp.vue

459 lines
11 KiB
Vue
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>