初始化

This commit is contained in:
2026-03-03 09:53:54 +08:00
commit 3f349a35a4
437 changed files with 65639 additions and 0 deletions

View File

@@ -0,0 +1,62 @@
<template>
<div class="flex w-full h-screen">
<LoginLeftView />
<div class="relative flex-1">
<AuthTopBar />
<div class="auth-right-wrap">
<div class="form">
<h3 class="title">{{ $t('forgetPassword.title') }}</h3>
<p class="sub-title">{{ $t('forgetPassword.subTitle') }}</p>
<div class="mt-5">
<span class="input-label" v-if="showInputLabel">账号</span>
<ElInput
class="custom-height"
:placeholder="$t('forgetPassword.placeholder')"
v-model.trim="username"
/>
</div>
<div style="margin-top: 15px">
<ElButton
class="w-full custom-height"
type="primary"
@click="register"
:loading="loading"
v-ripple
>
{{ $t('forgetPassword.submitBtnText') }}
</ElButton>
</div>
<div style="margin-top: 15px">
<ElButton class="w-full custom-height" plain @click="toLogin">
{{ $t('forgetPassword.backBtnText') }}
</ElButton>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
defineOptions({ name: 'ForgetPassword' })
const router = useRouter()
const showInputLabel = ref(false)
const username = ref('')
const loading = ref(false)
const register = async () => {}
const toLogin = () => {
router.push({ name: 'Login' })
}
</script>
<style scoped>
@import '../login/style.css';
</style>

View File

@@ -0,0 +1,216 @@
<!-- 登录页面 -->
<template>
<div class="flex w-full h-screen">
<LoginLeftView />
<div class="relative flex-1">
<AuthTopBar />
<div class="auth-right-wrap">
<div class="form">
<h3 class="title">{{ $t('login.title') }}</h3>
<p class="sub-title">{{ $t('login.subTitle') }}</p>
<ElForm
ref="formRef"
:model="formData"
:rules="rules"
:key="formKey"
@keyup.enter="handleSubmit"
style="margin-top: 25px"
>
<ElFormItem prop="username">
<ElInput
class="custom-height"
:placeholder="$t('login.placeholder.username')"
v-model.trim="formData.username"
/>
</ElFormItem>
<ElFormItem prop="password">
<ElInput
class="custom-height"
:placeholder="$t('login.placeholder.password')"
v-model.trim="formData.password"
type="password"
autocomplete="off"
show-password
/>
</ElFormItem>
<ElFormItem prop="code">
<ElInput
class="custom-height"
:placeholder="$t('login.placeholder.code')"
v-model.trim="formData.code"
type="text"
autocomplete="off"
>
<template #append>
<img
:src="captcha"
style="height: 36px; cursor: pointer"
@click="refreshCaptcha"
/>
</template>
</ElInput>
</ElFormItem>
<div class="flex-cb mt-2 text-sm">
<ElCheckbox v-model="formData.rememberPassword">{{
$t('login.rememberPwd')
}}</ElCheckbox>
<!-- <RouterLink class="text-theme" :to="{ name: 'ForgetPassword' }">{{
$t('login.forgetPwd')
}}</RouterLink> -->
</div>
<div style="margin-top: 30px">
<ElButton
class="w-full custom-height"
type="primary"
@click="handleSubmit"
:loading="loading"
v-ripple
>
{{ $t('login.btnText') }}
</ElButton>
</div>
<!-- <div class="mt-5 text-sm text-gray-600">
<span>{{ $t('login.noAccount') }}</span>
<RouterLink class="text-theme" :to="{ name: 'Register' }">{{
$t('login.register')
}}</RouterLink>
</div> -->
</ElForm>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import AppConfig from '@/config'
import { useUserStore } from '@/store/modules/user'
import { useI18n } from 'vue-i18n'
import { HttpError } from '@/utils/http/error'
import { fetchCaptcha, fetchLogin, fetchGetUserInfo } from '@/api/auth'
import { ElNotification, type FormInstance, type FormRules } from 'element-plus'
defineOptions({ name: 'Login' })
const { t, locale } = useI18n()
const formKey = ref(0)
// 监听语言切换,重置表单
watch(locale, () => {
formKey.value++
})
const userStore = useUserStore()
const router = useRouter()
const captcha = ref(
'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
)
const systemName = AppConfig.systemInfo.name
const formRef = ref<FormInstance>()
const formData = reactive({
username: '',
password: '',
code: '',
uuid: '',
rememberPassword: true
})
const rules = computed<FormRules>(() => ({
username: [{ required: true, message: t('login.placeholder.username'), trigger: 'blur' }],
password: [{ required: true, message: t('login.placeholder.password'), trigger: 'blur' }],
code: [{ required: true, message: t('login.placeholder.code'), trigger: 'blur' }]
}))
const loading = ref(false)
onMounted(() => {
refreshCaptcha()
})
// 登录
const handleSubmit = async () => {
if (!formRef.value) return
try {
// 表单验证
const valid = await formRef.value.validate()
if (!valid) return
loading.value = true
// 登录请求
const { access_token, refresh_token } = await fetchLogin({
username: formData.username,
password: formData.password,
code: formData.code,
uuid: formData.uuid
})
// 验证token
if (!access_token) {
throw new Error('Login failed - no token received')
}
// 存储token和用户信息
userStore.setToken(access_token, refresh_token)
const userInfo = await fetchGetUserInfo()
userStore.setUserInfo(userInfo)
userStore.setLoginStatus(true)
// 登录成功处理
showLoginSuccessNotice()
router.push('/')
} catch (error) {
// 处理 HttpError
if (error instanceof HttpError) {
// console.log(error.code)
} else {
// 处理非 HttpError
// ElMessage.error('登录失败,请稍后重试')
console.error('[Login] Unexpected error:', error)
}
} finally {
refreshCaptcha()
loading.value = false
}
}
// 获取验证码
const refreshCaptcha = async () => {
fetchCaptcha().then((res) => {
formData.uuid = res.uuid
captcha.value = res.image
})
}
// 登录成功提示
const showLoginSuccessNotice = () => {
setTimeout(() => {
ElNotification({
title: t('login.success.title'),
type: 'success',
duration: 2500,
zIndex: 10000,
message: `${t('login.success.message')}, ${systemName}!`
})
}, 150)
}
</script>
<style scoped>
@import './style.css';
</style>
<style lang="scss" scoped>
:deep(.el-select__wrapper) {
height: 40px !important;
}
</style>

