M5: 方法更新移植 —— 移除 accountOnly/定时同步,本地登录与 Casdoor SSO 并存,登录时自动建号
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
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
- 网关: 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 恢复注册入口恒显
This commit is contained in:
@@ -1,353 +1,341 @@
|
||||
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);
|
||||
// M3:accountOnly(Casdoor 为账号唯一数据源)时隐藏注册入口,登录统一走登录弹窗(仅 Casdoor SSO)
|
||||
const [accountOnly, setAccountOnly] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
get("/sso/casdoor/config")
|
||||
.then((res: any) => setAccountOnly(!!res?.data?.accountOnly))
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
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: () => {
|
||||
// M3:accountOnly 时始终打开登录弹窗(内含 Casdoor 单点登录按钮),不走旧 OMS 登录页
|
||||
if (!accountOnly && (authMode === 'SSO' || isSSOAvailable())) {
|
||||
// SSO 模式:跳转到 ME 登录
|
||||
window.location.href = `${ME_LOGIN_URL}?redirect=${encodeURIComponent(window.location.href)}`;
|
||||
} else {
|
||||
// JWT 模式:显示登录对话框
|
||||
setLoginOpen(true);
|
||||
}
|
||||
},
|
||||
},
|
||||
// M3:accountOnly 时隐藏「注册」入口(后端 /api/user/signup 亦已 403)
|
||||
...(accountOnly ? [] : [
|
||||
{
|
||||
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}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -14,7 +14,6 @@ interface LoginDialogProps {
|
||||
|
||||
interface SsoConfig {
|
||||
enabled?: boolean;
|
||||
accountOnly?: boolean;
|
||||
loginUrl?: string;
|
||||
logoutUrl?: string;
|
||||
issuer?: string;
|
||||
@@ -44,8 +43,6 @@ export function LoginDialog({ open, onOpenChange, onLogin, loading, onSignupClic
|
||||
const [form] = Form.useForm()
|
||||
const [ssoEnabled, setSsoEnabled] = useState(false)
|
||||
const [ssoLoginUrl, setSsoLoginUrl] = useState("")
|
||||
// M3:accountOnly 时后端已 403 本地登录/注册,前端只保留 Casdoor 单点登录
|
||||
const [accountOnly, setAccountOnly] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
@@ -53,7 +50,6 @@ export function LoginDialog({ open, onOpenChange, onLogin, loading, onSignupClic
|
||||
if (!alive) return;
|
||||
setSsoEnabled(!!cfg.enabled && !!cfg.loginUrl);
|
||||
if (cfg.loginUrl) setSsoLoginUrl(cfg.loginUrl);
|
||||
setAccountOnly(!!cfg.accountOnly);
|
||||
});
|
||||
return () => {
|
||||
alive = false;
|
||||
@@ -93,27 +89,19 @@ export function LoginDialog({ open, onOpenChange, onLogin, loading, onSignupClic
|
||||
<div className="mb-4">
|
||||
<Button
|
||||
type="primary"
|
||||
ghost
|
||||
block
|
||||
icon={<ShieldCheck className="w-4 h-4" />}
|
||||
onClick={handleCasdoorLogin}
|
||||
>
|
||||
{t('user.loginDialog.casdoorLogin')}
|
||||
</Button>
|
||||
{!accountOnly && (
|
||||
<Divider plain style={{ margin: '12px 0' }}>
|
||||
{t('user.loginDialog.or')}
|
||||
</Divider>
|
||||
)}
|
||||
<Divider plain style={{ margin: '12px 0' }}>
|
||||
{t('user.loginDialog.or')}
|
||||
</Divider>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{accountOnly && !ssoEnabled && (
|
||||
<div className="mb-4 text-sm text-amber-600">
|
||||
本地登录已关闭,且 Casdoor 单点登录入口不可用,请联系管理员。
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!accountOnly && (
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
@@ -160,7 +148,6 @@ export function LoginDialog({ open, onOpenChange, onLogin, loading, onSignupClic
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
)}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user