第八章:用户认证模块🎯本章目标:开发登录、注册页面,实现 Token 认证和表单验证。8.1 登录页面开发8.1.1 创建登录页面创建src/views/login/index.vue:template div !-- 左侧装饰区域 -- div div h1智能知识管理平台/h1 p 收集知识 → 组织知识 → 智能检索 → AI 辅助 /p div div el-icon :size="24"Folder //el-icon span多知识库管理/span /div div el-icon :size="24"Search //el-icon span智能语义搜索/span /div div el-icon :size="24"ChatDotRound //el-icon spanAI 知识问答/span /div /div /div /div !-- 右侧登录表单 -- div div !-- Logo -- div el-icon :size="40"Collection //el-icon span知识管理/span /div !-- 标题 -- div h2欢迎回来/h2 p请登录您的账号/p /div !-- 登录表单 -- el-form ref="loginFormRef" :model="loginForm" :rules="loginRules" @submit.prevent="handleLogin" !-- 用户名 -- el-form-item prop="username" el-input v-model="loginForm.username" placeholder="请输入用户名或邮箱" prefix-icon="User" size="large" clearable / /el-form-item !-- 密码 -- el-form-item prop="password" el-input v-model="loginForm.password" type="password" placeholder="请输入密码" prefix-icon="Lock" size="large" show-password @keyup.enter="handleLogin" / /el-form-item !-- 记住我 忘记密码 -- div el-checkbox v-model="loginForm.remember"记住我/el-checkbox router-link to="/forgot-password" 忘记密码? /router-link /div !-- 登录按钮 -- el-form-item el-button type="primary" size="large" :loading="loading" @click="handleLogin" 登 录 /el-button /el-form-item /el-form !-- 注册链接 -- div span还没有账号?/span router-link to="/register" 立即注册 /router-link /div /div /div /div /template script setup lang="ts" import { ref, reactive, onMounted } from 'vue' import { useRouter, useRoute } from 'vue-router' import { ElMessage } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus' import { Folder, Search, ChatDotRound, Collection } from '@element-plus/icons-vue' import { useUserStore } from '@/stores/user' import { login as loginApi } from '@/api/user' const router = useRouter() const route = useRoute() const userStore = useUserStore() // 本地存储键名 const REMEMBER_KEY = 'knowledge_remember_user' // 表单引用 const loginFormRef = refFormInstance() // 加载状态 const loading = ref(false) // 表单数据 const loginForm = reactive({ username: '', password: '', remember: false, }) // 表单验证规则 const loginRules: FormRules = { username: [ { required: true, message: '请输入用户名或邮箱', trigger: 'blur' }, ], password: [ { required: true, message: '请输入密码', trigger: 'blur' }, { min: 6, max: 20, message: '密码长度在 6 到 20 个字符', trigger: 'blur' }, ], } // 页面加载时读取记住的用户信息 onMounted(() = { const savedUser = localStorage.getItem(REMEMBER_KEY) if (savedUser) { try { const { username, password, remember } = JSON.parse(savedUser) loginForm.username = username || '' loginForm.password = password || '' loginForm.remember = remember || false } catch { localStorage.removeItem(REMEMBER_KEY) } } }) // 登录处理 async function handleLogin() { if (!loginFormRef.value) return try { await loginFormRef.value.validate() loading.value = true const result = await loginApi({ username: loginForm.username, password: loginForm.password, remember: loginForm.remember, }) // 处理"记住我":保存或清除用户名密码 if (loginForm.remember) { localStorage.setItem(REMEMBER_KEY, JSON.stringify({ username: loginForm.username, password: loginForm.password, remember: true, })) } else { localStorage.removeItem(REMEMBER_KEY) } // 保存登录状态 userStore.login(result.accessToken, result.refreshToken, result.user) ElMessage.success('登录成功') // 跳转到之前的页面或首页 const redirect = route.query.redirect as string router.push(redirect || '/') } catch (error) { console.error('登录失败:', error) } finally { loading.value = false } } /script style scoped .login-container { @apply min-h-screen flex; } /* 左侧装饰区域 */ .login-banner { @apply hidden lg:flex lg:w-1/2 bg-gradient-to-br from-primary to-primary-dark; @apply items-center justify-center p-12; } .banner-content { @apply text-white text-center; } .banner-title { @apply text-4xl font-bold mb-4; } .banner-subtitle { @apply text-lg text-white/80 mb-8; } .banner-features { @apply flex flex-col gap-4 items-center; } .feature-item { @apply flex items-center gap-3 text-white/90; } /* 右侧登录表单 */ .login-form-container { @apply flex-1 flex items-center justify-center p-8; @apply bg-gray-50; } html.dark .login-form-container { @apply bg-slate-900; } .login-form-wrapper { @apply w-full max-w-md; } .login-logo { @apply flex items-center justify-center gap-3 mb-8; } .logo-icon { @apply text-primary; } .logo-text { @apply text-2xl font-bold text-gray-900; } html.dark .logo-text { @apply text-gray-100; } .login-header { @apply text-center mb-8; } .login-title { @apply text-2xl font-bold text-gray-900 mb-2; } html.dark .login-title { @apply text-gray-100; } .login-subtitle { @apply text-gray-500; } .login-form { @apply space-y-4; } .login-options { @apply flex justify-between items-center mb-4; } .forgot-link { @apply text-sm text-primary hover:text-primary-hover; } .login-btn { @apply w-full; } .login-footer { @apply text-center mt-6 text-gray-500; } .register-link { @apply text-primary hover:text-primary-hover ml-1; } /style
前端:第八章-用户认证模块
第八章:用户认证模块🎯本章目标:开发登录、注册页面,实现 Token 认证和表单验证。8.1 登录页面开发8.1.1 创建登录页面创建src/views/login/index.vue:template div !-- 左侧装饰区域 -- div div h1智能知识管理平台/h1 p 收集知识 → 组织知识 → 智能检索 → AI 辅助 /p div div el-icon :size="24"Folder //el-icon span多知识库管理/span /div div el-icon :size="24"Search //el-icon span智能语义搜索/span /div div el-icon :size="24"ChatDotRound //el-icon spanAI 知识问答/span /div /div /div /div !-- 右侧登录表单 -- div div !-- Logo -- div el-icon :size="40"Collection //el-icon span知识管理/span /div !-- 标题 -- div h2欢迎回来/h2 p请登录您的账号/p /div !-- 登录表单 -- el-form ref="loginFormRef" :model="loginForm" :rules="loginRules" @submit.prevent="handleLogin" !-- 用户名 -- el-form-item prop="username" el-input v-model="loginForm.username" placeholder="请输入用户名或邮箱" prefix-icon="User" size="large" clearable / /el-form-item !-- 密码 -- el-form-item prop="password" el-input v-model="loginForm.password" type="password" placeholder="请输入密码" prefix-icon="Lock" size="large" show-password @keyup.enter="handleLogin" / /el-form-item !-- 记住我 忘记密码 -- div el-checkbox v-model="loginForm.remember"记住我/el-checkbox router-link to="/forgot-password" 忘记密码? /router-link /div !-- 登录按钮 -- el-form-item el-button type="primary" size="large" :loading="loading" @click="handleLogin" 登 录 /el-button /el-form-item /el-form !-- 注册链接 -- div span还没有账号?/span router-link to="/register" 立即注册 /router-link /div /div /div /div /template script setup lang="ts" import { ref, reactive, onMounted } from 'vue' import { useRouter, useRoute } from 'vue-router' import { ElMessage } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus' import { Folder, Search, ChatDotRound, Collection } from '@element-plus/icons-vue' import { useUserStore } from '@/stores/user' import { login as loginApi } from '@/api/user' const router = useRouter() const route = useRoute() const userStore = useUserStore() // 本地存储键名 const REMEMBER_KEY = 'knowledge_remember_user' // 表单引用 const loginFormRef = refFormInstance() // 加载状态 const loading = ref(false) // 表单数据 const loginForm = reactive({ username: '', password: '', remember: false, }) // 表单验证规则 const loginRules: FormRules = { username: [ { required: true, message: '请输入用户名或邮箱', trigger: 'blur' }, ], password: [ { required: true, message: '请输入密码', trigger: 'blur' }, { min: 6, max: 20, message: '密码长度在 6 到 20 个字符', trigger: 'blur' }, ], } // 页面加载时读取记住的用户信息 onMounted(() = { const savedUser = localStorage.getItem(REMEMBER_KEY) if (savedUser) { try { const { username, password, remember } = JSON.parse(savedUser) loginForm.username = username || '' loginForm.password = password || '' loginForm.remember = remember || false } catch { localStorage.removeItem(REMEMBER_KEY) } } }) // 登录处理 async function handleLogin() { if (!loginFormRef.value) return try { await loginFormRef.value.validate() loading.value = true const result = await loginApi({ username: loginForm.username, password: loginForm.password, remember: loginForm.remember, }) // 处理"记住我":保存或清除用户名密码 if (loginForm.remember) { localStorage.setItem(REMEMBER_KEY, JSON.stringify({ username: loginForm.username, password: loginForm.password, remember: true, })) } else { localStorage.removeItem(REMEMBER_KEY) } // 保存登录状态 userStore.login(result.accessToken, result.refreshToken, result.user) ElMessage.success('登录成功') // 跳转到之前的页面或首页 const redirect = route.query.redirect as string router.push(redirect || '/') } catch (error) { console.error('登录失败:', error) } finally { loading.value = false } } /script style scoped .login-container { @apply min-h-screen flex; } /* 左侧装饰区域 */ .login-banner { @apply hidden lg:flex lg:w-1/2 bg-gradient-to-br from-primary to-primary-dark; @apply items-center justify-center p-12; } .banner-content { @apply text-white text-center; } .banner-title { @apply text-4xl font-bold mb-4; } .banner-subtitle { @apply text-lg text-white/80 mb-8; } .banner-features { @apply flex flex-col gap-4 items-center; } .feature-item { @apply flex items-center gap-3 text-white/90; } /* 右侧登录表单 */ .login-form-container { @apply flex-1 flex items-center justify-center p-8; @apply bg-gray-50; } html.dark .login-form-container { @apply bg-slate-900; } .login-form-wrapper { @apply w-full max-w-md; } .login-logo { @apply flex items-center justify-center gap-3 mb-8; } .logo-icon { @apply text-primary; } .logo-text { @apply text-2xl font-bold text-gray-900; } html.dark .logo-text { @apply text-gray-100; } .login-header { @apply text-center mb-8; } .login-title { @apply text-2xl font-bold text-gray-900 mb-2; } html.dark .login-title { @apply text-gray-100; } .login-subtitle { @apply text-gray-500; } .login-form { @apply space-y-4; } .login-options { @apply flex justify-between items-center mb-4; } .forgot-link { @apply text-sm text-primary hover:text-primary-hover; } .login-btn { @apply w-full; } .login-footer { @apply text-center mt-6 text-gray-500; } .register-link { @apply text-primary hover:text-primary-hover ml-1; } /style