diff --git a/README.md b/README.md index 7ed21f7..09f4a2a 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ npm start 默认会在浏览器打开: -- `http://localhost:3003/` +- `http://localhost:8081/` 如未自动打开,可手动在浏览器输入上述地址访问。 @@ -67,6 +67,21 @@ REACT_APP_API_BASE_URL=http://115.190.223.209:5232/api 你可以根据实际后端地址进行修改。 +## OIDC 单点登录联动 + +前端已与后端认证流程联动: + +1. 应用启动时调用 `GET /api/auth/me` 读取当前 Session 用户。 +2. 若未登录(401),前端自动跳转 `/api/login?next=<当前页面>`。 +3. 后端回调成功后会带着 `next` 回跳到前端原页面。 +4. 业务 API 请求若返回 401,也会自动触发登录跳转。 + +相关实现文件: + +- `src/services/auth.js`:登录跳转、退出、用户信息获取 +- `src/services/api.js`:axios 401 拦截并跳转登录 +- `src/App.jsx`:应用初始化时鉴权和用户显示 + ## 功能说明 - 左侧侧边栏 diff --git a/package.json b/package.json index 070e2b9..a19bff6 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "xlsx": "^0.18.5" }, "scripts": { - "start": "react-scripts start", + "start": "set PORT=8081 && react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" diff --git a/src/App.css b/src/App.css index 91a4622..a0c2513 100644 --- a/src/App.css +++ b/src/App.css @@ -128,7 +128,6 @@ body { .header-actions { display: flex; gap: 20px; - cursor: pointer; margin-left: auto; } @@ -140,33 +139,105 @@ body { height: 100%; } +.user-trigger { + display: inline-flex; + align-items: center; + justify-content: center; + width: 38px; + height: 38px; + padding: 0; + border: 1px solid #ececec; + border-radius: 999px; + background: linear-gradient(180deg, #ffffff 0%, #f7f7f7 100%); + color: #4b5563; + cursor: pointer; + box-shadow: 0 8px 20px rgba(15, 23, 42, 0.06); + transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease; +} + +.user-trigger.has-name { + justify-content: flex-start; + width: auto; + max-width: 220px; + padding: 0 12px 0 8px; + gap: 8px; + border-radius: 999px; +} + +.user-trigger:hover { + border-color: #d8dee8; + box-shadow: 0 10px 24px rgba(15, 23, 42, 0.1); +} + +.user-avatar { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 999px; + background: linear-gradient(180deg, #77c7ff 0%, #4b9dff 100%); + color: #fff; + flex: 0 0 auto; +} + +.user-name { + max-width: 150px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 14px; + color: #374151; +} + .user-tooltip { display: none; position: absolute; - top: 30px; - right: -10px; - background: #333; - color: #fff; - padding: 6px 10px; - border-radius: 4px; - font-size: 12px; - white-space: nowrap; + top: calc(100% + 10px); + right: 0; + min-width: 120px; + padding: 8px; + background: #fff; + border: 1px solid #ececec; + border-radius: 12px; + box-shadow: 0 16px 32px rgba(15, 23, 42, 0.12); z-index: 100; - box-shadow: 0 2px 8px rgba(0,0,0,0.15); } .user-tooltip::before { content: ''; position: absolute; - top: -4px; - right: 14px; - width: 8px; - height: 8px; - background: #333; + top: -6px; + right: 18px; + width: 12px; + height: 12px; + background: #fff; + border-top: 1px solid #ececec; + border-left: 1px solid #ececec; transform: rotate(45deg); } -.user-profile:hover .user-tooltip { +.user-menu-item { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + padding: 10px 12px; + border: none; + border-radius: 10px; + background: #fff; + color: #111827; + font-size: 14px; + cursor: pointer; + transition: background 0.2s ease, color 0.2s ease; +} + +.user-menu-item:hover { + background: #f5f7fb; +} + +.user-profile:hover .user-tooltip, +.user-profile:focus-within .user-tooltip { display: block; } @@ -314,10 +385,6 @@ body { background: #40a9ff; } -.table-container { - /* 让外层 page-scroll 负责滚动,这里不再额外横向滚动 */ -} - .loading { text-align: center; padding: 40px; diff --git a/src/App.jsx b/src/App.jsx index e2caaf0..db4e1fc 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,13 +1,19 @@ // App.jsx import React, { useState, useEffect } from 'react'; import { - User, ChevronDown, + User, X } from 'lucide-react'; import './App.css'; import FstTagPage from './pages/FstTagPage'; import VersionListPage from './pages/VersionListPage'; +import { + getCurrentUser, + isAuthCallbackPath, + redirectToLogin, + relayAuthCallbackToBackend +} from './services/auth'; const App = () => { // 页面组件注册表 @@ -59,6 +65,45 @@ const App = () => { return defaultTabs; }); + const [currentUser, setCurrentUser] = useState(null); + const [authLoading, setAuthLoading] = useState(true); + const displayName = currentUser?.name || currentUser?.preferred_username || ''; + + useEffect(() => { + if (isAuthCallbackPath()) { + relayAuthCallbackToBackend(); + return; + } + + let mounted = true; + + const loadUser = async () => { + try { + const data = await getCurrentUser(); + if (mounted) { + setCurrentUser(data.user || null); + setAuthLoading(false); + } + } catch (error) { + if (mounted) { + if (error?.status === 401) { + // 未登录,立即重定向,不显示 Loading 界面 + setAuthLoading(false); + redirectToLogin(); + return; + } + setAuthLoading(false); + } + } + }; + + loadUser(); + + return () => { + mounted = false; + }; + }, []); + // 监听 activeTab 变化,同步到 URL useEffect(() => { const slug = pageToSlug[activeTab]; @@ -110,6 +155,10 @@ const App = () => { }); }; + if (authLoading) { + return
Loading...
; + } + return (