|
|
@@ -0,0 +1,459 @@
|
|
|
+<template>
|
|
|
+ <div class="auth-view">
|
|
|
+ <div class="auth-container">
|
|
|
+ <div class="auth-decoration">
|
|
|
+ <div class="decoration-circle circle-1"></div>
|
|
|
+ <div class="decoration-circle circle-2"></div>
|
|
|
+ <div class="decoration-circle circle-3"></div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <el-card class="auth-card" shadow="never">
|
|
|
+ <div class="auth-header">
|
|
|
+ <h1 class="auth-title">欢迎</h1>
|
|
|
+ <p class="auth-subtitle">请登录或注册您的账号</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <el-tabs v-model="activeTab" class="auth-tabs" stretch>
|
|
|
+ <el-tab-pane label="登录" name="login">
|
|
|
+ <el-form
|
|
|
+ ref="loginFormRef"
|
|
|
+ :model="loginForm"
|
|
|
+ :rules="loginRules"
|
|
|
+ class="auth-form"
|
|
|
+ @submit.prevent="handleLogin"
|
|
|
+ >
|
|
|
+ <el-form-item prop="account">
|
|
|
+ <el-input
|
|
|
+ v-model="loginForm.account"
|
|
|
+ placeholder="请输入账号"
|
|
|
+ :prefix-icon="User"
|
|
|
+ size="large"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item prop="password">
|
|
|
+ <el-input
|
|
|
+ v-model="loginForm.password"
|
|
|
+ type="password"
|
|
|
+ placeholder="请输入密码"
|
|
|
+ :prefix-icon="Lock"
|
|
|
+ show-password
|
|
|
+ size="large"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ :loading="loginLoading"
|
|
|
+ size="large"
|
|
|
+ class="auth-submit"
|
|
|
+ @click="handleLogin"
|
|
|
+ >
|
|
|
+ 登录
|
|
|
+ </el-button>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ </el-tab-pane>
|
|
|
+
|
|
|
+ <el-tab-pane label="注册" name="register">
|
|
|
+ <el-form
|
|
|
+ ref="registerFormRef"
|
|
|
+ :model="registerForm"
|
|
|
+ :rules="registerRules"
|
|
|
+ class="auth-form"
|
|
|
+ @submit.prevent="handleRegister"
|
|
|
+ >
|
|
|
+ <el-form-item prop="account">
|
|
|
+ <el-input
|
|
|
+ v-model="registerForm.account"
|
|
|
+ placeholder="请输入账号"
|
|
|
+ :prefix-icon="User"
|
|
|
+ size="large"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item prop="username">
|
|
|
+ <el-input
|
|
|
+ v-model="registerForm.username"
|
|
|
+ placeholder="请输入用户名"
|
|
|
+ :prefix-icon="User"
|
|
|
+ size="large"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item prop="password">
|
|
|
+ <el-input
|
|
|
+ v-model="registerForm.password"
|
|
|
+ type="password"
|
|
|
+ placeholder="请输入密码"
|
|
|
+ :prefix-icon="Lock"
|
|
|
+ show-password
|
|
|
+ size="large"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item prop="confirmPassword">
|
|
|
+ <el-input
|
|
|
+ v-model="registerForm.confirmPassword"
|
|
|
+ type="password"
|
|
|
+ placeholder="请再次输入密码"
|
|
|
+ :prefix-icon="Lock"
|
|
|
+ show-password
|
|
|
+ size="large"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ :loading="registerLoading"
|
|
|
+ size="large"
|
|
|
+ class="auth-submit"
|
|
|
+ @click="handleRegister"
|
|
|
+ >
|
|
|
+ 注册
|
|
|
+ </el-button>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ </el-tab-pane>
|
|
|
+ </el-tabs>
|
|
|
+ </el-card>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { reactive, ref } from 'vue'
|
|
|
+import { useRouter } from 'vue-router'
|
|
|
+import { ElMessage } from 'element-plus'
|
|
|
+import { User, Lock } from '@element-plus/icons-vue'
|
|
|
+import type { FormInstance, FormRules } from 'element-plus'
|
|
|
+import { getAuthController } from '../api/auth-controller'
|
|
|
+import type { LoginDto, RegisterDto } from '../api/models'
|
|
|
+import { useLoginUserStore } from '../store'
|
|
|
+import type { User as UserType } from '../type'
|
|
|
+
|
|
|
+const router = useRouter()
|
|
|
+const loginUserStore = useLoginUserStore()
|
|
|
+
|
|
|
+const activeTab = ref('login')
|
|
|
+
|
|
|
+const loginFormRef = ref<FormInstance>()
|
|
|
+const registerFormRef = ref<FormInstance>()
|
|
|
+const loginLoading = ref(false)
|
|
|
+const registerLoading = ref(false)
|
|
|
+
|
|
|
+const loginForm = reactive({
|
|
|
+ account: '',
|
|
|
+ password: ''
|
|
|
+})
|
|
|
+
|
|
|
+const registerForm = reactive({
|
|
|
+ account: '',
|
|
|
+ username: '',
|
|
|
+ password: '',
|
|
|
+ confirmPassword: ''
|
|
|
+})
|
|
|
+
|
|
|
+const validateConfirmPassword = (_rule: any, value: string, callback: any) => {
|
|
|
+ if (value !== registerForm.password) {
|
|
|
+ callback(new Error('两次输入密码不一致'))
|
|
|
+ } else {
|
|
|
+ callback()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const loginRules = reactive<FormRules>({
|
|
|
+ account: [{ required: true, message: '请输入账号', trigger: 'blur' }],
|
|
|
+ password: [
|
|
|
+ { required: true, message: '请输入密码', trigger: 'blur' },
|
|
|
+ { min: 4, message: '密码长度至少4位', trigger: 'blur' }
|
|
|
+ ]
|
|
|
+})
|
|
|
+
|
|
|
+const registerRules = reactive<FormRules>({
|
|
|
+ account: [
|
|
|
+ { required: true, message: '请输入账号', trigger: 'blur' },
|
|
|
+ { min: 4, max: 20, message: '账号长度4-20个字符', trigger: 'blur' }
|
|
|
+ ],
|
|
|
+ username: [
|
|
|
+ { required: true, message: '请输入用户名', trigger: 'blur' },
|
|
|
+ { min: 2, max: 32, message: '用户名长度2-32个字符', trigger: 'blur' }
|
|
|
+ ],
|
|
|
+ password: [
|
|
|
+ { required: true, message: '请输入密码', trigger: 'blur' },
|
|
|
+ { min: 4, max: 20, message: '密码长度4-20个字符', trigger: 'blur' }
|
|
|
+ ],
|
|
|
+ confirmPassword: [
|
|
|
+ { required: true, message: '请再次输入密码', trigger: 'blur' },
|
|
|
+ { validator: validateConfirmPassword, trigger: 'blur' }
|
|
|
+ ]
|
|
|
+})
|
|
|
+
|
|
|
+const handleLogin = async () => {
|
|
|
+ const valid = await loginFormRef.value?.validate().catch(() => false)
|
|
|
+ if (!valid) return
|
|
|
+
|
|
|
+ loginLoading.value = true
|
|
|
+ try {
|
|
|
+ const authApi = getAuthController()
|
|
|
+ const res = await authApi.login({
|
|
|
+ account: loginForm.account,
|
|
|
+ password: loginForm.password
|
|
|
+ } as LoginDto)
|
|
|
+
|
|
|
+ const tokenData = res.data
|
|
|
+ if (res.code === 200 && tokenData?.token) {
|
|
|
+ localStorage.setItem('token', tokenData.token)
|
|
|
+ loginUserStore.loginSuccess({
|
|
|
+ id: String(tokenData.userId || ''),
|
|
|
+ name: tokenData.username || tokenData.account || '用户',
|
|
|
+ role: (tokenData.role as UserType['role']) || 'user'
|
|
|
+ })
|
|
|
+ ElMessage.success('登录成功')
|
|
|
+ router.push('/')
|
|
|
+ } else {
|
|
|
+ ElMessage.error(res.message || '登录失败')
|
|
|
+ }
|
|
|
+ } catch (err: any) {
|
|
|
+ ElMessage.error(err.message || '登录失败,请检查网络')
|
|
|
+ } finally {
|
|
|
+ loginLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleRegister = async () => {
|
|
|
+ const valid = await registerFormRef.value?.validate().catch(() => false)
|
|
|
+ if (!valid) return
|
|
|
+
|
|
|
+ registerLoading.value = true
|
|
|
+ try {
|
|
|
+ const authApi = getAuthController()
|
|
|
+ const res = await authApi.register({
|
|
|
+ account: registerForm.account,
|
|
|
+ password: registerForm.password,
|
|
|
+ username: registerForm.username
|
|
|
+ } as RegisterDto)
|
|
|
+
|
|
|
+ if (res.code === 200) {
|
|
|
+ ElMessage.success('注册成功,请登录')
|
|
|
+ activeTab.value = 'login'
|
|
|
+ registerForm.account = ''
|
|
|
+ registerForm.username = ''
|
|
|
+ registerForm.password = ''
|
|
|
+ registerForm.confirmPassword = ''
|
|
|
+ } else {
|
|
|
+ ElMessage.error(res.message || '注册失败')
|
|
|
+ }
|
|
|
+ } catch (err: any) {
|
|
|
+ ElMessage.error(err.message || '注册失败,请检查网络')
|
|
|
+ } finally {
|
|
|
+ registerLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.auth-view {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ min-height: 100vh;
|
|
|
+ background: linear-gradient(135deg, #fdf2f8 0%, #fce7f3 50%, #fbcfe8 100%);
|
|
|
+ position: relative;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.auth-decoration {
|
|
|
+ position: absolute;
|
|
|
+ inset: 0;
|
|
|
+ pointer-events: none;
|
|
|
+}
|
|
|
+
|
|
|
+.decoration-circle {
|
|
|
+ position: absolute;
|
|
|
+ border-radius: 50%;
|
|
|
+ opacity: 0.15;
|
|
|
+}
|
|
|
+
|
|
|
+.circle-1 {
|
|
|
+ width: 400px;
|
|
|
+ height: 400px;
|
|
|
+ background: #db2777;
|
|
|
+ top: -100px;
|
|
|
+ right: -100px;
|
|
|
+ animation: float 20s ease-in-out infinite;
|
|
|
+}
|
|
|
+
|
|
|
+.circle-2 {
|
|
|
+ width: 300px;
|
|
|
+ height: 300px;
|
|
|
+ background: #f472b6;
|
|
|
+ bottom: -80px;
|
|
|
+ left: -80px;
|
|
|
+ animation: float 15s ease-in-out infinite reverse;
|
|
|
+}
|
|
|
+
|
|
|
+.circle-3 {
|
|
|
+ width: 200px;
|
|
|
+ height: 200px;
|
|
|
+ background: #ca8a04;
|
|
|
+ top: 50%;
|
|
|
+ left: 60%;
|
|
|
+ animation: float 18s ease-in-out infinite;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes float {
|
|
|
+ 0%,
|
|
|
+ 100% {
|
|
|
+ transform: translate(0, 0) scale(1);
|
|
|
+ }
|
|
|
+ 25% {
|
|
|
+ transform: translate(20px, -30px) scale(1.05);
|
|
|
+ }
|
|
|
+ 50% {
|
|
|
+ transform: translate(-10px, 20px) scale(0.95);
|
|
|
+ }
|
|
|
+ 75% {
|
|
|
+ transform: translate(15px, 10px) scale(1.02);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.auth-container {
|
|
|
+ position: relative;
|
|
|
+ z-index: 1;
|
|
|
+ width: 100%;
|
|
|
+ max-width: 420px;
|
|
|
+ padding: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.auth-card {
|
|
|
+ border-radius: 16px;
|
|
|
+ border: 1px solid rgba(219, 39, 119, 0.1);
|
|
|
+ background: rgba(255, 255, 255, 0.9);
|
|
|
+ backdrop-filter: blur(10px);
|
|
|
+ padding: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.auth-header {
|
|
|
+ text-align: center;
|
|
|
+ padding: 24px 0 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.auth-title {
|
|
|
+ margin: 0 0 8px;
|
|
|
+ font-size: 28px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #831843;
|
|
|
+ letter-spacing: 2px;
|
|
|
+}
|
|
|
+
|
|
|
+.auth-subtitle {
|
|
|
+ margin: 0;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #be185d;
|
|
|
+}
|
|
|
+
|
|
|
+.auth-tabs {
|
|
|
+ padding: 0 8px 8px;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.auth-tabs .el-tabs__header) {
|
|
|
+ margin-bottom: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.auth-tabs .el-tabs__item) {
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #831843;
|
|
|
+ transition: color 0.2s;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.auth-tabs .el-tabs__item.is-active) {
|
|
|
+ color: #db2777;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.auth-tabs .el-tabs__active-bar) {
|
|
|
+ background-color: #db2777;
|
|
|
+ height: 3px;
|
|
|
+ border-radius: 3px;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.auth-tabs .el-tabs__nav-wrap::after) {
|
|
|
+ background-color: rgba(219, 39, 119, 0.15);
|
|
|
+ height: 1px;
|
|
|
+}
|
|
|
+
|
|
|
+.auth-form {
|
|
|
+ padding: 0 8px;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-input__wrapper) {
|
|
|
+ border-radius: 10px;
|
|
|
+ padding: 8px 12px;
|
|
|
+ box-shadow: 0 0 0 1px rgba(219, 39, 119, 0.15) inset;
|
|
|
+ transition: box-shadow 0.2s;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-input__wrapper:hover) {
|
|
|
+ box-shadow: 0 0 0 1px rgba(219, 39, 119, 0.3) inset;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-input__wrapper.is-focus) {
|
|
|
+ box-shadow: 0 0 0 1px #db2777 inset;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-input__prefix) {
|
|
|
+ color: #db2777;
|
|
|
+ margin-right: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.auth-submit {
|
|
|
+ width: 100%;
|
|
|
+ border-radius: 10px;
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 500;
|
|
|
+ letter-spacing: 4px;
|
|
|
+ background: linear-gradient(135deg, #db2777 0%, #be185d 100%);
|
|
|
+ border: none;
|
|
|
+ transition: all 0.2s;
|
|
|
+}
|
|
|
+
|
|
|
+.auth-submit:hover {
|
|
|
+ background: linear-gradient(135deg, #be185d 0%, #9d174d 100%);
|
|
|
+ transform: translateY(-1px);
|
|
|
+ box-shadow: 0 4px 12px rgba(219, 39, 119, 0.3);
|
|
|
+}
|
|
|
+
|
|
|
+.auth-submit:active {
|
|
|
+ transform: translateY(0);
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-form-item) {
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-form-item__error) {
|
|
|
+ padding-left: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+@media (max-width: 480px) {
|
|
|
+ .auth-container {
|
|
|
+ padding: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .auth-title {
|
|
|
+ font-size: 24px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .circle-1 {
|
|
|
+ width: 250px;
|
|
|
+ height: 250px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .circle-2 {
|
|
|
+ width: 200px;
|
|
|
+ height: 200px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .circle-3 {
|
|
|
+ width: 150px;
|
|
|
+ height: 150px;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|