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 && (
正在加载菜单...
)}
)
}