diff --git a/frontend/src/components/auth/Login.tsx b/frontend/src/components/auth/Login.tsx
index 9022711..6a581a1 100644
--- a/frontend/src/components/auth/Login.tsx
+++ b/frontend/src/components/auth/Login.tsx
@@ -1,5 +1,5 @@
import { useState } from 'react';
-import { useNavigate } from 'react-router-dom';
+import { useNavigate, useSearchParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { User, Lock, Palette, Check, Loader2, AlertCircle, Eye, EyeOff, Languages, Activity } from 'lucide-react';
import { useUserStore, useLanguageStore } from '@/store';
@@ -22,6 +22,7 @@ import {
export function Login() {
const { t } = useTranslation();
const navigate = useNavigate();
+ const [searchParams] = useSearchParams();
const { theme, setTheme } = useTheme();
const { language, setLanguage } = useLanguageStore();
const [username, setUsername] = useState('admin');
@@ -38,7 +39,10 @@ export function Login() {
const result = await authApi.login(username, password) as { user: { id: number; username: string; email: string; status: number }; token: string; permissions?: string[] };
useUserStore.getState().login(result.user, result.token, result.permissions ?? null);
localStorage.setItem('token', result.token);
- navigate('/', { replace: true });
+ // 会话过期被踢到登录页时,登录成功后回到原页面;只接受站内相对路径,防开放跳转。 // Logan.Li
+ const redirect = searchParams.get('redirect');
+ const safeRedirect = redirect && redirect.startsWith('/') && !redirect.startsWith('//') ? redirect : '/';
+ navigate(safeRedirect, { replace: true });
} catch (err) {
const message = err instanceof Error ? err.message : t('login.loginFailed');
setError(message);
diff --git a/frontend/src/components/auth/ProtectedRoute.tsx b/frontend/src/components/auth/ProtectedRoute.tsx
index 7a05c38..8e6c2da 100644
--- a/frontend/src/components/auth/ProtectedRoute.tsx
+++ b/frontend/src/components/auth/ProtectedRoute.tsx
@@ -1,10 +1,13 @@
-import { Navigate } from 'react-router-dom';
+import { Navigate, useLocation } from 'react-router-dom';
import { useUserStore } from '@/store';
export function ProtectedRoute({ children }: { children: React.ReactNode }) {
const isAuthenticated = useUserStore((s) => s.isAuthenticated);
+ const location = useLocation();
if (!isAuthenticated) {
- return ;
+ // 携带当前路径,登录成功后回到原页面(借鉴 opsx-web-lite 的 redirect 回跳)。 // Logan.Li
+ const current = location.pathname + location.search;
+ return ;
}
return <>{children}>;
}
diff --git a/frontend/src/lib/bootLog.ts b/frontend/src/lib/bootLog.ts
new file mode 100644
index 0000000..adb4e12
--- /dev/null
+++ b/frontend/src/lib/bootLog.ts
@@ -0,0 +1,17 @@
+// 启动期控制台输出:打印 API 基础路径与运行环境信息,便于联调排查(参考 Vue 项目的启动横幅)。 // Logan.Li
+import { getApiBase, getPublicApiBase } from '@/utils/request';
+
+export function logBootInfo(): void {
+ const base = getApiBase();
+ const badgeStyle = 'background-color:#f4516c;color:#fff;padding:3px;border-radius:3px';
+ const textStyle = 'color:inherit';
+
+ // API 基础路径(baseURL 已包含 /api/v1,接口路径使用相对路径即可) // Logan.Li
+ console.log(`%cAIOps API Base:%c ${base}`, badgeStyle, textStyle);
+
+ // 当前应用运行环境信息 // Logan.Li
+ const fullUrl = window.location.href;
+ const hostname = window.location.hostname;
+ console.log('Domain:', hostname, 'Page:', fullUrl);
+ console.log('Mode:', import.meta.env.MODE, ' Public API:', getPublicApiBase());
+}
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
index a94b6d2..e9bb545 100644
--- a/frontend/src/main.tsx
+++ b/frontend/src/main.tsx
@@ -4,11 +4,15 @@ import './index.css'
// U3: i18n — 必须在 App 之前导入以初始化翻译实例
import './i18n'
import { getInitialTheme, applyThemeToDocument } from '@/utils/theme'
+import { logBootInfo } from '@/lib/bootLog'
import App from './App.tsx'
// 首屏前应用主题,避免闪烁 // Logan.Li
applyThemeToDocument(getInitialTheme())
+// 启动期打印 API 基础路径与环境信息,便于联调排查 // Logan.Li
+logBootInfo()
+
window.addEventListener('vite:preloadError', () => {
// 任一左侧子菜单懒加载资源在发版后失效时,统一整页刷新获取最新 index/chunk。 // Logan.Li
window.location.reload()
diff --git a/frontend/src/utils/request.ts b/frontend/src/utils/request.ts
index d0a2f74..e9d7f46 100644
--- a/frontend/src/utils/request.ts
+++ b/frontend/src/utils/request.ts
@@ -19,6 +19,22 @@ export interface ListResponse {
type Json = unknown;
+// 并发 401 只清理会话并跳转一次,避免多个请求同时过期时重复登出/提示(借鉴 opsx-web-lite)。 // Logan.Li
+let handling401 = false;
+
+/** 401 统一处理:清理本地会话并携带 redirect 跳转登录页 // Logan.Li */
+function handleUnauthorized(): void {
+ if (handling401) return;
+ handling401 = true;
+ clearToken();
+ useUserStore.getState().logout();
+ const current = window.location.pathname + window.location.search;
+ const target = current && current !== '/login'
+ ? `/login?redirect=${encodeURIComponent(current)}`
+ : '/login';
+ window.location.href = target;
+}
+
/** 统一 API 根路径,避免 get/post 与 download/upload 行为不一致 // Logan.Li */
export function getApiBase(): string {
const envBase = import.meta.env.VITE_API_BASE_URL;
@@ -93,16 +109,23 @@ async function request(
headers.Authorization = 'Bearer ' + token;
}
+ if (import.meta.env.DEV) {
+ // 开发环境输出请求日志,便于排查 404/代理类问题(借鉴 opsx-web-lite) // Logan.Li
+ console.log(`[HTTP] ${method} ${url}`);
+ }
+
const response = await fetch(fullUrl, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
});
+ if (import.meta.env.DEV) {
+ console.log(`[HTTP] ${method} ${url} → HTTP ${response.status}`);
+ }
+
if (response.status === 401) {
- clearToken();
- useUserStore.getState().logout();
- window.location.href = '/login';
+ handleUnauthorized();
throw new Error(i18n.t('common.requestts.sd3ab821c'));
}