Kaynağa Gözat

feat(frontend): 添加登录和注册页面

- 新增 Login.vue 登录页面
  - 用户名/邮箱 + 密码登录
  - 记住我功能
  - 社交登录占位

- 新增 Register.vue 注册页面
  - 用户名、邮箱、密码注册
  - 表单验证(密码强度等)
  - 用户协议确认

- 更新 api/index.js
  - 添加 authApi(登录、注册、登出、刷新Token等)

- 更新路由配置
  - 添加登录/注册路由
  - 路由守卫:未登录跳转登录页

- 更新 App.vue
  - 登录/注册页隐藏布局
  - 显示真实用户名
  - 退出登录功能

- 更新 Home.vue
  - 根据时间显示问候语
  - 显示真实用户名
何文松 1 ay önce
ebeveyn
işleme
ae65fa448d

+ 46 - 7
frontend/vue-demo/src/App.vue

@@ -1,6 +1,12 @@
 <template>
   <el-config-provider :locale="zhCn">
-    <div class="app-container">
+    <!-- 登录/注册页面不显示布局 -->
+    <template v-if="hideLayout">
+      <router-view />
+    </template>
+    
+    <!-- 正常布局 -->
+    <div v-else class="app-container">
       <!-- 顶部导航 -->
       <header class="app-header">
         <div class="header-left">
@@ -20,16 +26,16 @@
           <el-badge :value="3" class="notification-badge">
             <el-button :icon="Bell" circle />
           </el-badge>
-          <el-dropdown trigger="click">
+          <el-dropdown trigger="click" @command="handleUserCommand">
             <div class="user-menu">
-              <el-avatar :size="32" class="user-avatar"></el-avatar>
-              <span class="user-name">张三</span>
+              <el-avatar :size="32" class="user-avatar">{{ userInitial }}</el-avatar>
+              <span class="user-name">{{ username }}</span>
             </div>
             <template #dropdown>
               <el-dropdown-menu>
-                <el-dropdown-item>个人中心</el-dropdown-item>
-                <el-dropdown-item>系统设置</el-dropdown-item>
-                <el-dropdown-item divided>退出登录</el-dropdown-item>
+                <el-dropdown-item command="profile">个人中心</el-dropdown-item>
+                <el-dropdown-item command="settings">系统设置</el-dropdown-item>
+                <el-dropdown-item command="logout" divided>退出登录</el-dropdown-item>
               </el-dropdown-menu>
             </template>
           </el-dropdown>
@@ -78,6 +84,7 @@
 import { ref, computed } from 'vue'
 import { useRouter, useRoute } from 'vue-router'
 import { Bell, HomeFilled, Files, Document, QuestionFilled } from '@element-plus/icons-vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
 import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
 
 const router = useRouter()
@@ -87,6 +94,38 @@ const searchKeyword = ref('')
 
 const currentRoute = computed(() => route.path)
 const isEditorPage = computed(() => route.path.startsWith('/editor'))
+const hideLayout = computed(() => route.meta.hideLayout === true)
+
+// 用户信息
+const username = computed(() => localStorage.getItem('username') || '用户')
+const userInitial = computed(() => username.value.charAt(0))
+
+// 用户菜单操作
+function handleUserCommand(command) {
+  switch (command) {
+    case 'profile':
+      ElMessage.info('个人中心开发中...')
+      break
+    case 'settings':
+      ElMessage.info('系统设置开发中...')
+      break
+    case 'logout':
+      ElMessageBox.confirm('确定要退出登录吗?', '退出确认', {
+        confirmButtonText: '退出',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        // 清除登录信息
+        localStorage.removeItem('accessToken')
+        localStorage.removeItem('refreshToken')
+        localStorage.removeItem('userId')
+        localStorage.removeItem('username')
+        ElMessage.success('已退出登录')
+        router.push('/login')
+      }).catch(() => {})
+      break
+  }
+}
 </script>
 
 <style lang="scss">

+ 75 - 0
frontend/vue-demo/src/api/index.js

@@ -207,4 +207,79 @@ export const generationApi = {
   }
 }
 
