Files
deepdata/frontend/src/pages/Layout/Header.tsx
T
figmar 4e2ec55167
CodeQL Advanced / Analyze (actions) (push) Canceled after 0s
CodeQL Advanced / Analyze (java-kotlin) (push) Canceled after 0s
CodeQL Advanced / Analyze (javascript-typescript) (push) Canceled after 0s
CodeQL Advanced / Analyze (python) (push) Canceled after 0s
Docker Image Build & Push / manifest (push) Canceled after 0s
Docker Image Build & Push / build-and-push-arm (push) Canceled after 0s
Docker Image Build & Push / build-and-push-amd (push) Canceled after 0s
Docker Image Build & Push / manifest (push) Canceled after 0s
Docker Image Build & Push / build-and-push-arm (push) Canceled after 0s
Docker Image Build & Push / build-and-push-amd (push) Canceled after 0s
Frontend Docker Image CI / Build and Push Frontend Docker Image (push) Canceled after 0s
Gateway Docker Image CI / Build and Push Gateway Docker Image (push) Canceled after 0s
M5: 方法更新移植 —— 移除 accountOnly/定时同步,本地登录与 Casdoor SSO 并存,登录时自动建号
- 网关: CasdoorProperties 移除 accountOnly/sync* 配置块; SsoConfigController 移除 /sync 管理端点; UserController 移除本地登录/注册 403
- 删除 CasdoorAccountSyncService/CasdoorAdminClient/CasdoorUserDto (定时全量同步方案废弃, 由 CasdoorUserService.upsertFromCasdoor 登录时自动建号替代)
- 保留既有修复: JJWT 0.12 适配(IdTokenVerifier/validateToken), updateById 修主键, RoleResolver owner/name 组名处理, WebFlux 兼容
- 前端: LoginDialog 移除 accountOnly 条件(本地登录+SSO 按钮恒显), Header 恢复注册入口恒显
2026-09-01 18:04:28 +08:00

341 lines
11 KiB
TypeScript

