@@ -1,5 +1,6 @@
|
|||||||
import { NavLink, Outlet } from 'react-router-dom'
|
import { useState } from 'react'
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { NavLink, Outlet, useNavigate } from 'react-router-dom'
|
||||||
|
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
import { motion } from 'framer-motion'
|
import { motion } from 'framer-motion'
|
||||||
import { ToastContainer } from '@/components/Toast'
|
import { ToastContainer } from '@/components/Toast'
|
||||||
import { AlertToastContainer } from '@/components/AlertToast'
|
import { AlertToastContainer } from '@/components/AlertToast'
|
||||||
@@ -10,7 +11,6 @@ import { StockAnalysisBubble } from '@/components/stock-analysis/StockAnalysisBu
|
|||||||
import {
|
import {
|
||||||
useSettings,
|
useSettings,
|
||||||
usePreferences,
|
usePreferences,
|
||||||
useVersion,
|
|
||||||
} from '@/lib/useSharedQueries'
|
} from '@/lib/useSharedQueries'
|
||||||
import { QK } from '@/lib/queryKeys'
|
import { QK } from '@/lib/queryKeys'
|
||||||
import {
|
import {
|
||||||
@@ -25,6 +25,10 @@ import {
|
|||||||
Layers3,
|
Layers3,
|
||||||
Landmark,
|
Landmark,
|
||||||
BookOpenCheck,
|
BookOpenCheck,
|
||||||
|
Moon,
|
||||||
|
Sun,
|
||||||
|
User,
|
||||||
|
LogOut,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { Logo } from './Logo'
|
import { Logo } from './Logo'
|
||||||
import { api } from '@/lib/api'
|
import { api } from '@/lib/api'
|
||||||
@@ -76,14 +80,42 @@ function AIConfigBadge({ configured, model }: { configured?: boolean; model?: st
|
|||||||
export function Layout() {
|
export function Layout() {
|
||||||
// ===== 共享 hooks (替代内联 useQuery) =====
|
// ===== 共享 hooks (替代内联 useQuery) =====
|
||||||
const { data: settingsState } = useSettings()
|
const { data: settingsState } = useSettings()
|
||||||
const { data: versionData } = useVersion()
|
|
||||||
const { data: prefs } = usePreferences()
|
const { data: prefs } = usePreferences()
|
||||||
const { data: analysisMenus } = useQuery({
|
const { data: analysisMenus } = useQuery({
|
||||||
queryKey: QK.analysisMenus,
|
queryKey: QK.analysisMenus,
|
||||||
queryFn: api.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 ?? [])
|
const analysisNav = (analysisMenus?.items ?? [])
|
||||||
@@ -161,27 +193,48 @@ export function Layout() {
|
|||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="border-t border-border px-2 py-3 space-y-0.5 shrink-0">
|
<div className="border-t border-border px-2 py-2.5 space-y-1 shrink-0">
|
||||||
|
{/* 登录用户 */}
|
||||||
|
{username && (
|
||||||
|
<div className="flex items-center gap-2 px-3 py-1.5 text-xs text-foreground/70">
|
||||||
|
<User className="h-3.5 w-3.5 shrink-0 text-muted" />
|
||||||
|
<span className="truncate flex-1">{username}</span>
|
||||||
|
<button
|
||||||
|
onClick={handleLogout}
|
||||||
|
className="rounded p-1 text-muted hover:text-danger hover:bg-danger/10 transition-colors"
|
||||||
|
title="退出登录"
|
||||||
|
>
|
||||||
|
<LogOut className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 暗夜模式 + 设置 */}
|
||||||
|
<div className="flex items-center gap-1 px-1">
|
||||||
|
<button
|
||||||
|
onClick={toggleDark}
|
||||||
|
className="flex items-center gap-2 flex-1 rounded-btn px-2 py-1.5 text-xs text-foreground/70 hover:bg-elevated hover:text-foreground transition-colors"
|
||||||
|
title={darkMode ? '切换为日间模式' : '切换为暗夜模式'}
|
||||||
|
>
|
||||||
|
{darkMode ? <Moon className="h-3.5 w-3.5" /> : <Sun className="h-3.5 w-3.5" />}
|
||||||
|
<span>{darkMode ? '暗夜' : '日间'}</span>
|
||||||
|
</button>
|
||||||
<NavLink
|
<NavLink
|
||||||
to="/settings"
|
to="/settings"
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
cn(
|
cn(
|
||||||
'flex items-center justify-between gap-3 px-3 py-2 rounded-btn text-sm transition-colors duration-150 ease-smooth',
|
'flex items-center justify-center rounded-btn p-1.5 text-xs transition-colors',
|
||||||
isActive
|
isActive
|
||||||
? 'bg-elevated text-foreground font-medium'
|
? 'bg-accent/10 text-accent'
|
||||||
: 'text-foreground/80 hover:bg-elevated hover:text-foreground',
|
: 'text-foreground/70 hover:bg-elevated hover:text-foreground',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
title="设置"
|
||||||
>
|
>
|
||||||
<span className="flex items-center gap-3">
|
<Settings className="h-3.5 w-3.5" />
|
||||||
<Settings className="h-4 w-4 shrink-0" />
|
|
||||||
<span>设置</span>
|
|
||||||
</span>
|
|
||||||
<span className="font-mono text-[10px] text-muted/50 select-none">
|
|
||||||
{version ?? ''}
|
|
||||||
</span>
|
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<motion.main
|
<motion.main
|
||||||
|
|||||||
Reference in New Issue
Block a user