import { SyntheticEvent, useEffect, useMemo, useState } from 'react'; import { Settings, RefreshCw, GripVertical } from 'lucide-react'; import { DndContext, closestCenter, PointerSensor, KeyboardSensor, 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 type { PageType, StorageSchema } from '@/types/storage'; import { storageUtil } from '@/utils/chromeStorage'; import { getAllFeatureKeys, getDefaultPageOrder, getDefaultVisibleFeatureKeys, getFeatureByKey, } from '@/config/features'; import GlobalSnackbar, { useSnackbarState } from '@/components/GlobalSnackbar'; import PageErrorBoundary from '@/components/PageErrorBoundary'; import PageHeader from '@/components/PageHeader'; import { useTranslation } from 'react-i18next'; import type { PaletteColorKey } from '@/config/features'; const PALETTE_COLORS: Record = { primary: '#1976d2', success: '#2e7d32', warning: '#e65100', error: '#c62828', secondary: '#9c27b0', info: '#0288d1', }; const getColorCode = (key: PaletteColorKey): string => PALETTE_COLORS[key]; const isValidPage = (page: unknown): page is PageType => { return typeof page === 'string' && (getAllFeatureKeys() as string[]).includes(page); }; const isValidPageList = (pages: unknown): pages is PageType[] => { return Array.isArray(pages) && pages.every(isValidPage); }; type WindowType = 'popup' | 'sidepanel' | 'tab'; interface SortableFeatureRowProps { pageKey: PageType; isLast: boolean; isChecked: boolean; isDisabled: boolean; onToggle: (key: PageType) => void; } function SortableFeatureRow({ pageKey, isLast, isChecked, isDisabled, onToggle, }: SortableFeatureRowProps) { const { t } = useTranslation(['features']); const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: pageKey, }); const feature = getFeatureByKey(pageKey); if (!feature) return null; const colorKey = feature.themeColorKey ?? 'primary'; const colorCode = getColorCode(colorKey); const style = { transform: CSS.Transform.toString(transform), transition, zIndex: isDragging ? 1 : 'auto', position: 'relative' as const, backgroundColor: isDragging ? `${colorCode}0a` : 'transparent', boxShadow: isDragging ? '0 8px 20px rgba(0,0,0,0.08)' : 'none', }; return (
{/* 拖拽手柄 */}
{/* 功能图标 */}
{feature.icon && }
{/* 文本信息 */}
{t(feature.labelKey)}
{feature.descriptionKey && (
{t(feature.descriptionKey)}
)}
{/* 开关 */}
); } export default function App() { const { t } = useTranslation(['features', 'common']); const initialWindowType = useMemo(() => { if (typeof window === 'undefined') return 'popup'; const params = new URLSearchParams(window.location.search); const tab = params.get('tab'); if (tab === 'popup' || tab === 'sidepanel' || tab === 'tab') { return tab as WindowType; } return 'popup'; }, []); const [windowType, setWindowType] = useState(initialWindowType); const [visiblePages, setVisiblePages] = useState([]); const [pageOrder, setPageOrder] = useState([]); const [isLoaded, setIsLoaded] = useState(false); const { snackbarProps, showMessage } = useSnackbarState(); const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 4 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }), ); const configKeys = useMemo(() => { switch (windowType) { case 'sidepanel': return { visible: 'app/sidepanelVisiblePages' as keyof StorageSchema, order: 'app/sidepanelPageOrder' as keyof StorageSchema, }; case 'tab': return { visible: 'app/tabVisiblePages' as keyof StorageSchema, order: 'app/tabPageOrder' as keyof StorageSchema, }; case 'popup': default: return { visible: 'app/popupVisiblePages' as keyof StorageSchema, order: 'app/popupPageOrder' as keyof StorageSchema, }; } }, [windowType]); useEffect(() => { const url = new URL(window.location.href); url.searchParams.set('tab', windowType); window.history.replaceState({}, '', url.toString()); }, [windowType]); useEffect(() => { const loadConfig = async () => { setIsLoaded(false); try { const [savedVisible, savedOrder] = await Promise.all([ storageUtil.get(configKeys.visible, getDefaultVisibleFeatureKeys()), storageUtil.get(configKeys.order, getDefaultPageOrder()), ]); setVisiblePages( isValidPageList(savedVisible) ? savedVisible : getDefaultVisibleFeatureKeys(), ); setPageOrder(isValidPageList(savedOrder) ? savedOrder : getDefaultPageOrder()); } catch (error) { console.error('Failed to load config:', error); setVisiblePages(getDefaultVisibleFeatureKeys()); setPageOrder(getDefaultPageOrder()); } finally { setIsLoaded(true); } }; loadConfig().catch(console.error); }, [configKeys]); const showToast = (message: string, severity: 'success' | 'info' | 'warning') => { showMessage(message, { severity }); }; const handlePageToggle = async (page: PageType) => { const isCurrentlyVisible = visiblePages.includes(page); let newPages: PageType[]; if (isCurrentlyVisible) { if (visiblePages.length <= 1) { showToast('至少需要保留一个可见页面', 'warning'); return; } newPages = visiblePages.filter((p) => p !== page); } else { newPages = [...visiblePages, page]; } try { await storageUtil.set(configKeys.visible, newPages); setVisiblePages(newPages); const feature = getFeatureByKey(page); const label = feature ? t(feature.labelKey) : page; showToast(`已${isCurrentlyVisible ? '隐藏' : '显示'} ${label}`, 'success'); } catch (error) { console.error('Failed to save config:', error); showToast('保存失败', 'warning'); } }; const handleDragEnd = async (event: DragEndEvent) => { const { active, over } = event; if (!over || active.id === over.id) return; const oldIndex = pageOrder.indexOf(active.id as PageType); const newIndex = pageOrder.indexOf(over.id as PageType); if (oldIndex < 0 || newIndex < 0) return; const newOrder = arrayMove(pageOrder, oldIndex, newIndex); setPageOrder(newOrder); try { await storageUtil.set(configKeys.order, newOrder); } catch (error) { console.error('Failed to save order:', error); showToast('排序保存失败', 'warning'); } }; const handleRestoreDefaults = async () => { try { const defaults = getDefaultVisibleFeatureKeys(); const defaultOrder = getDefaultPageOrder(); await Promise.all([ storageUtil.set(configKeys.visible, defaults), storageUtil.set(configKeys.order, defaultOrder), ]); setVisiblePages(defaults); setPageOrder(defaultOrder); showToast('已恢复当前模式默认设置', 'success'); } catch (error) { console.error('Failed to restore defaults:', error); showToast('恢复失败', 'warning'); } }; const handleWindowTypeChange = (_event: SyntheticEvent, newType: WindowType) => { if (newType !== null) { setWindowType(newType); } }; if (!isLoaded) { return (
); } return (
{/* 顶部标题与导航栏 */}
} iconColor="#1976d2" title="应用设置" subtitle="针对不同窗口类型独立配置 Dashboard 中显示的功能及其排序" sx={{ marginBottom: '1rem' }} /> {/* Tab 与恢复按钮同行 */}
{(['popup', 'sidepanel', 'tab'] as WindowType[]).map((type) => ( ))}
{/* 主内容区域 */}
{pageOrder.map((key, index, array) => { const isChecked = visiblePages.includes(key); const isDisabled = isChecked && visiblePages.length === 1; return ( ); })}
); }