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' import { AiAnalysisHost } from '@/components/financials/AiAnalysisHost' import { AiReportBubble } from '@/components/financials/AiReportBubble' import { StockAnalysisHost } from '@/components/stock-analysis/StockAnalysisHost' import { StockAnalysisBubble } from '@/components/stock-analysis/StockAnalysisBubble' import { useSettings, usePreferences, } from '@/lib/useSharedQueries' import { QK } from '@/lib/queryKeys' import { FileText, Settings, LayoutDashboard, Tags, TrendingUp, Flame, BarChart3, Sparkles, Layers3, Landmark, BookOpenCheck, Moon, Sun, User, LogOut, } from 'lucide-react' import { Logo } from './Logo' import { api } from '@/lib/api' import { cn } from '@/lib/cn' // 品牌色 — 只用于 logo / brand 区域,不影响功能语义色 const BRAND = '#8B5CF6' const nav = [ { to: '/', label: '看板', icon: LayoutDashboard }, { to: '/stock-analysis', label: '个股分析', icon: TrendingUp }, { to: '/limit-ladder', label: '连板梯队', icon: Flame }, { to: '/concept-analysis', label: '概念分析', icon: Layers3 }, { to: '/industry-analysis', label: '行业分析', icon: Landmark }, { to: '/financials', label: '财务分析', icon: FileText }, { to: '/review', label: '复盘', icon: BookOpenCheck }, { to: '/indices', label: '指数', icon: BarChart3 }, ] as const function AIConfigBadge({ configured, model }: { configured?: boolean; model?: string }) { return (
AI 配置
{configured ? (model || '已接入模型') : '接入策略生成模型'}
) } export function Layout() { // ===== 共享 hooks (替代内联 useQuery) ===== const { data: settingsState } = useSettings() 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 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 ?? []) .filter(m => m.visible) .map(m => ({ to: `/analysis/${m.id}`, label: m.label, icon: m.icon === 'tags' ? Tags : BarChart3 })) const allNav = [...nav, ...analysisNav] const savedOrder = prefs?.nav_order ?? [] const navItems = savedOrder.length > 0 ? (() => { const byTo = new Map(allNav.map(n => [n.to, n])) const ordered = savedOrder .map(id => byTo.get(id) ?? byTo.get(`/analysis/${id}`)) .filter(Boolean) const seen = new Set(ordered.map(n => n!.to)) return [...ordered as typeof allNav, ...allNav.filter(n => !seen.has(n.to))] })() : allNav const hiddenIds = new Set(prefs?.nav_hidden ?? []) const visibleNavItems = navItems.filter(n => !hiddenIds.has(n.to) && !hiddenIds.has(n.to.replace(/^\/analysis\//, ''))) return (
) }