Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions frontend/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { store } from "./store";
import { Provider } from "react-redux";
import theme from "./theme";
import {errorConfigStore} from "@/utils/errorConfigStore.ts";
import { setCachedHomePageUrl, getCachedHomePageUrl } from "@/utils/systemParam";
import "@/i18n";

function showLoadingUI() {
Expand Down Expand Up @@ -43,10 +44,84 @@ function showLoadingUI() {
`;
}

/**
* 从 localStorage 读取 JWT token
*/
function getAuthToken(): string | null {
const session = localStorage.getItem('session');
if (session) {
try {
return JSON.parse(session).token || null;
} catch {
return null;
}
}
return null;
}

/**
* 自定义首页URL重定向
* 在任何渲染之前检查系统参数 sys.home.page.url,若已配置则立即跳转,确保无闪烁。
* 使用原始 fetch 但携带 JWT token,避免已登录用户仍收到 401。
*/
async function checkHomePageRedirect(): Promise<{ redirected: boolean; authNeeded: boolean }> {
if (window.location.pathname !== '/') {
return { redirected: false, authNeeded: false };
}

const headers: Record<string, string> = { 'Content-Type': 'application/json' };
const token = getAuthToken();
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}

try {
const response = await fetch('/api/sys-param/sys.home.page.url', {
method: 'GET',
credentials: 'include',
headers,
});
if (response.ok) {
const result = await response.json();
const url = result?.data?.paramValue?.trim();
if (url) {
setCachedHomePageUrl(url);
window.location.replace(url);
return { redirected: true, authNeeded: false };
}
// 参数存在但值为空 → 管理员已清除,清掉缓存
setCachedHomePageUrl(null);
} else if (response.status === 401) {
// 未登录,尝试从缓存读取
const cachedUrl = getCachedHomePageUrl();
if (cachedUrl) {
window.location.replace(cachedUrl);
return { redirected: true, authNeeded: false };
}
// 未登录且无缓存,需要弹出登录框
return { redirected: false, authNeeded: true };
}
} catch {
// 网络错误等,尝试从缓存读取
const cachedUrl = getCachedHomePageUrl();
if (cachedUrl) {
window.location.replace(cachedUrl);
return { redirected: true, authNeeded: false };
}
}
return { redirected: false, authNeeded: false };
}

async function bootstrap() {
const container = document.getElementById("root");
if (!container) return;

// 在任何 UI 渲染之前检查自定义首页重定向
const { redirected, authNeeded } = await checkHomePageRedirect();
if (redirected) {
return;
}

showLoadingUI();

try {
Expand All @@ -72,6 +147,13 @@ async function bootstrap() {
</Provider>
</StrictMode>
);

// 未登录且无缓存时,等 React 挂载后弹出登录框
if (authNeeded) {
setTimeout(() => {
window.dispatchEvent(new CustomEvent('show-login'));
}, 500);
}
}

bootstrap();
19 changes: 17 additions & 2 deletions frontend/src/pages/Layout/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { User, Globe, LogIn, UserPlus, Sparkles, Shield } from "lucide-react"
import { memo, useState, useEffect } from "react";
import { memo, useState, useEffect, useCallback } from "react";
import { NavLink } from "react-router";
import { Button, Dropdown, message } from "antd"
import type { MenuProps } from 'antd'
import { LoginDialog } from "./LoginDialog"
import { SignupDialog } from "./SignupDialog"
import { post, get } from "@/utils/request.ts";
import { getCachedHomePageUrl, setCachedHomePageUrl } from "@/utils/systemParam";
import { getHomePageUrl } from "@/utils/systemParam";
import { useTranslation } from "react-i18next";
import i18n from "@/i18n";

Expand Down Expand Up @@ -121,6 +123,19 @@ export function Header() {
setSignupOpen(true);
};

const handleHomeClick = useCallback((e: React.MouseEvent) => {
const homeUrl = getCachedHomePageUrl();
if (homeUrl) {
e.preventDefault();
window.location.href = homeUrl;
}
}, []);

// 已登录时后台刷新缓存,保持与后端同步
useEffect(() => {
getHomePageUrl().then(url => setCachedHomePageUrl(url)).catch(() => {});
}, []);

// 检测是否在 ME 环境
const isSSOAvailable = () => {
const hostname = window.location.hostname;
Expand Down Expand Up @@ -269,7 +284,7 @@ export function Header() {
<div className="flex h-14 items-center justify-between px-6">
<div className="flex items-center gap-2">
<div className="flex items-center gap-2">
<NavLink to="/" className="flex items-center gap-2 cursor-pointer">
<NavLink to="/" onClick={handleHomeClick} className="flex items-center gap-2 cursor-pointer">
<div className="w-8 h-8 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-lg flex items-center justify-center">
<Sparkles className="w-5 h-5 text-white" />
</div>
Expand Down
22 changes: 21 additions & 1 deletion frontend/src/utils/systemParam.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,27 @@
/**
* System Parameter API
* 系统参数 API 接口
*/

// localStorage 缓存 key
const HOME_PAGE_URL_CACHE_KEY = 'datamate:homePageUrl';

/**
* 将首页URL写入缓存
*/
export function setCachedHomePageUrl(url: string | null) {
if (url) {
localStorage.setItem(HOME_PAGE_URL_CACHE_KEY, url);
} else {
localStorage.removeItem(HOME_PAGE_URL_CACHE_KEY);
}
}

/**
* 同步读取缓存的首页URL
*/
export function getCachedHomePageUrl(): string | null {
return localStorage.getItem(HOME_PAGE_URL_CACHE_KEY);
}
import { get } from '@/utils/request';

export interface SysParam {
Expand Down
Loading