import { useMemo, useState } from 'react' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, type DragEndEvent, } from '@dnd-kit/core' import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' import { Eye, EyeOff, ExternalLink, GripVertical, Settings, Bell } from 'lucide-react' import { Link } from 'react-router-dom' import { api } from '@/lib/api' import { QK } from '@/lib/queryKeys' import { usePreferences } from '@/lib/useSharedQueries' interface NavEntry { id: string label: string type: 'builtin' | 'analysis' visible: boolean } const BUILTIN_PAGES: NavEntry[] = [ { id: '/', label: '看板', type: 'builtin', visible: true }, { id: '/screener', label: '策略', type: 'builtin', visible: true }, { id: '/limit-ladder', label: '连板梯队', type: 'builtin', visible: true }, { id: '/concept-analysis', label: '概念分析', type: 'builtin', visible: true }, { id: '/industry-analysis', label: '行业分析', type: 'builtin', visible: true }, { id: '/stock-analysis', label: '个股分析', type: 'builtin', visible: true }, { id: '/review', label: '复盘', type: 'builtin', visible: true }, { id: '/financials', label: '财务分析', type: 'builtin', visible: true }, { id: '/indices', label: '指数', type: 'builtin', visible: true }, { id: '/monitor', label: '监控中心', type: 'builtin', visible: true }, { id: '/data', label: '数据', type: 'builtin', visible: true }, ] // ── Sortable row ── function SortableItem({ entry, hidden, onToggleHidden, badgeEnabled, onToggleBadge }: { entry: NavEntry hidden: boolean onToggleHidden: (id: string) => void badgeEnabled?: boolean onToggleBadge?: (id: string) => void }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: entry.id }) const style = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.6 : 1, zIndex: isDragging ? 10 : undefined, } return (
{entry.label} {hidden && ( 已隐藏 )} {entry.id}
{entry.type === 'builtin' ? '内置' : '扩展'}
{entry.type === 'builtin' ? ( ) : ( )}
{/* 第 6 列: 徽标开关 (仅监控中心) */}
{onToggleBadge && ( )}
) } // ── Main panel ── export function SettingsMenuSettingsPanel() { const qc = useQueryClient() const { data: prefs } = usePreferences() const menus = useQuery({ queryKey: QK.analysisMenus, queryFn: api.analysisMenus }) const analysisEntries: NavEntry[] = (menus.data?.items ?? []).map(m => ({ id: m.id, label: m.label, type: 'analysis' as const, visible: m.visible, })) const allEntries = useMemo(() => { const saved = prefs?.nav_order ?? [] const entryMap = new Map() for (const e of BUILTIN_PAGES) entryMap.set(e.id, e) for (const e of analysisEntries) entryMap.set(e.id, e) if (saved.length === 0) return [...BUILTIN_PAGES, ...analysisEntries] const ordered: NavEntry[] = [] const seen = new Set() for (const id of saved) { const entry = entryMap.get(id) if (entry) { ordered.push(entry) seen.add(id) } } for (const e of [...BUILTIN_PAGES, ...analysisEntries]) { if (!seen.has(e.id)) ordered.push(e) } return ordered }, [prefs?.nav_order, analysisEntries]) const hiddenSet = useMemo(() => new Set(prefs?.nav_hidden ?? []), [prefs?.nav_hidden]) // Local order state for optimistic drag updates const [localOrder, setLocalOrder] = useState(null) const orderedEntries = useMemo(() => { const order = localOrder ?? prefs?.nav_order ?? [] if (!order.length) return allEntries const byId = new Map(allEntries.map(e => [e.id, e])) const result: NavEntry[] = [] const seen = new Set() for (const id of order) { const e = byId.get(id) if (e) { result.push(e); seen.add(id) } } for (const e of allEntries) { if (!seen.has(e.id)) result.push(e) } return result }, [localOrder, prefs?.nav_order, allEntries]) const saveNavOrder = useMutation({ mutationFn: (order: string[]) => api.saveNavOrder(order), onSuccess: () => { setLocalOrder(null) qc.invalidateQueries({ queryKey: QK.preferences }) }, }) const saveNavHidden = useMutation({ mutationFn: (hidden: string[]) => api.saveNavHidden(hidden), onSuccess: () => qc.invalidateQueries({ queryKey: QK.preferences }), }) const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 5 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }), ) const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event if (!over || active.id === over.id) return const ids = orderedEntries.map(e => e.id) const oldIdx = ids.indexOf(active.id as string) const newIdx = ids.indexOf(over.id as string) const reordered = arrayMove(ids, oldIdx, newIdx) setLocalOrder(reordered) saveNavOrder.mutate(reordered) } const toggleHidden = (id: string) => { const next = new Set(hiddenSet) if (next.has(id)) next.delete(id) else next.add(id) saveNavHidden.mutate([...next]) } // 监控中心徽标开关 (localStorage) const [badgeEnabled, setBadgeEnabled] = useState(() => { try { return localStorage.getItem('monitor_badge_enabled') !== '0' } catch { return true } }) const toggleBadge = (id: string) => { if (id !== '/monitor') return const next = !badgeEnabled setBadgeEnabled(next) try { localStorage.setItem('monitor_badge_enabled', next ? '1' : '0') } catch { /* ignore */ } } return (
菜单设置

调整左侧菜单顺序

拖动左侧手柄调整菜单排列顺序,点击眼睛图标控制菜单在侧边栏中的显示或隐藏。

菜单
类型
显示
设置
数字
e.id)} strategy={verticalListSortingStrategy} > {orderedEntries.map((entry) => ( {menus.isLoading && (
正在加载菜单...
)}
) }