import { Link, useLocation } from 'react-router-dom'; import { Icon } from './Icon'; interface NavRailItem { to: string; icon: string; label: string; badge?: number; } const TOP_ITEMS: NavRailItem[] = [ { to: '/messages', icon: 'chat_bubble', label: '消息' }, { to: '/contacts', icon: 'contacts', label: '通讯录' }, { to: '/favorites', icon: 'inventory_2', label: '收藏' }, { to: '/files', icon: 'folder', label: '文件助手' }, ]; const BOTTOM_ITEMS: NavRailItem[] = [ { to: '/calls', icon: 'phone', label: '通话记录' }, { to: '/settings', icon: 'settings', label: '设置' }, ]; /** `/files` is `` (App.tsx) -- the real URL right after * clicking "文件助手" is `/messages/slf`, which a plain (non-`end`) NavLink match makes "消息" * light up for (a same-topicId prefix match) while "文件助手" itself (`to: '/files'`, a path that * no longer exists once redirected) never matches at all. Overriding both items' active state off * the real pathname instead of trusting NavLink's own prefix match fixes both sides of the mix-up. */ function isNavItemActive(to: string, pathname: string): boolean { if (to === '/files') return pathname === '/messages/slf'; if (to === '/messages') return pathname.startsWith('/messages') && pathname !== '/messages/slf'; return pathname === to || pathname.startsWith(`${to}/`); } function NavRailButton({ item }: { item: NavRailItem }) { const location = useLocation(); const isActive = isNavItemActive(item.to, location.pathname); return ( // Plain Link, not NavLink -- NavLink computes its OWN `aria-current` from ITS OWN prefix-match // logic unconditionally (confirmed by reading react-router's source: passing an `aria-current` // prop only changes the STRING used, not whether it's applied at all, which stays gated on // NavLink's internal isActive). That's exactly `/files`'s own problem above -- NavLink's naive // match would light up "消息" for `/messages/slf`, not "文件助手" -- so a second, independent // aria-current computation fighting the same override this file already needed for `className` // is a real, verifiable a11y bug (screen readers and sighted highlighting would disagree on // which icon is current). Driving both off the one already-correct `isActive` avoids that // entirely. {item.badge ? ( {item.badge} ) : null} ); } /** Persistent 64px icon rail, visible at >=640px (see App.tsx for the <640 bottom-tab fallback). */ export function NavRail({ selfInitial = '我', unreadMessages = 0 }: { selfInitial?: string; unreadMessages?: number }) { return ( ); }