+// ==================== 认证 API ====================
+// 注意:认证接口不在 /api/v1 下,使用独立的 axios 实例
+
+const authInstance = axios.create({
+  baseURL: '/auth',
+  timeout: 30000,
+  headers: {
+    'Content-Type': 'application/json'
+  }
+})
+
+// 认证 API 响应拦截器
+authInstance.interceptors.response.use(
+  response => {
+    const { data } = response
+    if (data.code === 200) {
+      return data.data
+    }
+    return Promise.reject(new Error(data.msg || '请求失败'))
+  },
+  error => {
+    console.error('Auth API Error:', error)
+    return Promise.reject(error)
+  }
+)
+
+export const authApi = {
+  // 用户登录
+  login(data) {
+    return authInstance.post('/login', data)
+  },
+
+  // 用户注册
+  register(data) {
+    return authInstance.post('/register', data)
+  },
+
+  // 用户登出
+  logout() {
+    const token = localStorage.getItem('accessToken')
+    return authInstance.post('/logout', null, {
+      headers: { Authorization: `Bearer ${token}` }
+    })
+  },
+
+  // 刷新 Token
+  refreshToken(refreshToken) {
+    return authInstance.post('/refresh', { refreshToken })
+  },
+
+  // 获取当前用户信息
+  getCurrentUser() {
+    const token = localStorage.getItem('accessToken')
+    return authInstance.get('/me', {
+      headers: { Authorization: `Bearer ${token}` }
+    })
+  },
+
+  // 更新用户资料
+  updateProfile(data) {
+    const token = localStorage.getItem('accessToken')
+    return authInstance.put('/profile', data, {
+      headers: { Authorization: `Bearer ${token}` }
+    })
+  },
+
+  // 修改密码
+  changePassword(data) {
+    const token = localStorage.getItem('accessToken')
+    return authInstance.put('/password', data, {
+      headers: { Authorization: `Bearer ${token}` }
+    })
+  }
+}
+
 export default api

+ 42 - 6
frontend/vue-demo/src/router/index.js

@@ -1,35 +1,55 @@
 import { createRouter, createWebHistory } from 'vue-router'
 
 const routes = [
+  // 认证页面(无需登录)
+  {
+    path: '/login',
+    name: 'Login',
+    component: () => import('@/views/Login.vue'),
+    meta: { requiresAuth: false, hideLayout: true }
+  },
+  {
+    path: '/register',
+    name: 'Register',
+    component: () => import('@/views/Register.vue'),
+    meta: { requiresAuth: false, hideLayout: true }
+  },
+  // 业务页面(需要登录)
   {
     path: '/',
     name: 'Home',
-    component: () => import('@/views/Home.vue')
+    component: () => import('@/views/Home.vue'),
+    meta: { requiresAuth: true }
   },
   {
     path: '/templates',
     name: 'Templates',
-    component: () => import('@/views/Templates.vue')
+    component: () => import('@/views/Templates.vue'),
+    meta: { requiresAuth: true }
   },
   {
     path: '/templates/:id',
     name: 'TemplateDetail',
-    component: () => import('@/views/TemplateDetail.vue')
+    component: () => import('@/views/TemplateDetail.vue'),
+    meta: { requiresAuth: true }
   },
   {
     path: '/editor/:templateId',
     name: 'Editor',
-    component: () => import('@/views/Editor.vue')
+    component: () => import('@/views/Editor.vue'),
+    meta: { requiresAuth: true }
   },
   {
     path: '/generations',
     name: 'Generations',
-    component: () => import('@/views/Generations.vue')
+    component: () => import('@/views/Generations.vue'),
+    meta: { requiresAuth: true }
   },
   {
     path: '/generations/:id',
     name: 'GenerationDetail',
-    component: () => import('@/views/GenerationDetail.vue')
+    component: () => import('@/views/GenerationDetail.vue'),
+    meta: { requiresAuth: true }
   }
 ]
 
@@ -38,4 +58,20 @@ const router = createRouter({
   routes
 })
 
+// 路由守卫:检查登录状态
+router.beforeEach((to, from, next) => {
+  const token = localStorage.getItem('accessToken')
+  const requiresAuth = to.meta.requiresAuth !== false
+  
+  if (requiresAuth && !token) {
+    // 需要登录但未登录,跳转到登录页
+    next({ path: '/login', query: { redirect: to.fullPath } })
+  } else if ((to.path === '/login' || to.path === '/register') && token) {
+    // 已登录用户访问登录/注册页,跳转到首页
+    next('/')
+  } else {
+    next()
+  }
+})
+
 export default router

+ 16 - 2
frontend/vue-demo/src/views/Home.vue

@@ -2,7 +2,7 @@
   <div class="home-page">
     <!-- 欢迎区 -->
     <div class="welcome-section">
-      <h1>早上好,张三!<span class="gradient-text">智能报告,洞察未来。</span></h1>
+      <h1>{{ greeting }},{{ username }}!<span class="gradient-text">智能报告,洞察未来。</span></h1>
       <p>今天是个创作的好日子,开始您的智能报告之旅吧</p>
     </div>
 
