5 Commits ba7bec7a3f ... d21ab02a32

Author SHA1 Message Date
  yang yi d21ab02a32 # style:移除不需要的文件 13 hours ago
  yang yi 35d8d57824 # feat:菜单权限校验; 13 hours ago
  yang yi 2d3820cfa6 # feat:登录注册提供返回主页的入口;登录按钮跳转登录页面 13 hours ago
  yang yi 157eca7cd9 # api:api更新 14 hours ago
  yang yi d2563739b4 # feat:用户登录注册实现 14 hours ago

+ 41 - 0
src/api/auth-controller.ts

@@ -0,0 +1,41 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * Serve API
+ * Serve应用接口文档
+ * OpenAPI spec version: 0.0.1-SNAPSHOT
+ */
+import type { LoginDto, RegisterDto, ResponseAuthTokenVo } from "./models";
+import { customAxiosInstance } from "../util/axios-instance";
+
+export const getAuthController = () => {
+  /**
+   * @summary 用户注册
+   */
+  const register = (registerDto: RegisterDto) => {
+    return customAxiosInstance<ResponseAuthTokenVo>({
+      url: `/auth/register`,
+      method: "POST",
+      headers: { "Content-Type": "application/json" },
+      data: registerDto,
+    });
+  };
+  /**
+   * @summary 用户登录
+   */
+  const login = (loginDto: LoginDto) => {
+    return customAxiosInstance<ResponseAuthTokenVo>({
+      url: `/auth/login`,
+      method: "POST",
+      headers: { "Content-Type": "application/json" },
+      data: loginDto,
+    });
+  };
+  return { register, login };
+};
+export type RegisterResult = NonNullable<
+  Awaited<ReturnType<ReturnType<typeof getAuthController>["register"]>>
+>;
+export type LoginResult = NonNullable<
+  Awaited<ReturnType<ReturnType<typeof getAuthController>["login"]>>
+>;

+ 23 - 0
src/api/models/authTokenVo.ts

@@ -0,0 +1,23 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * Serve API
+ * Serve应用接口文档
+ * OpenAPI spec version: 0.0.1-SNAPSHOT
+ */
+
+/**
+ * 登录响应
+ */
+export interface AuthTokenVo {
+  /** 账号 */
+  account?: string;
+  /** 角色 */
+  role?: string;
+  /** JWT token */
+  token?: string;
+  /** 用户ID */
+  userId?: number;
+  /** 用户名 */
+  username?: string;
+}

+ 3 - 2
src/api/models/deleteByIdParams.ts

