Bläddra i källkod

# feat:用户登录注册实现

yang yi 12 timmar sedan
förälder
incheckning
d2563739b4

+ 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;
+}

+ 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;
+}

+ 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;
+}

+ 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;
+}

+ 1 - 1
src/layout/UserLayout.vue

@@ -36,7 +36,7 @@
           </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>

+ 459 - 0
src/view/LoginView.vue

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