| 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 |