import { User, Globe, LogIn, UserPlus, Sparkles, Shield } from "lucide-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";
interface UserResponse {
username: string;
email?: string;
groupId?: string;
authenticated: boolean;
authMode: 'SSO' | 'JWT' | 'NONE';
requireLogin?: boolean; // 是否强制要求登录(由 DEEPDATA_JWT_ENABLE 控制)
role?: string; // Casdoor 主角色:admin/dev/annotator/viewer(由 RoleResolver 解析)
groups?: string[]; // Casdoor 用户组(含 deepdata-<角色> 前缀组)
}
function loginUsingPost(data: any) {
return post("/api/user/login", data);
}
function signupUsingPost(data: any) {
return post("/api/user/signup", data);
}
function getCurrentUser() {
return get<UserResponse>("/api/user/me");
}
// DeepData SSO 登录 URL(根据实际环境修改)
const ME_LOGIN_URL = import.meta.env.VITE_ME_LOGIN_URL || 'https://deepdata.ailab.yilise.com/login';
const OMS_LOGOUT_URL = import.meta.env.VITE_OMS_LOGOUT_URL || 'https://oms-service/logout';
export function Header() {
const { t } = useTranslation();
const [loginOpen, setLoginOpen] = useState(false)
const [signupOpen, setSignupOpen] = useState(false)
const [loading, setLoading] = useState(false);
const [currentUser, setCurrentUser] = useState<UserResponse | null>(null);
const [authMode, setAuthMode] = useState<'SSO' | 'JWT' | 'NONE'>('NONE');
const [userLoading, setUserLoading] = useState(true);
const handleLogin = async (values: { username: string; password: string }) => {
try {
setLoading(true);
const response = await loginUsingPost(values);
// Store the token in localStorage
localStorage.setItem('session', JSON.stringify(response.data));
message.success(t('user.messages.loginSuccess'));
setLoginOpen(false);
// Optionally refresh the page or update the UI
window.location.reload();
} catch (error) {
console.error('Login error:', error);
message.error(t('user.messages.loginFailed'));
} finally {
setLoading(false);
}
};
const handleSignup = async (values: {
username: string;
email: string;
password: string;
confirmPassword: string
}) => {
if (values.password !== values.confirmPassword) {
message.error(t('user.messages.passwordMismatch'));
return;
}
try {
setLoading(true);
const { username, email, password } = values;
const response = await signupUsingPost({ username, email, password });
message.success(t('user.messages.signupSuccess'));
localStorage.setItem('session', JSON.stringify(response.data));
setSignupOpen(false);
} catch (error) {
console.error('Registration error:', error);
message.error(t('user.messages.signupFailed'));
} finally {
setLoading(false);
}
};
const handleLogout = () => {
if (authMode === 'SSO') {
// SSO 模式:检查是否配置了有效的登出 URL
const logoutUrl = OMS_LOGOUT_URL;
// 如果配置的是默认值(内部 service 名称),只清除本地状态
if (logoutUrl.includes('oms-service') || logoutUrl.includes('localhost')) {
console.warn('OMS logout URL not configured or using internal address, skipping redirect');
setCurrentUser(null);
setAuthMode('NONE');
message.success(t('user.messages.logoutSuccess'));
window.location.reload();
} else {
// 使用配置的登出 URL
window.location.href = `${logoutUrl}?redirect=${encodeURIComponent(window.location.href)}`;
}
} else {
// JWT 模式:清除本地 session
localStorage.removeItem('session');
setCurrentUser(null);
setAuthMode('NONE');
message.success(t('user.messages.logoutSuccess'));
window.location.reload();
}
};
const openLoginDialog = () => {
setLoginOpen(true);
};
const openSignupDialog = () => {
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(() => {});
}, []);
// 检测是否在生产环境(deepdata 域名)
const isSSOAvailable = () => {
const hostname = window.location.hostname;
// 通过域名或注入的全局变量判断
return hostname.includes('deepdata') ||
hostname.includes('yilise.com') ||
(window as any).__DEEPDATA_ENV__ === true;
};
// 获取当前用户信息(支持双模式)
useEffect(() => {
const fetchCurrentUser = async () => {
try {
const response = await getCurrentUser();
if (response.data) {
setCurrentUser(response.data);
setAuthMode(response.data.authMode);
// 如果未登录,根据 requireLogin 决定是否弹出登录框
if (!response.data.authenticated) {
if (response.data.requireLogin) {
// 强制要求登录:弹出登录框
window.dispatchEvent(new CustomEvent('show-login'));
}
// 不强制登录:允许匿名访问
}
}
} catch (error) {
console.error('Failed to fetch current user:', error);
// 请求失败时,保持未登录状态
} finally {
setUserLoading(false);
}
};
fetchCurrentUser();
}, []);
const languageMenuItems: MenuProps['items'] = [
{
key: 'zh',
label: t('header.simplifiedChinese'),
onClick: () => {
i18n.changeLanguage('zh');
localStorage.setItem('language', 'zh');
},
},
{
key: 'en',
label: t('header.english'),
onClick: () => {
i18n.changeLanguage('en');
localStorage.setItem('language', 'en');
},
}
]
const userDropdownItems: MenuProps['items'] = currentUser?.authenticated
? [
{
key: 'profile',
label: (
<div className="flex flex-col">
<span className="font-medium">{currentUser.username}</span>
<span className="text-xs text-gray-500">
{authMode === 'SSO' ? (
<span className="flex items-center gap-1">
<Shield className="h-3 w-3" />
{t('user.authMode.sso')}
</span>
) : (
<span className="flex items-center gap-1">
<Sparkles className="h-3 w-3" />
{t('user.authMode.jwt')}
</span>
)}
</span>
</div>
),
},
currentUser.groupId && {
key: 'groupId',
label: `${t('user.group')}: ${currentUser.groupId}`,
disabled: true,
},
currentUser.email && {
key: 'email',
label: currentUser.email,
disabled: true,
},
// 只有 JWT 模式才显示退出登录按钮
...(authMode !== 'SSO' ? [
{
type: 'divider',
},
{
key: 'logout',
label: t('user.actions.logout'),
icon: <LogIn className="h-4 w-4" />,
onClick: handleLogout,
},
] : []),
]
: [
{
key: 'login',
label: authMode === 'SSO' || isSSOAvailable()
? t('user.actions.gotoLogin')
: t('user.actions.login'),
icon: <LogIn className="h-4 w-4" />,
onClick: () => {
if (authMode === 'SSO' || isSSOAvailable()) {
// SSO 模式:跳转到 ME 登录
window.location.href = `${ME_LOGIN_URL}?redirect=${encodeURIComponent(window.location.href)}`;
} else {
// JWT 模式:显示登录对话框
setLoginOpen(true);
}
},
},
{
type: 'divider',
},
{
key: 'register',
label: t('user.actions.register'),
icon: <UserPlus className="h-4 w-4" />,
onClick: () => {
if (authMode === 'SSO' || isSSOAvailable()) {
message.info(t('user.messages.useSSO'));
} else {
setSignupOpen(true);
}
},
},
];
return (
<>
<header className="sticky top-0 z-50 w-full border-b border-gray-200 bg-white">
<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="/" onClick={handleHomeClick} className="flex items-center gap-2 cursor-pointer">
{/* xuhuanqing */}
{/*
<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>
*/}
<span className="text-lg font-bold text-gray-900">DeepData</span>
</NavLink>
</div>
</div>
<div className="flex items-center gap-4">
<Dropdown
menu={{ items: languageMenuItems }}
placement="bottomRight"
>
<Button type="text" className="flex items-center gap-2">
<Globe className="h-4 w-4" />
<span>{i18n.language === 'zh' ? t('header.simplifiedChinese') : t('header.english')}</span>
</Button>
</Dropdown>
<Dropdown
menu={{ items: userDropdownItems }}
placement="bottomRight"
overlayClassName="w-40"
>
<Button type="text" icon={<User className="h-4 w-4" />} loading={userLoading}>
{currentUser?.authenticated && !userLoading && (
<span className="ml-1">{currentUser.username}</span>
)}
</Button>
</Dropdown>
</div>
</div>
</header>
<LoginDialog
open={loginOpen}
onOpenChange={setLoginOpen}
onLogin={handleLogin}
loading={loading}
onSignupClick={openSignupDialog}
/>
<SignupDialog
open={signupOpen}
onOpenChange={setSignupOpen}
onSignup={handleSignup}
loading={loading}
onLoginClick={openLoginDialog}
/>
</>
)
}
export default memo(Header);