返回 AiToEarn
MobileNavList.tsx
根目录 / project / aitoearn-web / src / app / layout / MobileNav / components / MobileNavList.tsx
1 /**
2 * MobileNavList - 移动端导航列表
3 * 路由导航可折叠/展开(持久化),功能项(GitHub/帮助文档)始终显示
4 */
5 import type { MobileNavListProps } from '../types'
6 import type { IRouterDataItem } from '@/app/layout/routerData'
7 import { ChevronDown, ChevronUp, Code2, HelpCircle } from 'lucide-react'
8 import { useShallow } from 'zustand/react/shallow'
9 import { useTransClient } from '@/app/i18n/client'
10 import { DOCS_URL, GITHUB_REPO } from '@/app/layout/shared/constants'
11 import { useGitHubStars } from '@/app/layout/shared/hooks/useGitHubStars'
12 import { useVisibleRouterData } from '@/app/layout/shared/hooks/useVisibleRouterData'
13 import { useSystemStore } from '@/store/system'
14 import { cn } from '@/utils/className'
15 import { MobileMyChannelsButton } from './MobileMyChannelsButton'
16 import { MobileNavItem } from './MobileNavItem'
17
18 function isRouteActive(item: IRouterDataItem, currentRoute: string): boolean {
19 if (item.path === currentRoute) {
20 return true
21 }
22
23 return item.children?.some(child => isRouteActive(child, currentRoute)) ?? false
24 }
25
26 export function MobileNavList({ currentRoute, onClose, onOpenMyChannels }: MobileNavListProps) {
27 const { t } = useTransClient(['route', 'common'])
28 const starCount = useGitHubStars()
29 const visibleRoutes = useVisibleRouterData()
30
31 const { mobileNavExpanded, setMobileNavExpanded } = useSystemStore(
32 useShallow(state => ({
33 mobileNavExpanded: state.mobileNavExpanded,
34 setMobileNavExpanded: state.setMobileNavExpanded,
35 })),
36 )
37
38 const funcItemClassName = cn(
39 'flex items-center gap-3 px-4 py-3 rounded-lg text-base font-medium transition-all w-full',
40 'text-muted-foreground hover:bg-brand-cyan/10 hover:text-brand-cyan',
41 )
42
43 const renderRouteItem = (item: IRouterDataItem, level = 0): React.ReactNode => {
44 const isActive = isRouteActive(item, currentRoute)
45
46 return (
47 <div key={item.path || item.translationKey} className="space-y-1">
48 {item.path ? (
49 <MobileNavItem
50 path={item.path}
51 translationKey={item.translationKey}
52 icon={level === 0 ? item.icon : undefined}
53 isActive={isActive}
54 onClose={onClose}
55 className={cn(level > 0 && 'ml-6 py-2.5 text-sm')}
56 />
57 ) : null}
58 {item.children?.length ? (
59 <div className={cn('space-y-1 border-l border-border/70 pl-2', level === 0 && 'ml-4')}>
60 {item.children.map((child: IRouterDataItem) => renderRouteItem(child, level + 1))}
61 </div>
62 ) : null}
63 </div>
64 )
65 }
66
67 return (
68 <nav className="flex flex-col gap-1 p-4" data-testid="mobile-nav-list">
69 {/* 导航折叠/展开按钮 */}
70 <button
71 onClick={() => setMobileNavExpanded(!mobileNavExpanded)}
72 className={cn(
73 'flex items-center gap-3 px-4 py-3 rounded-lg text-base font-medium transition-all w-full cursor-pointer',
74 'text-muted-foreground hover:bg-brand-cyan/10 hover:text-brand-cyan',
75 mobileNavExpanded && 'bg-brand-cyan/10 text-brand-cyan',
76 )}
77 data-testid="mobile-nav-toggle"
78 >
79 <span className="flex-1 text-left">{t('route:navigation')}</span>
80 {mobileNavExpanded ? (
81 <ChevronUp size={18} className="text-muted-foreground" />
82 ) : (
83 <ChevronDown size={18} className="text-muted-foreground" />
84 )}
85 </button>
86
87 {/* 路由导航项 - 折叠/展开 */}
88 {mobileNavExpanded && (
89 <div className="flex flex-col gap-1" data-testid="mobile-nav-routes">
90 {visibleRoutes.map((item: IRouterDataItem) => renderRouteItem(item))}
91
92 {/* 我的频道 */}
93 <MobileMyChannelsButton onClose={onClose} onOpenMyChannels={onOpenMyChannels} />
94
95 </div>
96 )}
97
98 {/* 功能导航项 - 始终显示 */}
99 <a
100 href={`https://github.com/${GITHUB_REPO}`}
101 target="_blank"
102 rel="noopener noreferrer"
103 className={funcItemClassName}
104 data-testid="mobile-github-link"
105 >
106 <span className="flex items-center justify-center">
107 <Code2 size={20} />
108 </span>
109 <span>GitHub</span>
110 {starCount && (
111 <span className="ml-auto rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground">
112 {starCount}
113 </span>
114 )}
115 </a>
116
117 <a
118 href={DOCS_URL}
119 target="_blank"
120 rel="noopener noreferrer"
121 className={funcItemClassName}
122 data-testid="mobile-docs-link"
123 >
124 <span className="flex items-center justify-center">
125 <HelpCircle size={20} />
126 </span>
127 <span>{t('common:helpDocs')}</span>
128 </a>
129 </nav>
130 )
131 }
132
132 lines Plain Text