返回 AiToEarn
1 /**
2 * ConfigSectionNav - 配置分组目录
3 * 支持当前滚动分组高亮与点击锚点滚动。
4 */
5 'use client'
6
7 import type { ConfigSectionView } from '../../types'
8 import { useEffect, useRef } from 'react'
9 import { useTransClient } from '@/app/i18n/client'
10 import { cn } from '@/utils/className'
11
12 interface ConfigSectionNavProps {
13 sections: ConfigSectionView[]
14 activeSectionId: string
15 disabled?: boolean
16 onSectionClick: (sectionId: string) => void
17 }
18
19 export function ConfigSectionNav({ sections, activeSectionId, disabled, onSectionClick }: ConfigSectionNavProps) {
20 const { t } = useTransClient('configManager')
21 const navRef = useRef<HTMLElement | null>(null)
22
23 useEffect(() => {
24 const nav = navRef.current
25 if (!nav || !activeSectionId)
26 return
27
28 const activeItem = Array.from(nav.querySelectorAll<HTMLElement>('[data-config-section-nav-id]'))
29 .find(item => item.dataset.configSectionNavId === activeSectionId)
30
31 if (!activeItem)
32 return
33
34 nav.scrollTo({
35 top: Math.max(0, activeItem.offsetTop - nav.clientHeight / 2 + activeItem.clientHeight / 2),
36 behavior: 'smooth',
37 })
38 }, [activeSectionId, sections])
39
40 return (
41 <nav ref={navRef} className="flex h-full flex-col gap-1 overflow-y-auto p-3 pb-[45%]" aria-label="Configuration sections">
42 {sections.map((section) => {
43 const active = section.id === activeSectionId
44 return (
45 <button
46 key={section.id}
47 type="button"
48 data-config-section-nav-id={section.id}
49 aria-current={active ? 'true' : undefined}
50 disabled={disabled}
51 onClick={() => onSectionClick(section.id)}
52 className={cn(
53 'group relative flex cursor-pointer flex-col rounded-lg px-3 py-2 text-left transition-colors disabled:cursor-not-allowed disabled:opacity-60',
54 active
55 ? 'bg-brand-cyan/10 text-foreground'
56 : 'text-muted-foreground hover:bg-accent hover:text-foreground',
57 )}
58 >
59 {active && <span className="absolute left-0 top-2 h-[calc(100%-16px)] w-0.5 rounded-full bg-gradient-back" />}
60 <span className="flex items-center justify-between gap-3 text-sm font-medium">
61 <span>{section.label}</span>
62 <span className="flex shrink-0 items-center gap-1">
63 {section.notRecommended && (
64 <span className="rounded-full bg-warning/10 px-1.5 py-0.5 text-[10px] text-warning ring-1 ring-warning/30">
65 {t('status.notRecommended')}
66 </span>
67 )}
68 {section.modifiedFieldCount > 0 && (
69 <span className="rounded-full bg-warning/10 px-1.5 py-0.5 text-[10px] text-warning ring-1 ring-warning/30">
70 {section.modifiedFieldCount}
71 </span>
72 )}
73 <span className="rounded-full bg-background px-1.5 py-0.5 text-[10px] text-muted-foreground ring-1 ring-border">
74 {section.fieldCount}
75 </span>
76 </span>
77 </span>
78 {section.description && (
79 <span className="mt-1 line-clamp-2 text-xs leading-4 text-muted-foreground">
80 {section.description}
81 </span>
82 )}
83 </button>
84 )
85 })}
86 </nav>
87 )
88 }
89
89 lines Plain Text