@@ -1,8 +1,9 @@
 /**
  * Generated by orval v7.0.1 🍺
  * Do not edit manually.
- * OpenAPI definition
- * OpenAPI spec version: v0
+ * Serve API
+ * Serve应用接口文档
+ * OpenAPI spec version: 0.0.1-SNAPSHOT
  */
 
 export type DeleteByIdParams = {

+ 3 - 2
src/api/models/fieldError.ts

@@ -1,8 +1,9 @@
 /**
  * Generated by orval v7.0.1 🍺
  * Do not edit manually.
- * OpenAPI definition
- * OpenAPI spec version: v0
+ * Serve API
+ * Serve应用接口文档
+ * OpenAPI spec version: 0.0.1-SNAPSHOT
  */
 
 export interface FieldError {

+ 7 - 2
src/api/models/index.ts

@@ -1,15 +1,20 @@
 /**
  * Generated by orval v7.0.1 🍺
  * Do not edit manually.
- * OpenAPI definition
- * OpenAPI spec version: v0
+ * Serve API
+ * Serve应用接口文档
+ * OpenAPI spec version: 0.0.1-SNAPSHOT
  */
 
+export * from "./authTokenVo";
 export * from "./deleteByIdParams";
 export * from "./fieldError";
+export * from "./loginDto";
 export * from "./pageVoListUserVo";
 export * from "./queryByPageParams";
+export * from "./registerDto";
 export * from "./response";
+export * from "./responseAuthTokenVo";
 export * from "./responseBoolean";
 export * from "./responseListFieldError";
 export * from "./responsePageVoListUserVo";

+ 23 - 0
src/api/models/loginDto.ts

@@ -0,0 +1,23 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * Serve API
+ * Serve应用接口文档
+ * OpenAPI spec version: 0.0.1-SNAPSHOT
+ */
+
+/**
+ * 登录请求参数
+ */
+export interface LoginDto {
+  /**
+   * 账号
+   * @minLength 1
+   */
+  account: string;
+  /**
+   * 密码
+   * @minLength 1
+   */
+  password: string;
+}

+ 3 - 2
src/api/models/pageVoListUserVo.ts

@@ -1,8 +1,9 @@
 /**
  * Generated by orval v7.0.1 🍺
  * Do not edit manually.
- * OpenAPI definition
- * OpenAPI spec version: v0
+ * Serve API
+ * Serve应用接口文档
+ * OpenAPI spec version: 0.0.1-SNAPSHOT
  */
 import type { UserVo } from "./userVo";
 

+ 3 - 2
src/api/models/queryByPageParams.ts

@@ -1,8 +1,9 @@
 /**
  * Generated by orval v7.0.1 🍺
  * Do not edit manually.
- * OpenAPI definition
- * OpenAPI spec version: v0
+ * Serve API
+ * Serve应用接口文档
+ * OpenAPI spec version: 0.0.1-SNAPSHOT
  */
 
 export type QueryByPageParams = {

+ 31 - 0
src/api/models/registerDto.ts

@@ -0,0 +1,31 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * Serve API
+ * Serve应用接口文档
+ * OpenAPI spec version: 0.0.1-SNAPSHOT
+ */
+
+/**
+ * 注册请求参数
+ */
+export interface RegisterDto {
+  /**
+   * 账号
+   * @minLength 4
+   * @maxLength 20
+   */
+  account: string;
+  /**
+   * 密码
+   * @minLength 4
+   * @maxLength 20
+   */
+  password: string;
+  /**
+   * 用户名
+   * @minLength 2
+   * @maxLength 32
+   */
+  username: string;
+}

+ 3 - 2
src/api/models/response.ts

@@ -1,8 +1,9 @@
 /**
  * Generated by orval v7.0.1 🍺
  * Do not edit manually.
- * OpenAPI definition
- * OpenAPI spec version: v0
+ * Serve API
+ * Serve应用接口文档
+ * OpenAPI spec version: 0.0.1-SNAPSHOT
  */
 
 /**

+ 20 - 0
src/api/models/responseAuthTokenVo.ts

@@ -0,0 +1,20 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * Serve API
+ * Serve应用接口文档
+ * OpenAPI spec version: 0.0.1-SNAPSHOT
+ */
+import type { AuthTokenVo } from "./authTokenVo";
+
+/**
+ * 后端统一的响应实体
+ */
+export interface ResponseAuthTokenVo {
+  /** 状态码;200:成功 */
+  code?: number;
+  /** 响应的具体数据 */
+  data?: AuthTokenVo;
+  /** 响应附加信息 */
+  message?: string;
+}

+ 3 - 2
src/api/models/responseBoolean.ts

@@ -1,8 +1,9 @@
 /**
  * Generated by orval v7.0.1 🍺
  * Do not edit manually.
- * OpenAPI definition
- * OpenAPI spec version: v0
+ * Serve API
+ * Serve应用接口文档
+ * OpenAPI spec version: 0.0.1-SNAPSHOT
  */
 
 /**

+ 3 - 2
src/api/models/responseListFieldError.ts

@@ -1,8 +1,9 @@
 /**
  * Generated by orval v7.0.1 🍺
  * Do not edit manually.
- * OpenAPI definition
- * OpenAPI spec version: v0
+ * Serve API
+ * Serve应用接口文档
+ * OpenAPI spec version: 0.0.1-SNAPSHOT
  */
 import type { FieldError } from "./fieldError";
 

+ 3 - 2
src/api/models/responsePageVoListUserVo.ts

@@ -1,8 +1,9 @@
 /**
  * Generated by orval v7.0.1 🍺
  * Do not edit manually.
- * OpenAPI definition
- * OpenAPI spec version: v0
+ * Serve API
+ * Serve应用接口文档
+ * OpenAPI spec version: 0.0.1-SNAPSHOT
  */
 import type { PageVoListUserVo } from "./pageVoListUserVo";
 

+ 3 - 2
src/api/models/responseUserVo.ts

@@ -1,8 +1,9 @@
 /**
  * Generated by orval v7.0.1 🍺
  * Do not edit manually.
- * OpenAPI definition
- * OpenAPI spec version: v0
+ * Serve API
+ * Serve应用接口文档
+ * OpenAPI spec version: 0.0.1-SNAPSHOT
  */
 import type { UserVo } from "./userVo";
 

+ 3 - 2
src/api/models/updateUserAvatarDto.ts

@@ -1,8 +1,9 @@
 /**
  * Generated by orval v7.0.1 🍺
  * Do not edit manually.
- * OpenAPI definition
- * OpenAPI spec version: v0
+ * Serve API
+ * Serve应用接口文档
+ * OpenAPI spec version: 0.0.1-SNAPSHOT
  */
 
 /**

+ 3 - 2
src/api/models/updateUserPasswordDto.ts

@@ -1,8 +1,9 @@
 /**
  * Generated by orval v7.0.1 🍺
  * Do not edit manually.
- * OpenAPI definition
- * OpenAPI spec version: v0
+ * Serve API
+ * Serve应用接口文档
+ * OpenAPI spec version: 0.0.1-SNAPSHOT
  */
 
 /**

+ 4 - 2
src/api/models/updateUserStatusDto.ts

@@ -1,8 +1,9 @@
 /**
  * Generated by orval v7.0.1 🍺
  * Do not edit manually.
- * OpenAPI definition
- * OpenAPI spec version: v0
+ * Serve API
+ * Serve应用接口文档
+ * OpenAPI spec version: 0.0.1-SNAPSHOT
  */
 
 /**
@@ -11,6 +12,7 @@
 export interface UpdateUserStatusDto {
   /**
    * 更新后的状态;1:启用,0:禁用
+   * @minLength 1
    * @minimum 0
    * @maximum 1
    */

+ 3 - 2
src/api/models/userDto.ts

@@ -1,8 +1,9 @@
 /**
  * Generated by orval v7.0.1 🍺
  * Do not edit manually.
- * OpenAPI definition
- * OpenAPI spec version: v0
+ * Serve API
+ * Serve应用接口文档
+ * OpenAPI spec version: 0.0.1-SNAPSHOT
  */
 
 /**

+ 3 - 2
src/api/models/userVo.ts

@@ -1,8 +1,9 @@
 /**
  * Generated by orval v7.0.1 🍺
  * Do not edit manually.
- * OpenAPI definition
- * OpenAPI spec version: v0
+ * Serve API
+ * Serve应用接口文档
+ * OpenAPI spec version: 0.0.1-SNAPSHOT
  */
 
 /**

+ 3 - 2
src/api/user-controller.ts

@@ -1,8 +1,9 @@
 /**
  * Generated by orval v7.0.1 🍺
  * Do not edit manually.
- * OpenAPI definition
- * OpenAPI spec version: v0
+ * Serve API
+ * Serve应用接口文档
+ * OpenAPI spec version: 0.0.1-SNAPSHOT
  */
 import type {
   DeleteByIdParams,

+ 0 - 41
src/components/HelloWorld.vue

@@ -1,41 +0,0 @@
-<script setup lang="ts">
-import { ref } from 'vue'
-
-defineProps<{ msg: string }>()
-
-const count = ref(0)
-</script>
-
-<template>
-  <h1>{{ msg }}</h1>
-
-  <div class="card">
-    <button type="button" @click="count++">count is {{ count }}</button>
-    <p>
-      Edit
-      <code>components/HelloWorld.vue</code> to test HMR
-    </p>
-  </div>
-
-  <p>
-    Check out
-    <a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
-      >create-vue</a
-    >, the official Vue + Vite starter
-  </p>
-  <p>
-    Learn more about IDE Support for Vue in the
-    <a
-      href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
-      target="_blank"
-      >Vue Docs Scaling up Guide</a
-    >.
-  </p>
-  <p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
-</template>
-
-<style scoped>
-.read-the-docs {
-  color: #888;
-}
-</style>

+ 29 - 11
src/layout/UserLayout.vue

@@ -17,7 +17,7 @@
             <el-icon><component :is="menu.meta?.icon"/></el-icon>
             <template #title>{{ menu.meta?.title }}</template>
           </el-menu-item>
-          <el-menu-item index="/admin">
+          <el-menu-item v-if="isAdmin" index="/admin">
             <el-icon><Tools/></el-icon>
             <template #title>后台管理</template>
           </el-menu-item>
@@ -25,18 +25,21 @@
         <div class="header-right">
           <template v-if="login">
             <el-dropdown placement="bottom">
-              <el-button> {{ loginUserStore.loginUser?.user?.name }} </el-button>
+              <el-button class="user-btn" text>
+                <el-icon class="user-avatar"><UserFilled /></el-icon>
+                <span class="username">{{ loginUserStore.loginUser?.user?.name }}</span>
+              </el-button>
               <template #dropdown>
                 <el-dropdown-menu>
                   <el-dropdown-item>个人信息</el-dropdown-item>
-                  <el-dropdown-item @click="logoutHandler">退出</el-dropdown-item>
+                  <el-dropdown-item divided @click="logoutHandler">退出登录</el-dropdown-item>
                 </el-dropdown-menu>
               </template>
             </el-dropdown>
           </template>
           <template v-else>
             <el-button type="primary" size="small" @click="loginHandler">登录</el-button>
-            <el-button size="small">注册</el-button>
+            <el-button size="small" @click="router.push('/login')">注册</el-button>
           </template>
         </div>
       </el-header>
@@ -55,18 +58,15 @@ import {computed, ref} from 'vue'
 import {useLoginUserStore, useMetaStore} from "../store";
 import router, {userRoutes} from "../router";
 import {
-  Tools
+  Tools,
+  UserFilled
 } from '@element-plus/icons-vue'
 const metaStore = useMetaStore()
 const loginUserStore = useLoginUserStore()
 const login = computed(() => loginUserStore.loginUser.isLogin)
-//todo:模拟登陆
+const isAdmin = computed(() => loginUserStore.loginUser.user?.role === 'admin')
 const loginHandler = () => {
-  loginUserStore.loginSuccess({
-    id:"",
-    name:"张三",
-    role:"user"
-  });
+  router.push('/login')
 }
 let activeIndex = ref('/')
 const menus = computed(()=>{
@@ -77,6 +77,7 @@ const handleSelect = () => {
 }
 const logoutHandler = () => {
   loginUserStore.logoutUser()
+  router.push('/')
 }
 </script>
 <style scoped>
@@ -122,4 +123,21 @@ el-main{
   font-size: 14px;
   color: #606266;
 }
+
+.user-btn {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  cursor: pointer;
+  transition: color 0.2s;
+}
+
+.user-btn:hover {
+  color: #db2777;
+}
+
+.user-avatar {
+  font-size: 20px;
+  color: #be185d;
+}
 </style>

+ 37 - 4
src/router/index.ts

@@ -2,6 +2,7 @@ import {createRouter, createWebHistory, type Router, type RouteRecordRaw} from '
 
 import HomeView from '../view/HomeView.vue'
 import AboutView from '../view/AboutView.vue'
+import LoginView from '../view/LoginView.vue'
 import UserLayout from '../layout/UserLayout.vue'
 import AdminLayout from "../layout/AdminLayout.vue";
 import UserView from "../view/UserView.vue";
@@ -10,6 +11,8 @@ import {
     Menu,
     User,
 } from '@element-plus/icons-vue'
+import {useLoginUserStore} from "../store";
+import {ElMessage} from "element-plus";
 
 export const userRoutes:RouteRecordRaw[] = [
     {
@@ -50,7 +53,8 @@ export const adminRoutes:RouteRecordRaw[] = [
                 component: UserView,
                 meta:{
                     title: '用户管理',
-                    icon: User
+                    icon: User,
+                    requiresAdmin: true
                 }
             },
             {
@@ -59,15 +63,44 @@ export const adminRoutes:RouteRecordRaw[] = [
                 component: AdminLayout,
                 meta:{
                     title: '其他',
-                    icon: Menu
+                    icon: Menu,
+                    requiresAdmin: true
                 }
             }
-        ]
+        ],
+        meta: {
+            requiresAdmin: true
+        }
     }
 ]
 
 const router : Router = createRouter({
     history: createWebHistory(),
-    routes: [...userRoutes,...adminRoutes],
+    routes: [
+        {
+            path: '/login',
+            name: 'login',
+            component: LoginView
+        },
+        ...userRoutes,
+        ...adminRoutes
+    ],
 });
+router.beforeEach((to, _from, next) => {
+    if (to.matched.some(record => record.meta.requiresAdmin)) {
+        const loginUserStore = useLoginUserStore()
+        if (!loginUserStore.loginUser.isLogin) {
+            ElMessage.warning('请先登录')
+            next({path: '/login', query: {redirect: to.fullPath}})
+        } else if (loginUserStore.loginUser.user?.role !== 'admin') {
+            ElMessage.error('无权访问')
+            next('/')
+        } else {
+            next()
+        }
+    } else {
+        next()
+    }
+})
+
 export default router

+ 7 - 1
src/store/user.ts

@@ -25,7 +25,13 @@ export const useLoginUserStore = defineStore('loginUser', ()=>{
         loginUser.isLogin = true;
     }
     function logoutUser():void {
-        loginUser.isLogin = false;
+        loginUser.isLogin = false
+        loginUser.user = {
+            id: '',
+            name: '未登录',
+            role: 'guest'
+        }
+        localStorage.removeItem('token')
     }
    return{
         //States

+ 483 - 0
src/view/LoginView.vue

@@ -0,0 +1,483 @@
+<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">
+        <el-button class="back-btn" text @click="router.push('/')">
+          <el-icon><ArrowLeft /></el-icon>
+          返回主页
+        </el-button>
+
+        <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 { useRoute, useRouter } from 'vue-router'
+import { ElMessage } from 'element-plus'
+import { User, Lock, ArrowLeft } 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 route = useRoute()
+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((route.query.redirect as string) || '/')
+    } 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;
+}
+
+.back-btn {
+  position: absolute;
+  top: 16px;
+  left: 16px;
+  color: #be185d;
+  font-size: 14px;
+  cursor: pointer;
+  transition: color 0.2s;
+}
+
+.back-btn:hover {
+  color: #db2777;
+}
+
+.back-btn .el-icon {
+  margin-right: 4px;
+}
+
+.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>