diff --git a/serve/frontend/src/components/Layout.tsx b/serve/frontend/src/components/Layout.tsx index ce6a2d5..a1d891a 100644 --- a/serve/frontend/src/components/Layout.tsx +++ b/serve/frontend/src/components/Layout.tsx @@ -1,5 +1,6 @@ -import { NavLink, Outlet } from 'react-router-dom' -import { useQuery } from '@tanstack/react-query' +import { useState } from 'react' +import { NavLink, Outlet, useNavigate } from 'react-router-dom' +import { useQuery, useQueryClient } from '@tanstack/react-query' import { motion } from 'framer-motion' import { ToastContainer } from '@/components/Toast' import { AlertToastContainer } from '@/components/AlertToast' @@ -10,7 +11,6 @@ import { StockAnalysisBubble } from '@/components/stock-analysis/StockAnalysisBu import { useSettings, usePreferences, - useVersion, } from '@/lib/useSharedQueries' import { QK } from '@/lib/queryKeys' import { @@ -25,6 +25,10 @@ import { Layers3, Landmark, BookOpenCheck, + Moon, + Sun, + User, + LogOut, } from 'lucide-react' import { Logo } from './Logo' import { api } from '@/lib/api' @@ -76,14 +80,42 @@ function AIConfigBadge({ configured, model }: { configured?: boolean; model?: st export function Layout() { // ===== 共享 hooks (替代内联 useQuery) ===== const { data: settingsState } = useSettings() - const { data: versionData } = useVersion() const { data: prefs } = usePreferences() const { data: analysisMenus } = useQuery({ queryKey: QK.analysisMenus, queryFn: api.analysisMenus, }) + const { data: authData } = useQuery({ + queryKey: ['auth-status'], + queryFn: api.authStatus, + refetchInterval: 60_000, + }) - const version = versionData?.version + const username = authData?.username + const navigate = useNavigate() + const qc = useQueryClient() + const [darkMode, setDarkMode] = useState(() => { + if (typeof document === 'undefined') return true + try { + const saved = localStorage.getItem('theme') + if (saved === 'light' || saved === 'dark') return saved === 'dark' + } catch {} + return document.documentElement.classList.contains('dark') + }) + + // 切换暗夜模式 + const toggleDark = () => { + const next = !darkMode + setDarkMode(next) + document.documentElement.classList.toggle('dark', next) + try { localStorage.setItem('theme', next ? 'dark' : 'light') } catch {} + } + + const handleLogout = async () => { + try { await api.authLogout() } catch {} + qc.invalidateQueries({ queryKey: ['auth-status'] }) + navigate('/login', { replace: true }) + } // 合并内置页面 + 可见的扩展分析菜单 const analysisNav = (analysisMenus?.items ?? []) @@ -161,26 +193,47 @@ export function Layout() { ))} -
- - cn( - 'flex items-center justify-between gap-3 px-3 py-2 rounded-btn text-sm transition-colors duration-150 ease-smooth', - isActive - ? 'bg-elevated text-foreground font-medium' - : 'text-foreground/80 hover:bg-elevated hover:text-foreground', - ) - } - > - - - 设置 - - - {version ?? ''} - - +
+ {/* 登录用户 */} + {username && ( +
+ + {username} + +
+ )} + + {/* 暗夜模式 + 设置 */} +
+ + + cn( + 'flex items-center justify-center rounded-btn p-1.5 text-xs transition-colors', + isActive + ? 'bg-accent/10 text-accent' + : 'text-foreground/70 hover:bg-elevated hover:text-foreground', + ) + } + title="设置" + > + + +