From 84394f4674848e43e1374a76cc9712231757e667 Mon Sep 17 00:00:00 2001 From: hezhaohui Date: Tue, 14 Jul 2026 15:39:19 +0800 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20=E6=B7=BB=E5=8A=A0=E5=9F=BA?= =?UTF-8?q?=E7=A1=80=E8=AE=BE=E6=96=BD=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 工具函数: - utils/auth.ts,Token 存储、解析、过期校验 API 模块: - api/request.ts,Axios 实例封装,请求/响应拦截器 状态管理: - stores/auth.ts,认证状态管理(Pinia) 组合式函数: - composables/useAuth.ts,认证逻辑封装 路由配置: - router/index.ts,路由表定义、路由守卫(未登录重定向) --- frontend/src/api/request.ts | 44 +++++++++++ frontend/src/composables/useAuth.ts | 35 +++++++++ frontend/src/router/index.ts | 112 ++++++++++++++++++++++++++++ frontend/src/stores/auth.ts | 34 +++++++++ frontend/src/utils/auth.ts | 36 +++++++++ 5 files changed, 261 insertions(+) create mode 100644 frontend/src/api/request.ts create mode 100644 frontend/src/composables/useAuth.ts create mode 100644 frontend/src/router/index.ts create mode 100644 frontend/src/stores/auth.ts create mode 100644 frontend/src/utils/auth.ts diff --git a/frontend/src/api/request.ts b/frontend/src/api/request.ts new file mode 100644 index 0000000..295a2a4 --- /dev/null +++ b/frontend/src/api/request.ts @@ -0,0 +1,44 @@ +import axios from 'axios' +import { getToken, removeToken } from '@/utils/auth' +import { ElMessage } from 'element-plus' + +const request = axios.create({ + baseURL: '/api/v1', + timeout: 10000, +}) + +// 请求拦截器 +request.interceptors.request.use( + (config) => { + const token = getToken() + if (token) { + config.headers.Authorization = `Bearer ${token}` + } + return config + }, + (error) => { + return Promise.reject(error) + } +) + +// 响应拦截器 +request.interceptors.response.use( + (response) => { + const { data } = response + if (data.code !== 0) { + ElMessage.error(data.message || '请求失败') + return Promise.reject(new Error(data.message)) + } + return data + }, + (error) => { + if (error.response?.status === 401) { + removeToken() + window.location.href = '/login' + } + ElMessage.error(error.message || '网络错误') + return Promise.reject(error) + } +) + +export default request diff --git a/frontend/src/composables/useAuth.ts b/frontend/src/composables/useAuth.ts new file mode 100644 index 0000000..777807a --- /dev/null +++ b/frontend/src/composables/useAuth.ts @@ -0,0 +1,35 @@ +import { useRouter } from 'vue-router' +import { useAuthStore } from '@/stores/auth' +import { authApi } from '@/api/auth' + +export function useAuth() { + const router = useRouter() + const authStore = useAuthStore() + + const login = async (username: string, password: string) => { + const response = await authApi.login({ username, password }) + const { token, user } = response.data + authStore.setAuth(token, user) + return response + } + + const logout = async () => { + await authApi.logout() + authStore.clearAuth() + router.push('/login') + } + + const checkAuth = () => { + if (!authStore.isLoggedIn()) { + router.push('/login') + return false + } + return true + } + + return { + login, + logout, + checkAuth, + } +} diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts new file mode 100644 index 0000000..4e30cac --- /dev/null +++ b/frontend/src/router/index.ts @@ -0,0 +1,112 @@ +import { createRouter, createWebHistory } from 'vue-router' +import { getToken } from '@/utils/auth' + +const router = createRouter({ + history: createWebHistory(), + routes: [ + { + path: '/login', + name: 'Login', + component: () => import('@/views/auth/Login.vue'), + meta: { requiresAuth: false }, + }, + { + path: '/', + component: () => import('@/layouts/DefaultLayout.vue'), + meta: { requiresAuth: true }, + children: [ + { + path: '', + redirect: '/dashboard', + }, + { + path: 'dashboard', + name: 'Dashboard', + component: () => import('@/views/dashboard/Index.vue'), + meta: { title: '资源大盘' }, + }, + { + path: 'subsystem', + name: 'Subsystem', + component: () => import('@/views/subsystem/Navigation.vue'), + meta: { title: '子系统导航' }, + }, + { + path: 'audit/login', + name: 'LoginAudit', + component: () => import('@/views/audit/LoginAudit.vue'), + meta: { title: '登录审计' }, + }, + { + path: 'audit/ops', + name: 'OpsAudit', + component: () => import('@/views/audit/OpsAudit.vue'), + meta: { title: '运维操作审计' }, + }, + { + path: 'resource/status', + name: 'ResourceStatus', + component: () => import('@/views/resource/StatusBoard.vue'), + meta: { title: '资源状态看板' }, + }, + { + path: 'resource/mgmt', + name: 'ResourceMgmt', + component: () => import('@/views/resource/Management.vue'), + meta: { title: '资源管理' }, + }, + { + path: 'cluster', + name: 'Cluster', + component: () => import('@/views/cluster/ClusterList.vue'), + meta: { title: '集群 / 节点' }, + }, + { + path: 'service/catalog', + name: 'ServiceCatalog', + component: () => import('@/views/service/Catalog.vue'), + meta: { title: '基础服务目录' }, + }, + { + path: 'service/mgmt', + name: 'ServiceMgmt', + component: () => import('@/views/service/Management.vue'), + meta: { title: '服务管理' }, + }, + { + path: 'tasks', + name: 'Tasks', + component: () => import('@/views/task/TaskCenter.vue'), + meta: { title: '任务中心' }, + }, + { + path: 'config', + name: 'ConfigCenter', + component: () => import('@/views/config/ConfigCenter.vue'), + meta: { title: '配置中心管理' }, + }, + { + path: 'monitor', + name: 'Monitor', + component: () => import('@/views/monitor/Monitor.vue'), + meta: { title: '监控 / 日志 / 告警' }, + }, + ], + }, + ], +}) + +// 路由守卫 +router.beforeEach((to, from, next) => { + const token = getToken() + + if (to.meta.requiresAuth !== false && !token) { + next('/login') + } else if (to.path === '/login' && token) { + next('/') + } else { + next() + } +}) + +export default router diff --git a/frontend/src/stores/auth.ts b/frontend/src/stores/auth.ts new file mode 100644 index 0000000..92c7928 --- /dev/null +++ b/frontend/src/stores/auth.ts @@ -0,0 +1,34 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { getToken, setToken, removeToken, getUser, setUser, removeUser } from '@/utils/auth' + +export const useAuthStore = defineStore('auth', () => { + const token = ref(getToken()) + const user = ref(getUser()) + + function setAuth(newToken: string, newUser: any) { + token.value = newToken + user.value = newUser + setToken(newToken) + setUser(newUser) + } + + function clearAuth() { + token.value = null + user.value = null + removeToken() + removeUser() + } + + function isLoggedIn() { + return !!token.value + } + + return { + token, + user, + setAuth, + clearAuth, + isLoggedIn, + } +}) diff --git a/frontend/src/utils/auth.ts b/frontend/src/utils/auth.ts new file mode 100644 index 0000000..b51c2d7 --- /dev/null +++ b/frontend/src/utils/auth.ts @@ -0,0 +1,36 @@ +const TOKEN_KEY = 'xinfra_token' +const USER_KEY = 'xinfra_user' + +export function getToken(): string | null { + return localStorage.getItem(TOKEN_KEY) +} + +export function setToken(token: string): void { + localStorage.setItem(TOKEN_KEY, token) +} + +export function removeToken(): void { + localStorage.removeItem(TOKEN_KEY) +} + +export function getUser(): any { + const userStr = localStorage.getItem(USER_KEY) + return userStr ? JSON.parse(userStr) : null +} + +export function setUser(user: any): void { + localStorage.setItem(USER_KEY, JSON.stringify(user)) +} + +export function removeUser(): void { + localStorage.removeItem(USER_KEY) +} + +export function isTokenExpired(token: string): boolean { + try { + const payload = JSON.parse(atob(token.split('.')[1])) + return payload.exp * 1000 < Date.now() + } catch { + return true + } +}