forked from ZhuLinsen/daily_stock_analysis
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathSidebarNav.tsx
More file actions
134 lines (125 loc) · 5.35 KB
/
Copy pathSidebarNav.tsx
File metadata and controls
134 lines (125 loc) · 5.35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
import React, { useState } from 'react';
import { motion } from 'motion/react';
import { BarChart3, BriefcaseBusiness, Home, LogOut, MessageSquareQuote, ScanSearch, Settings2 } from 'lucide-react';
import { NavLink } from 'react-router-dom';
import { useAuth } from '../../contexts/AuthContext';
import { useAgentChatStore } from '../../stores/agentChatStore';
import { cn } from '../../utils/cn';
import { ConfirmDialog } from '../common/ConfirmDialog';
import { StatusDot } from '../common/StatusDot';
import { ThemeToggle } from '../theme/ThemeToggle';
type SidebarNavProps = {
collapsed?: boolean;
onNavigate?: () => void;
};
type NavItem = {
key: string;
label: string;
to: string;
icon: React.ComponentType<{ className?: string }>;
exact?: boolean;
badge?: 'completion';
};
const NAV_ITEMS: NavItem[] = [
{ key: 'home', label: '首页', to: '/', icon: Home, exact: true },
{ key: 'screen', label: 'AI选股', to: '/screen', icon: ScanSearch },
{ key: 'chat', label: '问股', to: '/chat', icon: MessageSquareQuote, badge: 'completion' },
{ key: 'portfolio', label: '持仓', to: '/portfolio', icon: BriefcaseBusiness },
{ key: 'backtest', label: '回测', to: '/backtest', icon: BarChart3 },
{ key: 'settings', label: '设置', to: '/settings', icon: Settings2 },
];
export const SidebarNav: React.FC<SidebarNavProps> = ({ collapsed = false, onNavigate }) => {
const { authEnabled, logout } = useAuth();
const completionBadge = useAgentChatStore((state) => state.completionBadge);
const [showLogoutConfirm, setShowLogoutConfirm] = useState(false);
return (
<div className="flex h-full flex-col">
<div className={cn('mb-4 flex items-center gap-2 px-1', collapsed ? 'justify-center' : '')}>
<div className="flex h-10 w-10 items-center justify-center rounded-2xl bg-primary-gradient text-[hsl(var(--primary-foreground))] shadow-[0_12px_28px_var(--nav-brand-shadow)]">
<BarChart3 className="h-5 w-5" />
</div>
{!collapsed ? (
<p className="min-w-0 truncate text-sm font-semibold text-foreground">DSA</p>
) : null}
</div>
<nav className="flex flex-1 flex-col gap-1.5" aria-label="主导航">
{NAV_ITEMS.map(({ key, label, to, icon: Icon, exact, badge }) => (
<NavLink
key={key}
to={to}
end={exact}
onClick={onNavigate}
aria-label={label}
className={({ isActive }) =>
cn(
'group relative flex items-center gap-3 border-y border-x-0 text-sm transition-all',
'h-[var(--nav-item-height)]',
collapsed ? 'justify-center px-0' : 'px-[var(--nav-item-padding-x)]',
isActive
? 'border-[var(--nav-active-border)] bg-[var(--nav-active-bg)] text-[hsl(var(--primary))] font-medium'
: 'border-transparent text-secondary-text hover:bg-[var(--nav-hover-bg)] hover:text-foreground'
)
}
>
{({ isActive }) => (
<>
{isActive && (
<motion.div
layoutId="activeIndicator"
className="absolute top-0 bottom-0 left-0 w-[var(--nav-indicator-width)] bg-[var(--nav-indicator-bg)] shadow-[0_0_10px_var(--nav-indicator-shadow)]"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.2 }}
/>
)}
<Icon className={cn('ml-1 h-5 w-5 shrink-0', isActive ? 'text-[var(--nav-icon-active)]' : 'text-current')} />
{!collapsed ? <span className="truncate">{label}</span> : null}
{badge === 'completion' && completionBadge ? (
<StatusDot
tone="info"
data-testid="chat-completion-badge"
className={cn(
'absolute right-3 border-2 border-background shadow-[0_0_10px_var(--nav-indicator-shadow)]',
collapsed ? 'right-2 top-2' : ''
)}
aria-label="问股有新消息"
/>
) : null}
</>
)}
</NavLink>
))}
</nav>
<div className="mt-4 mb-2">
<ThemeToggle variant="nav" collapsed={collapsed} />
</div>
{authEnabled ? (
<button
type="button"
onClick={() => setShowLogoutConfirm(true)}
className={cn(
'mt-5 flex h-11 w-full cursor-pointer select-none items-center gap-3 rounded-2xl border border-transparent px-3 text-sm text-secondary-text transition-all hover:border-border/70 hover:bg-hover hover:text-foreground',
collapsed ? 'justify-center px-2' : ''
)}
>
<LogOut className="h-5 w-5 shrink-0" />
{!collapsed ? <span>退出</span> : null}
</button>
) : null}
<ConfirmDialog
isOpen={showLogoutConfirm}
title="退出登录"
message="确认退出当前登录状态吗?退出后需要重新输入密码。"
confirmText="确认退出"
cancelText="取消"
isDanger
onConfirm={() => {
setShowLogoutConfirm(false);
onNavigate?.();
void logout();
}}
onCancel={() => setShowLogoutConfirm(false)}
/>
</div>
);
};