返回 AiToEarn
1 /**
2 * MobileBottomBar - 移动端底部导航栏
3 * 底部贴边六等分文字导航,激活项使用品牌渐变胶囊强调。
4 */
5 'use client'
6
7 import type { MobileBottomBarProps } from '../../types'
8 import type { IRouterDataItem } from '@/app/layout/routerData'
9 import Link from 'next/link'
10 import { useTransClient } from '@/app/i18n/client'
11 import { visibleRouterData } from '@/app/layout/routerData'
12 import { cn } from '@/utils/className'
13
14 const bottomNavItems = [
15 { routerKey: 'header.draftBox', labelKey: 'header.draftBox', testId: 'content' },
16 { routerKey: 'aiSocial', labelKey: 'aiSocial', testId: 'publish' },
17 { routerKey: 'tasksHistory', labelKey: 'tasksHistory', testId: 'history' },
18 { routerKey: 'accounts', labelKey: 'accounts', testId: 'accounts' },
19 { routerKey: 'header.agentAssets', labelKey: 'header.agentAssets', testId: 'agent-assets' },
20 ] as const
21
22 const navLabelBaseClassName
23 = 'flex h-9 min-w-0 max-w-full items-center justify-center truncate whitespace-nowrap rounded-full px-1.5 text-center text-[12px] font-semibold leading-none tracking-[0.01em] transition-all duration-200'
24
25 function getNavLabelClassName(isActive: boolean) {
26 return cn(
27 navLabelBaseClassName,
28 isActive
29 ? 'h-10 min-w-12 px-3 bg-gradient-back text-gradient-foreground shadow-lg shadow-brand-purple/20'
30 : 'text-muted-foreground hover:text-foreground active:text-foreground',
31 )
32 }
33
34 function getNavItem(key: string) {
35 const itemsToCheck = [...visibleRouterData]
36
37 while (itemsToCheck.length > 0) {
38 const item = itemsToCheck.shift()
39
40 if (!item) {
41 continue
42 }
43
44 if (item.translationKey === key) {
45 return item
46 }
47
48 if (item.children) {
49 itemsToCheck.push(...item.children)
50 }
51 }
52
53 return undefined
54 }
55
56 function isNavItemActive(item: IRouterDataItem | undefined, currentRoute: string) {
57 if (!item?.path) {
58 return false
59 }
60
61 if (item.path === '/') {
62 return currentRoute === '/'
63 }
64
65 return currentRoute === item.path || currentRoute.startsWith(`${item.path}/`)
66 }
67
68 function MobileBottomItem({
69 item,
70 labelKey,
71 testId,
72 currentRoute,
73 }: {
74 item: IRouterDataItem | undefined
75 labelKey: string
76 testId: string
77 currentRoute: string
78 }) {
79 const { t } = useTransClient('route')
80
81 if (!item?.path) {
82 return <div />
83 }
84
85 const isActive = isNavItemActive(item, currentRoute)
86
87 return (
88 <Link
89 href={item.path}
90 className={cn(
91 'flex h-full min-w-0 cursor-pointer items-center justify-center px-0.5',
92 isActive ? 'scale-[1.02]' : 'active:scale-[0.98]',
93 )}
94 aria-current={isActive ? 'page' : undefined}
95 data-testid={`mobile-bottom-nav-${testId}`}
96 >
97 <span className={getNavLabelClassName(isActive)}>{t(labelKey)}</span>
98 </Link>
99 )
100 }
101
102 export function MobileBottomBar({ currentRoute, hidden }: MobileBottomBarProps) {
103 const navItems = bottomNavItems.map(config => ({
104 ...config,
105 item: getNavItem(config.routerKey),
106 }))
107
108 if (hidden) {
109 return null
110 }
111
112 return (
113 <>
114 <nav
115 className="fixed inset-x-0 bottom-0 z-50 border-t border-border/80 bg-background/95 pb-[env(safe-area-inset-bottom)] shadow-lg shadow-primary/10 backdrop-blur-xl md:hidden"
116 data-testid="mobile-bottom-bar"
117 >
118 <div className="grid h-14 grid-cols-5 items-center gap-0.5 px-2 py-2">
119 {navItems.map(({ item, labelKey, routerKey, testId }) => (
120 <MobileBottomItem
121 key={routerKey}
122 item={item}
123 labelKey={labelKey}
124 testId={testId}
125 currentRoute={currentRoute}
126 />
127 ))}
128 </div>
129 </nav>
130 </>
131 )
132 }
133
133 lines Plain Text