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 (
)
}