459 lines
11 KiB
Vue
Executable File
459 lines
11 KiB
Vue
Executable File
<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> |