diff --git a/public/_locales/zh/messages.json b/public/_locales/zh/messages.json index 48d5eb7..b4ce328 100644 --- a/public/_locales/zh/messages.json +++ b/public/_locales/zh/messages.json @@ -75,6 +75,18 @@ "message": "仪表盘", "description": "Translation key: dashboard_title" }, + "dashboard_searchPlaceholder": { + "message": "搜索工具...", + "description": "Translation key: dashboard_searchPlaceholder" + }, + "dashboard_recentlyUsed": { + "message": "最近使用", + "description": "Translation key: dashboard_recentlyUsed" + }, + "dashboard_allTools": { + "message": "全部工具", + "description": "Translation key: dashboard_allTools" + }, "timestamp_title": { "message": "时间戳", "description": "Translation key: timestamp_title" diff --git a/src/pages/Dashboard/index.tsx b/src/pages/Dashboard/index.tsx index f843fef..29220a1 100644 --- a/src/pages/Dashboard/index.tsx +++ b/src/pages/Dashboard/index.tsx @@ -2,40 +2,115 @@ import { useRouter } from '@/providers/RouterProvider'; import ToolCard from '@/pages/Dashboard/ToolCard'; import { getFeatureByKey } from '@/config/features'; import type { PageType } from '@/types/storage'; -import { useMemo } from 'react'; +import { useMemo, useState } from 'react'; import { useI18n } from '@/utils/chromeI18n'; import { cn } from '@/lib/utils'; +import { Search } from 'lucide-react'; export default function DashboardPage() { - const { navigateTo, visiblePages, pageOrder } = useRouter(); + const { navigateTo, visiblePages, pageOrder, recentlyUsedTools } = useRouter(); const { t } = useI18n(['features']); + const [searchQuery, setSearchQuery] = useState(''); const visibleSet = useMemo(() => new Set(visiblePages), [visiblePages]); + const visibleFeatures = useMemo(() => { + return pageOrder + .filter((key) => visibleSet.has(key)) + .map((key) => ({ key, feature: getFeatureByKey(key) })) + .filter((item) => item.feature?.themeColorKey && item.feature.icon != null); + }, [pageOrder, visibleSet]); + + const filteredFeatures = useMemo(() => { + if (!searchQuery.trim()) return visibleFeatures; + const query = searchQuery.toLowerCase(); + return visibleFeatures.filter( + (item) => + t(item.feature!.labelKey).toLowerCase().includes(query) || + t(item.feature!.descriptionKey).toLowerCase().includes(query), + ); + }, [visibleFeatures, searchQuery, t]); + + const recentFeatures = useMemo(() => { + return recentlyUsedTools + .filter((key) => visibleSet.has(key)) + .map((key) => ({ key, feature: getFeatureByKey(key) })) + .filter((item) => item.feature?.themeColorKey && item.feature.icon != null); + }, [recentlyUsedTools, visibleSet]); + + const showRecent = !searchQuery.trim() && recentFeatures.length > 0; + return ( -
+ {/* 搜索栏 */} +
+ + setSearchQuery(e.target.value)} + placeholder={t('dashboard_searchPlaceholder')} + className={cn( + 'w-full h-9 pl-9 pr-3 rounded-lg border border-border/70 bg-background text-sm', + 'placeholder:text-muted-foreground/50 outline-none', + 'focus:border-primary/50 focus:ring-1 focus:ring-primary/20', + 'transition-colors', + )} + /> +
+ + {/* 最近使用 */} + {showRecent && ( +
+

+ {t('dashboard_recentlyUsed')} +

+
+ {recentFeatures.map(({ key, feature }) => { + const IconComponent = feature!.icon!; + return ( + + ); + })} +
+
)} - > - {pageOrder.map((key) => { - if (!visibleSet.has(key)) return null; - const feature = getFeatureByKey(key); - if (!feature?.themeColorKey || feature.icon == null) return null; - - return ( - navigateTo(key as PageType)} - /> - ); - })} + {/* 全部工具 */} +
+

+ {t('dashboard_allTools')} +

+
+ {filteredFeatures.map(({ key, feature }) => ( + navigateTo(key as PageType)} + /> + ))} +
+
); } diff --git a/src/providers/RouterProvider.tsx b/src/providers/RouterProvider.tsx index badd823..6c90a41 100644 --- a/src/providers/RouterProvider.tsx +++ b/src/providers/RouterProvider.tsx @@ -17,6 +17,8 @@ import { } from '@/config/features'; import { CONTEXT_MENU_DATA_EXPIRY_MS, saveContextMenuData } from '@/utils/useContextMenuData'; +const MAX_RECENTLY_USED = 3; + /** * 校验是否为合法的页面类型 */ @@ -48,6 +50,7 @@ interface RouterContextType { currentPage: PageType; visiblePages: PageType[]; pageOrder: PageType[]; + recentlyUsedTools: PageType[]; isLoaded: boolean; navigateTo: (page: PageType) => void; goBack: () => void; @@ -118,6 +121,10 @@ export function RouterProvider({ return mergeWithDefaults(snapshot, getDefaultPageOrder()); }); + const [recentlyUsedTools, setRecentlyUsedTools] = useState(() => + getSyncSnapshot('app/recentlyUsedTools', [], isValidPageList), + ); + const [isLoaded, setIsLoaded] = useState(() => { const snapshotKey = localStorage.getItem(`snapshot/${syncKey as string}`); const snapshotVisible = localStorage.getItem(`snapshot/${visiblePagesKey as string}`); @@ -136,6 +143,7 @@ export function RouterProvider({ getDefaultVisibleFeatureKeys(), ); const savedPageOrder = await storageUtil.get(pageOrderKey, getDefaultPageOrder()); + const savedRecentTools = await storageUtil.get('app/recentlyUsedTools', []); if (isValidPage(savedRoute) && syncRoute) { setCurrentPage(savedRoute); @@ -146,6 +154,9 @@ export function RouterProvider({ if (isValidPageList(savedPageOrder) && savedPageOrder.length > 0) { setPageOrder(mergeWithDefaults(savedPageOrder, getDefaultPageOrder())); } + if (isValidPageList(savedRecentTools)) { + setRecentlyUsedTools(savedRecentTools); + } setIsLoaded(true); } catch (error) { console.error('[Router Init Error] Core data fetch failed:', error); @@ -234,6 +245,17 @@ export function RouterProvider({ } }, [pageOrder, isLoaded, pageOrderKey]); + useEffect(() => { + if (isLoaded) { + void storageUtil.set('app/recentlyUsedTools', recentlyUsedTools).catch(console.error); + try { + localStorage.setItem('snapshot/app/recentlyUsedTools', JSON.stringify(recentlyUsedTools)); + } catch (err) { + console.error('[Router LocalStorage Error]', err); + } + } + }, [recentlyUsedTools, isLoaded]); + const currentPageRef = useRef(currentPage); useEffect(() => { currentPageRef.current = currentPage; @@ -261,6 +283,12 @@ export function RouterProvider({ setPageOrder(newOrder); } } + if (changes['app/recentlyUsedTools']) { + const newRecent = changes['app/recentlyUsedTools'].newValue; + if (isValidPageList(newRecent)) { + setRecentlyUsedTools(newRecent); + } + } if (changes['contextMenu/pendingData']) { const newData = changes['contextMenu/pendingData'] .newValue as ContextMenuPendingData | null; @@ -282,6 +310,10 @@ export function RouterProvider({ const navigateTo = (page: PageType) => { setCurrentPage(page); + setRecentlyUsedTools((prev) => { + const filtered = prev.filter((p) => p !== page); + return [page, ...filtered].slice(0, MAX_RECENTLY_USED); + }); }; const goBack = () => { @@ -294,6 +326,7 @@ export function RouterProvider({ currentPage, visiblePages, pageOrder, + recentlyUsedTools, isLoaded, navigateTo, goBack, diff --git a/src/types/storage.d.ts b/src/types/storage.d.ts index 1743e9d..64fe134 100644 --- a/src/types/storage.d.ts +++ b/src/types/storage.d.ts @@ -113,6 +113,8 @@ export interface StorageSchema { 'app/language': string; /** 右键菜单待处理数据 */ 'contextMenu/pendingData': ContextMenuPendingData; + /** 最近使用的工具列表(最多保留 3 个) */ + 'app/recentlyUsedTools': PageType[]; } /**