Navbar.tsx 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. import React, { useState, useEffect, useCallback, useRef } from 'react';
  2. import { useNavigate } from 'react-router-dom';
  3. import { LogIn, User, LogOut, ChevronDown, ShieldCheck, ShieldAlert } from '../icons/commonIcons';
  4. import { Lightbulb } from '../icons/commonIcons';
  5. import { authService, UserInfo } from '../services/authService';
  6. import { BrandingContext } from '../App';
  7. const Navbar: React.FC = () => {
  8. const navigate = useNavigate();
  9. const branding = React.useContext(BrandingContext);
  10. const [userInfo, setUserInfo] = useState<UserInfo | null>(null);
  11. const [isAuthenticated, setIsAuthenticated] = useState(false);
  12. const [, forceUpdate] = useState({});
  13. const [showDropdown, setShowDropdown] = useState(false);
  14. const dropdownRef = useRef<HTMLDivElement>(null);
  15. const checkAuthStatus = useCallback(() => {
  16. const authenticated = authService.isAuthenticated();
  17. const user = authService.getUserInfo();
  18. console.log('[Navbar] checkAuthStatus - authenticated:', authenticated, 'user:', user);
  19. setIsAuthenticated(authenticated);
  20. setUserInfo(authenticated ? user : null);
  21. forceUpdate({});
  22. }, []);
  23. useEffect(() => {
  24. // 初始检查
  25. checkAuthStatus();
  26. // 订阅认证状态变化
  27. const unsubscribe = authService.subscribe(() => {
  28. console.log('[Navbar] auth_state_change event received');
  29. checkAuthStatus();
  30. });
  31. // 监听存储变化(其他标签页)
  32. const handleStorageChange = (e: StorageEvent) => {
  33. if (e.key === 'aigc_space_token' || e.key === 'aigc_space_user_info' || e.key === null) {
  34. checkAuthStatus();
  35. }
  36. };
  37. window.addEventListener('storage', handleStorageChange);
  38. return () => {
  39. unsubscribe();
  40. window.removeEventListener('storage', handleStorageChange);
  41. };
  42. }, [checkAuthStatus]);
  43. const handleLogin = () => {
  44. navigate('/login');
  45. };
  46. const handleProfile = () => {
  47. setShowDropdown(false);
  48. navigate('/profile');
  49. };
  50. const handleLogout = async () => {
  51. setShowDropdown(false);
  52. // 先本地清理登录状态
  53. try {
  54. if (typeof window !== 'undefined') {
  55. localStorage.removeItem('aigc_space_token');
  56. localStorage.removeItem('aigc_space_refresh_token');
  57. localStorage.removeItem('aigc_space_user_info');
  58. }
  59. } catch (e) {
  60. // ignore
  61. }
  62. if (typeof window === 'undefined') return;
  63. // 查询后端 SSO 配置,决定跳转目标
  64. try {
  65. const cfg = await authService.getSsoConfig();
  66. if (cfg?.sso_enabled) {
  67. // SSO 启用:跳 CAS 注销页
  68. const casLogoutUrl = cfg.cas_login_url.replace(/\/login$/, '/logout');
  69. try {
  70. if (window.top && window.top !== window) {
  71. window.top.location.replace(casLogoutUrl);
  72. } else {
  73. window.location.replace(casLogoutUrl);
  74. }
  75. } catch {
  76. window.location.href = casLogoutUrl;
  77. }
  78. } else {
  79. // SSO 未启用:直接跳普通登录页
  80. window.location.replace('/');
  81. }
  82. } catch {
  83. window.location.replace('/');
  84. }
  85. };
  86. // 点击外部关闭下拉菜单
  87. useEffect(() => {
  88. const handleClickOutside = (event: MouseEvent) => {
  89. if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
  90. setShowDropdown(false);
  91. }
  92. };
  93. document.addEventListener('mousedown', handleClickOutside);
  94. return () => document.removeEventListener('mousedown', handleClickOutside);
  95. }, []);
  96. return (
  97. <header className="fixed top-0 left-0 right-0 h-14 sm:h-16 bg-white border-b border-gray-100 flex items-center justify-between px-4 sm:px-6 z-50">
  98. {/* 左侧:标题 */}
  99. <div className="flex items-center space-x-2">
  100. <div className="w-7 h-7 sm:w-8 sm:h-8 bg-blue-600 rounded-lg flex items-center justify-center flex-shrink-0 overflow-hidden">
  101. {branding.system_logo
  102. ? <img src={branding.system_logo} alt="logo" className="w-full h-full object-contain" />
  103. : <Lightbulb className="text-white w-4 h-4 sm:w-5 sm:h-5" />}
  104. </div>
  105. <span className="text-base sm:text-xl font-bold text-blue-600 tracking-tight hidden xs:inline sm:inline">
  106. {branding.system_name}
  107. </span>
  108. </div>
  109. {/* 右侧:用户操作 */}
  110. <div className="flex items-center space-x-2 sm:space-x-4">
  111. {isAuthenticated ? (
  112. <>
  113. {/* 用户头像和昵称 - 下拉菜单 */}
  114. <div className="relative" ref={dropdownRef}>
  115. <div
  116. onClick={() => setShowDropdown(!showDropdown)}
  117. className="flex items-center space-x-1 sm:space-x-2 cursor-pointer group hover:bg-gray-50 rounded-lg px-1 sm:px-2 py-1 transition-colors"
  118. >
  119. <div className="w-7 h-7 sm:w-8 sm:h-8 rounded-full overflow-hidden border border-gray-200 flex-shrink-0">
  120. {userInfo?.avatar ? (
  121. <img
  122. src={userInfo.avatar}
  123. alt="用户头像"
  124. className="w-full h-full object-cover"
  125. />
  126. ) : (
  127. <div className="w-full h-full bg-gradient-to-br from-blue-400 to-blue-600 flex items-center justify-center text-white text-xs sm:text-sm font-bold">
  128. {userInfo?.nickname?.charAt(0)?.toUpperCase() || 'U'}
  129. </div>
  130. )}
  131. </div>
  132. <div className="hidden sm:flex items-center">
  133. <span className="text-sm font-medium text-gray-700 group-hover:text-blue-600 transition-colors">
  134. {userInfo?.nickname || '用户'}
  135. </span>
  136. <ChevronDown className={`w-4 h-4 ml-1 text-gray-400 transition-transform ${showDropdown ? 'rotate-180' : ''}`} />
  137. </div>
  138. <ChevronDown className={`w-3 h-3 sm:hidden text-gray-400 transition-transform ${showDropdown ? 'rotate-180' : ''}`} />
  139. </div>
  140. {/* 下拉菜单 */}
  141. {showDropdown && (
  142. <div className="absolute right-0 top-full mt-2 w-40 bg-white rounded-lg shadow-lg border border-gray-100 py-1 z-50">
  143. <button
  144. onClick={handleProfile}
  145. className="w-full flex items-center space-x-2 px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 hover:text-blue-600 transition-colors"
  146. >
  147. <User className="w-4 h-4" />
  148. <span>个人中心</span>
  149. </button>
  150. <button
  151. onClick={handleLogout}
  152. className="w-full flex items-center space-x-2 px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 hover:text-red-600 transition-colors"
  153. >
  154. <LogOut className="w-4 h-4" />
  155. <span>退出登录</span>
  156. </button>
  157. </div>
  158. )}
  159. </div>
  160. </>
  161. ) : (
  162. /* 未登录状态 */
  163. <div className="flex items-center space-x-3">
  164. <button
  165. onClick={handleLogin}
  166. className="flex items-center space-x-1 sm:space-x-2 px-3 sm:px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-sm font-medium"
  167. >
  168. <LogIn className="w-4 h-4" />
  169. <span>登录</span>
  170. </button>
  171. </div>
  172. )}
  173. </div>
  174. </header>
  175. );
  176. };
  177. export default Navbar;