@@ -169,7 +169,7 @@
 </template>
 
 <script setup>
-import { ref, reactive, onMounted } from 'vue'
+import { ref, reactive, computed, onMounted } from 'vue'
 import { useRouter } from 'vue-router'
 import { Promotion, UploadFilled } from '@element-plus/icons-vue'
 import { ElMessage } from 'element-plus'
@@ -184,6 +184,20 @@ const thinkingMode = ref('deep')
 const showUploadDialog = ref(false)
 const showCreateDialog = ref(false)
 
+// 用户信息
+const username = computed(() => localStorage.getItem('username') || '用户')
+
+// 问候语
+const greeting = computed(() => {
+  const hour = new Date().getHours()
+  if (hour < 6) return '夜深了'
+  if (hour < 9) return '早上好'
+  if (hour < 12) return '上午好'
+  if (hour < 14) return '中午好'
+  if (hour < 18) return '下午好'
+  return '晚上好'
+})
+
 const stats = reactive({
   reportCount: 0,
   templateCount: 0,

+ 240 - 0
frontend/vue-demo/src/views/Login.vue

@@ -0,0 +1,240 @@
+<template>
+  <div class="login-page">
+    <div class="login-container">
+      <!-- Logo 区域 -->
+      <div class="login-header">
+        <div class="logo">🚀 灵越智报</div>
+        <p class="subtitle">智能报告生成平台</p>
+      </div>
+
+      <!-- 登录表单 -->
+      <el-card class="login-card">
+        <template #header>
+          <div class="card-header">
+            <span>用户登录</span>
+            <router-link to="/register" class="switch-link">没有账号?立即注册</router-link>
+          </div>
+        </template>
+
+        <el-form
+          ref="formRef"
+          :model="form"
+          :rules="rules"
+          label-width="0"
+          size="large"
+          @submit.prevent="handleLogin"
+        >
+          <el-form-item prop="usernameOrEmail">
+            <el-input
+              v-model="form.usernameOrEmail"
+              placeholder="用户名或邮箱"
+              :prefix-icon="User"
+              clearable
+            />
+          </el-form-item>
+
+          <el-form-item prop="password">
+            <el-input
+              v-model="form.password"
+              type="password"
+              placeholder="密码"
+              :prefix-icon="Lock"
+              show-password
+              @keyup.enter="handleLogin"
+            />
+          </el-form-item>
+
+          <el-form-item>
+            <div class="login-options">
+              <el-checkbox v-model="rememberMe">记住我</el-checkbox>
+              <a href="#" class="forgot-password">忘记密码?</a>
+            </div>
+          </el-form-item>
+
+          <el-form-item>
+            <el-button
+              type="primary"
+              class="login-btn"
+              :loading="loading"
+              @click="handleLogin"
+            >
+              {{ loading ? '登录中...' : '登 录' }}
+            </el-button>
+          </el-form-item>
+        </el-form>
+
+        <!-- 其他登录方式 -->
+        <div class="other-login">
+          <el-divider>其他登录方式</el-divider>
+          <div class="social-login">
+            <el-button :icon="ChatDotRound" circle title="微信登录" />
+            <el-button :icon="Message" circle title="企业微信" />
+            <el-button :icon="Phone" circle title="手机验证码" />
+          </div>
+        </div>
+      </el-card>
+
+      <!-- 底部信息 -->
+      <div class="login-footer">
+        <p>© 2026 灵越智报 - 企业级智能报告生成平台</p>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive } from 'vue'
+import { useRouter } from 'vue-router'
+import { User, Lock, ChatDotRound, Message, Phone } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+import { authApi } from '@/api'
+
+const router = useRouter()
+const formRef = ref(null)
+const loading = ref(false)
+const rememberMe = ref(false)
+
+const form = reactive({
+  usernameOrEmail: '',
+  password: ''
+})
+
+const rules = {
+  usernameOrEmail: [
+    { required: true, message: '请输入用户名或邮箱', trigger: 'blur' }
+  ],
+  password: [
+    { required: true, message: '请输入密码', trigger: 'blur' }
+  ]
+}
+
+async function handleLogin() {
+  if (!formRef.value) return
+  
+  await formRef.value.validate(async (valid) => {
+    if (!valid) return
+    
+    loading.value = true
+    try {
+      const response = await authApi.login(form)
+      
+      // 保存 Token
+      localStorage.setItem('accessToken', response.accessToken)
+      localStorage.setItem('refreshToken', response.refreshToken)
+      localStorage.setItem('userId', response.userId)
+      localStorage.setItem('username', response.username)
+      
+      if (rememberMe.value) {
+        localStorage.setItem('rememberMe', 'true')
+      }
+      
+      ElMessage.success('登录成功')
+      router.push('/')
+    } catch (error) {
+      console.error('登录失败:', error)
+      ElMessage.error(error.message || '登录失败,请检查用户名和密码')
+    } finally {
+      loading.value = false
+    }
+  })
+}
+</script>
+
+<style lang="scss" scoped>
+.login-page {
+  min-height: 100vh;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  padding: 20px;
+}
+
+.login-container {
+  width: 100%;
+  max-width: 420px;
+}
+
+.login-header {
+  text-align: center;
+  margin-bottom: 30px;
+  color: #fff;
+
+  .logo {
+    font-size: 32px;
+    font-weight: 700;
+    margin-bottom: 8px;
+  }
+
+  .subtitle {
+    font-size: 14px;
+    opacity: 0.9;
+  }
+}
+
+.login-card {
+  border-radius: 12px;
+  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
+
+  .card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+
+    span {
+      font-size: 18px;
+      font-weight: 600;
+    }
+
+    .switch-link {
+      font-size: 13px;
+      color: var(--el-color-primary);
+      text-decoration: none;
+
+      &:hover {
+        text-decoration: underline;
+      }
+    }
+  }
+}
+
+.login-options {
+  width: 100%;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+
+  .forgot-password {
+    font-size: 13px;
+    color: var(--el-color-primary);
+    text-decoration: none;
+
+    &:hover {
+      text-decoration: underline;
+    }
+  }
+}
+
+.login-btn {
+  width: 100%;
+  height: 44px;
+  font-size: 16px;
+}
+
+.other-login {
+  margin-top: 10px;
+
+  .social-login {
+    display: flex;
+    justify-content: center;
+    gap: 16px;
+  }
+}
+
+.login-footer {
+  text-align: center;
+  margin-top: 24px;
+  color: rgba(255, 255, 255, 0.7);
+  font-size: 12px;
+}
+</style>

