From c447e3ad0b9514b3df54ab89288af7bd2de4139f Mon Sep 17 00:00:00 2001 From: tbphp Date: Tue, 1 Jul 2025 12:52:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=89=8D=E7=AB=AF=E8=AE=A4=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/App.vue | 5 +- web/src/api/auth.ts | 17 +++ web/src/api/index.ts | 42 ++++++- web/src/layouts/MainLayout.vue | 193 +++++++++++++++++++++++++++------ web/src/main.ts | 5 + web/src/router/index.ts | 92 +++++++++++----- web/src/stores/authStore.ts | 49 +++++++++ web/src/types/models.ts | 5 + web/src/views/Login.vue | 192 ++++++++++++++++++++++++++++++++ web/vite.config.ts | 11 +- 10 files changed, 545 insertions(+), 66 deletions(-) create mode 100644 web/src/api/auth.ts create mode 100644 web/src/stores/authStore.ts create mode 100644 web/src/views/Login.vue diff --git a/web/src/App.vue b/web/src/App.vue index 68b4fd7..8ca4818 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -1,7 +1,8 @@ diff --git a/web/src/api/auth.ts b/web/src/api/auth.ts new file mode 100644 index 0000000..f9f2b94 --- /dev/null +++ b/web/src/api/auth.ts @@ -0,0 +1,17 @@ +import apiClient from './index'; + +export interface LoginRequest { + auth_key: string; +} + +export interface LoginResponse { + success: boolean; + message: string; +} + +export const login = async (authKey: string): Promise => { + const response = await apiClient.post('/auth/login', { + auth_key: authKey + }); + return response.data; +}; \ No newline at end of file diff --git a/web/src/api/index.ts b/web/src/api/index.ts index 96899c5..1a8e347 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -1,4 +1,6 @@ import axios from "axios"; +import { useAuthStore } from "@/stores/authStore"; +import router from "@/router"; const apiClient = axios.create({ baseURL: "/api", @@ -7,8 +9,42 @@ const apiClient = axios.create({ }, }); -// 可以添加请求和响应拦截器 -// apiClient.interceptors.request.use(...) -// apiClient.interceptors.response.use(...) +// 请求拦截器:自动添加认证头 +apiClient.interceptors.request.use( + (config) => { + const authStore = useAuthStore(); + const authKey = authStore.getAuthKey(); + + if (authKey) { + config.headers.Authorization = `Bearer ${authKey}`; + } + + return config; + }, + (error) => { + return Promise.reject(error); + } +); + +// 响应拦截器:处理401认证失败 +apiClient.interceptors.response.use( + (response) => { + return response; + }, + (error) => { + if (error.response?.status === 401) { + // 认证失败,清除登录状态并跳转到登录页 + const authStore = useAuthStore(); + authStore.logout(); + + // 跳转到登录页(如果不在登录页的话) + if (router.currentRoute.value.path !== '/login') { + router.push('/login'); + } + } + + return Promise.reject(error); + } +); export default apiClient; diff --git a/web/src/layouts/MainLayout.vue b/web/src/layouts/MainLayout.vue index 93adabf..7b97577 100644 --- a/web/src/layouts/MainLayout.vue +++ b/web/src/layouts/MainLayout.vue @@ -1,54 +1,179 @@ \ No newline at end of file diff --git a/web/src/main.ts b/web/src/main.ts index 538c4b3..5b82ee3 100644 --- a/web/src/main.ts +++ b/web/src/main.ts @@ -4,6 +4,7 @@ import App from './App.vue' import router from './router' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' +import { useAuthStore } from './stores/authStore' const app = createApp(App) @@ -11,4 +12,8 @@ app.use(createPinia()) app.use(router) app.use(ElementPlus) +// 确保认证状态在应用启动时初始化 +const authStore = useAuthStore() +authStore.initializeAuth() + app.mount('#app') diff --git a/web/src/router/index.ts b/web/src/router/index.ts index 6985c4c..0ed3b1b 100644 --- a/web/src/router/index.ts +++ b/web/src/router/index.ts @@ -1,33 +1,50 @@ -import { createRouter, createWebHistory } from 'vue-router'; -import Dashboard from '../views/Dashboard.vue'; +import { createRouter, createWebHistory } from "vue-router"; +import { useAuthStore } from "../stores/authStore"; +import Dashboard from "../views/Dashboard.vue"; +import Login from "../views/Login.vue"; +import MainLayout from "../layouts/MainLayout.vue"; const routes = [ { - path: '/', - redirect: '/dashboard', + path: "/login", + name: "Login", + component: Login, + meta: { requiresAuth: false }, }, { - path: '/dashboard', - name: 'Dashboard', - component: Dashboard, - }, - { - path: '/groups', - name: 'Groups', - // route level code-splitting - // this generates a separate chunk (About.[hash].js) for this route - // which is lazy-loaded when the route is visited. - component: () => import('../views/Groups.vue'), - }, - { - path: '/logs', - name: 'Logs', - component: () => import('../views/Logs.vue'), - }, - { - path: '/settings', - name: 'Settings', - component: () => import('../views/Settings.vue'), + path: "/", + component: MainLayout, + meta: { requiresAuth: true }, + children: [ + { + path: "", + redirect: "/dashboard", + }, + { + path: "/dashboard", + name: "Dashboard", + component: Dashboard, + meta: { requiresAuth: true }, + }, + { + path: "/groups", + name: "Groups", + component: () => import("../views/Groups.vue"), + meta: { requiresAuth: true }, + }, + { + path: "/logs", + name: "Logs", + component: () => import("../views/Logs.vue"), + meta: { requiresAuth: true }, + }, + { + path: "/settings", + name: "Settings", + component: () => import("../views/Settings.vue"), + meta: { requiresAuth: true }, + }, + ], }, ]; @@ -36,4 +53,27 @@ const router = createRouter({ routes, }); -export default router; \ No newline at end of file +// 路由守卫 +router.beforeEach((to, _from, next) => { + const authStore = useAuthStore(); + const isAuthenticated = authStore.isAuthenticated; + const requiresAuth = to.matched.some( + (record) => record.meta.requiresAuth !== false + ); + + if (requiresAuth && !isAuthenticated) { + // 需要认证但未登录,重定向到登录页 + next({ + name: "Login", + query: { redirect: to.fullPath }, + }); + } else if (to.name === "Login" && isAuthenticated) { + // 已登录用户访问登录页,重定向到仪表盘 + next({ name: "Dashboard" }); + } else { + // 正常访问 + next(); + } +}); + +export default router; diff --git a/web/src/stores/authStore.ts b/web/src/stores/authStore.ts new file mode 100644 index 0000000..b31b032 --- /dev/null +++ b/web/src/stores/authStore.ts @@ -0,0 +1,49 @@ +import { defineStore } from 'pinia'; +import { ref, computed } from 'vue'; + +const AUTH_KEY_STORAGE = 'gpt-load-auth-key'; + +export const useAuthStore = defineStore('auth', () => { + // State + const authKey = ref(''); + + // Computed + const isAuthenticated = computed(() => !!authKey.value); + + // Actions + function login(key: string) { + authKey.value = key; + localStorage.setItem(AUTH_KEY_STORAGE, key); + } + + function logout() { + authKey.value = ''; + localStorage.removeItem(AUTH_KEY_STORAGE); + } + + function getAuthKey(): string { + return authKey.value; + } + + function initializeAuth() { + const storedKey = localStorage.getItem(AUTH_KEY_STORAGE); + if (storedKey) { + authKey.value = storedKey; + } + } + + // 在store初始化时自动恢复认证状态 + initializeAuth(); + + return { + // State + authKey, + // Computed + isAuthenticated, + // Actions + login, + logout, + getAuthKey, + initializeAuth, + }; +}); \ No newline at end of file diff --git a/web/src/types/models.ts b/web/src/types/models.ts index 6a053ce..2b2c38c 100644 --- a/web/src/types/models.ts +++ b/web/src/types/models.ts @@ -53,6 +53,11 @@ export interface Setting { key: string; value: string; } + +export interface AuthUser { + key: string; + isAuthenticated: boolean; +} export interface Setting { key: string; value: string; diff --git a/web/src/views/Login.vue b/web/src/views/Login.vue new file mode 100644 index 0000000..efbd5b9 --- /dev/null +++ b/web/src/views/Login.vue @@ -0,0 +1,192 @@ + + + + + \ No newline at end of file diff --git a/web/vite.config.ts b/web/vite.config.ts index c2526b3..6f046ba 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -13,5 +13,14 @@ export default defineConfig({ build: { outDir: 'dist' }, - base: './' + base: './', + server: { + proxy: { + '/api': { + target: 'http://localhost:3000', + changeOrigin: true, + secure: false + } + } + } })