View File

@@ -0,0 +1,38 @@
@reference '@styles/core/tailwind.css';
/* 授权页右侧区域 */
.auth-right-wrap {
@apply absolute inset-0 w-[440px] h-[650px] py-[5px] m-auto overflow-hidden
max-sm:px-7 max-sm:w-full
animate-[slideInRight_0.6s_cubic-bezier(0.25,0.46,0.45,0.94)_forwards]
max-md:animate-none;
.form {
@apply h-full py-[40px];
}
.title {
@apply text-g-900 text-4xl font-semibold max-md:text-3xl max-sm:pt-10;
}
.sub-title {
@apply mt-[10px] text-g-600 text-sm;
}
.custom-height {
@apply !h-[40px];
}
}
/* 滑入动画 */
@keyframes slideInRight {
from {
opacity: 0;
transform: translateX(30px);
}
to {
opacity: 1;
transform: translateX(0);
}
}

View File

@@ -0,0 +1,240 @@
<!-- 注册页面 -->
<template>
<div class="flex w-full h-screen">
<LoginLeftView />
<div class="relative flex-1">
<AuthTopBar />
<div class="auth-right-wrap">
<div class="form">
<h3 class="title">{{ $t('register.title') }}</h3>
<p class="sub-title">{{ $t('register.subTitle') }}</p>
<ElForm
class="mt-7.5"
ref="formRef"
:model="formData"
:rules="rules"
label-position="top"
:key="formKey"
>
<ElFormItem prop="username">
<ElInput
class="custom-height"
v-model.trim="formData.username"
:placeholder="$t('register.placeholder.username')"
/>
</ElFormItem>
<ElFormItem prop="password">
<ElInput
class="custom-height"
v-model.trim="formData.password"
:placeholder="$t('register.placeholder.password')"
type="password"
autocomplete="off"
show-password
/>
</ElFormItem>
<ElFormItem prop="confirmPassword">
<ElInput
class="custom-height"
v-model.trim="formData.confirmPassword"
:placeholder="$t('register.placeholder.confirmPassword')"
type="password"
autocomplete="off"
@keyup.enter="register"
show-password
/>
</ElFormItem>
<ElFormItem prop="agreement">
<ElCheckbox v-model="formData.agreement">
{{ $t('register.agreeText') }}
<RouterLink
style="color: var(--theme-color); text-decoration: none"
to="/privacy-policy"
>{{ $t('register.privacyPolicy') }}</RouterLink
>
</ElCheckbox>
</ElFormItem>
<div style="margin-top: 15px">
<ElButton
class="w-full custom-height"
type="primary"
@click="register"
:loading="loading"
v-ripple
>
{{ $t('register.submitBtnText') }}
</ElButton>
</div>
<div class="mt-5 text-sm text-g-600">
<span>{{ $t('register.hasAccount') }}</span>
<RouterLink class="text-theme" :to="{ name: 'Login' }">{{
$t('register.toLogin')
}}</RouterLink>
</div>
</ElForm>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import type { FormInstance, FormRules } from 'element-plus'
defineOptions({ name: 'Register' })
interface RegisterForm {
username: string
password: string
confirmPassword: string
agreement: boolean
}
const USERNAME_MIN_LENGTH = 3
const USERNAME_MAX_LENGTH = 20
const PASSWORD_MIN_LENGTH = 6
const REDIRECT_DELAY = 1000
const { t, locale } = useI18n()
const router = useRouter()
const formRef = ref<FormInstance>()
const loading = ref(false)
const formKey = ref(0)
// 监听语言切换,重置表单
watch(locale, () => {
formKey.value++
})
const formData = reactive<RegisterForm>({
username: '',
password: '',
confirmPassword: '',
agreement: false
})
/**
* 验证密码
* 当密码输入后,如果确认密码已填写,则触发确认密码的验证
*/
const validatePassword = (_rule: any, value: string, callback: (error?: Error) => void) => {
if (!value) {
callback(new Error(t('register.placeholder.password')))
return
}
if (formData.confirmPassword) {
formRef.value?.validateField('confirmPassword')
}
callback()
}
/**
* 验证确认密码
* 检查确认密码是否与密码一致
*/
const validateConfirmPassword = (
_rule: any,
value: string,
callback: (error?: Error) => void
) => {
if (!value) {
callback(new Error(t('register.rule.confirmPasswordRequired')))
return
}
if (value !== formData.password) {
callback(new Error(t('register.rule.passwordMismatch')))
return
}
callback()
}
/**
* 验证用户协议
* 确保用户已勾选同意协议
*/
const validateAgreement = (_rule: any, value: boolean, callback: (error?: Error) => void) => {
if (!value) {
callback(new Error(t('register.rule.agreementRequired')))
return
}
callback()
}
const rules = computed<FormRules<RegisterForm>>(() => ({
username: [
{ required: true, message: t('register.placeholder.username'), trigger: 'blur' },
{
min: USERNAME_MIN_LENGTH,
max: USERNAME_MAX_LENGTH,
message: t('register.rule.usernameLength'),
trigger: 'blur'
}
],
password: [
{ required: true, validator: validatePassword, trigger: 'blur' },
{ min: PASSWORD_MIN_LENGTH, message: t('register.rule.passwordLength'), trigger: 'blur' }
],
confirmPassword: [{ required: true, validator: validateConfirmPassword, trigger: 'blur' }],
agreement: [{ validator: validateAgreement, trigger: 'change' }]
}))
/**
* 注册用户
* 验证表单后提交注册请求
*/
const register = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
loading.value = true
// TODO: 替换为真实 API 调用
// const params = {
// username: formData.username,
// password: formData.password
// }
// const res = await AuthService.register(params)
// if (res.code === ApiStatus.success) {
// ElMessage.success('注册成功')
// toLogin()
// }
// 模拟注册请求
setTimeout(() => {
loading.value = false
ElMessage.success('注册成功')
toLogin()
}, REDIRECT_DELAY)
} catch (error) {
console.error('表单验证失败:', error)
loading.value = false
}
}
/**
* 跳转到登录页面
*/
const toLogin = () => {
setTimeout(() => {
router.push({ name: 'Login' })
}, REDIRECT_DELAY)
}
</script>
<style scoped>
@import '../login/style.css';
</style>