+ 269 - 0
frontend/vue-demo/src/views/Register.vue

@@ -0,0 +1,269 @@
+<template>
+  <div class="register-page">
+    <div class="register-container">
+      <!-- Logo 区域 -->
+      <div class="register-header">
+        <div class="logo">🚀 灵越智报</div>
+        <p class="subtitle">智能报告生成平台</p>
+      </div>
+
+      <!-- 注册表单 -->
+      <el-card class="register-card">
+        <template #header>
+          <div class="card-header">
+            <span>用户注册</span>
+            <router-link to="/login" class="switch-link">已有账号?立即登录</router-link>
+          </div>
+        </template>
+
+        <el-form
+          ref="formRef"
+          :model="form"
+          :rules="rules"
+          label-width="0"
+          size="large"
+          @submit.prevent="handleRegister"
+        >
+          <el-form-item prop="username">
+            <el-input
+              v-model="form.username"
+              placeholder="用户名(3-20个字符,字母、数字、下划线)"
+              :prefix-icon="User"
+              clearable
+            />
+          </el-form-item>
+
+          <el-form-item prop="email">
+            <el-input
+              v-model="form.email"
+              placeholder="邮箱地址"
+              :prefix-icon="Message"
+              clearable
+            />
+          </el-form-item>
+
+          <el-form-item prop="password">
+            <el-input
+              v-model="form.password"
+              type="password"
+              placeholder="密码(8-32位,包含字母和数字)"
+              :prefix-icon="Lock"
+              show-password
+            />
+          </el-form-item>
+
+          <el-form-item prop="confirmPassword">
+            <el-input
+              v-model="form.confirmPassword"
+              type="password"
+              placeholder="确认密码"
+              :prefix-icon="Lock"
+              show-password
+              @keyup.enter="handleRegister"
+            />
+          </el-form-item>
+
+          <el-form-item prop="agreement">
+            <el-checkbox v-model="form.agreement">
+              我已阅读并同意
+              <a href="#" class="link">《用户协议》</a>
+              和
+              <a href="#" class="link">《隐私政策》</a>
+            </el-checkbox>
+          </el-form-item>
+
+          <el-form-item>
+            <el-button
+              type="primary"
+              class="register-btn"
+              :loading="loading"
+              @click="handleRegister"
+            >
+              {{ loading ? '注册中...' : '立即注册' }}
+            </el-button>
+          </el-form-item>
+        </el-form>
+      </el-card>
+
+      <!-- 底部信息 -->
+      <div class="register-footer">
+        <p>© 2026 灵越智报 - 企业级智能报告生成平台</p>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive } from 'vue'
+import { useRouter } from 'vue-router'
+import { User, Lock, Message } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+import { authApi } from '@/api'
+
+const router = useRouter()
+const formRef = ref(null)
+const loading = ref(false)
+
+const form = reactive({
+  username: '',
+  email: '',
+  password: '',
+  confirmPassword: '',
+  agreement: false
+})
+
+// 确认密码验证
+const validateConfirmPassword = (rule, value, callback) => {
+  if (value !== form.password) {
+    callback(new Error('两次输入的密码不一致'))
+  } else {
+    callback()
+  }
+}
+
+// 用户协议验证
+const validateAgreement = (rule, value, callback) => {
+  if (!value) {
+    callback(new Error('请阅读并同意用户协议'))
+  } else {
+    callback()
+  }
+}
+
+const rules = {
+  username: [
+    { required: true, message: '请输入用户名', trigger: 'blur' },
+    { min: 3, max: 20, message: '用户名长度必须在3-20个字符之间', trigger: 'blur' },
+    { pattern: /^[a-zA-Z0-9_]+$/, message: '用户名只能包含字母、数字和下划线', trigger: 'blur' }
+  ],
+  email: [
+    { required: true, message: '请输入邮箱地址', trigger: 'blur' },
+    { type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
+  ],
+  password: [
+    { required: true, message: '请输入密码', trigger: 'blur' },
+    { min: 8, max: 32, message: '密码长度必须在8-32个字符之间', trigger: 'blur' },
+    { pattern: /^(?=.*[a-zA-Z])(?=.*\d).+$/, message: '密码必须包含字母和数字', trigger: 'blur' }
+  ],
+  confirmPassword: [
+    { required: true, message: '请再次输入密码', trigger: 'blur' },
+    { validator: validateConfirmPassword, trigger: 'blur' }
+  ],
+  agreement: [
+    { validator: validateAgreement, trigger: 'change' }
+  ]
+}
+
+async function handleRegister() {
+  if (!formRef.value) return
+  
+  await formRef.value.validate(async (valid) => {
+    if (!valid) return
+    
+    loading.value = true
+    try {
+      const response = await authApi.register({
+        username: form.username,
+        email: form.email,
+        password: form.password,
+        confirmPassword: form.confirmPassword
+      })
+      
+      // 注册成功后自动登录
+      localStorage.setItem('accessToken', response.accessToken)
+      localStorage.setItem('refreshToken', response.refreshToken)
+      localStorage.setItem('userId', response.userId)
+      localStorage.setItem('username', response.username)
+      
+      ElMessage.success('注册成功')
+      router.push('/')
+    } catch (error) {
+      console.error('注册失败:', error)
+      ElMessage.error(error.message || '注册失败,请稍后重试')
+    } finally {
+      loading.value = false
+    }
+  })
+}
+</script>
+
+<style lang="scss" scoped>
+.register-page {
+  min-height: 100vh;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  padding: 20px;
+}
+
+.register-container {
+  width: 100%;
+  max-width: 420px;
+}
+
+.register-header {
+  text-align: center;
+  margin-bottom: 30px;
+  color: #fff;
+
+  .logo {
+    font-size: 32px;
+    font-weight: 700;
+    margin-bottom: 8px;
+  }
+
+  .subtitle {
+    font-size: 14px;
+    opacity: 0.9;
+  }
+}
+
+.register-card {
+  border-radius: 12px;
+  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
+
+  .card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+
+    span {
+      font-size: 18px;
+      font-weight: 600;
+    }
+
+    .switch-link {
+      font-size: 13px;
+      color: var(--el-color-primary);
+      text-decoration: none;
+
+      &:hover {
+        text-decoration: underline;
+      }
+    }
+  }
+}
+
+.link {
+  color: var(--el-color-primary);
+  text-decoration: none;
+
+  &:hover {
+    text-decoration: underline;
+  }
+}
+
+.register-btn {
+  width: 100%;
+  height: 44px;
+  font-size: 16px;
+}
+
+.register-footer {
+  text-align: center;
+  margin-top: 24px;
+  color: rgba(255, 255, 255, 0.7);
+  font-size: 12px;
+}
+</style>

+ 5 - 0
frontend/vue-demo/vite.config.js

@@ -19,6 +19,11 @@ export default defineConfig({
         target: API_SERVER,
         changeOrigin: true,
         secure: false
+      },
+      '/auth': {
+        target: API_SERVER,
+        changeOrigin: true,
+        secure: false
       }
     }
   },