feat: redesign Dashboard layout with search and recently used tools

- Add search input at top to filter tools by name/description
- Add 'recently used' section showing last 3 used tools as compact chips
- Add 'all tools' section header for the tool card grid
- Track tool usage via new 'app/recentlyUsedTools' storage key
- Update navigateTo to record recently used tools (max 3, LRU order)
- Add i18n keys: dashboard_searchPlaceholder, dashboard_recentlyUsed, dashboard_allTools

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
雨霖铃
2026-05-28 21:22:30 +08:00
parent 067a20fb0a
commit c6cadda95b
4 changed files with 145 additions and 23 deletions
+12
View File
@@ -75,6 +75,18 @@
"message": "仪表盘", "message": "仪表盘",
"description": "Translation key: dashboard_title" "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": { "timestamp_title": {
"message": "时间戳", "message": "时间戳",
"description": "Translation key: timestamp_title" "description": "Translation key: timestamp_title"
+95 -20
View File
@@ -2,40 +2,115 @@ import { useRouter } from '@/providers/RouterProvider';
import ToolCard from '@/pages/Dashboard/ToolCard'; import ToolCard from '@/pages/Dashboard/ToolCard';
import { getFeatureByKey } from '@/config/features'; import { getFeatureByKey } from '@/config/features';
import type { PageType } from '@/types/storage'; import type { PageType } from '@/types/storage';
import { useMemo } from 'react'; import { useMemo, useState } from 'react';
import { useI18n } from '@/utils/chromeI18n'; import { useI18n } from '@/utils/chromeI18n';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { Search } from 'lucide-react';
export default function DashboardPage() { export default function DashboardPage() {
const { navigateTo, visiblePages, pageOrder } = useRouter(); const { navigateTo, visiblePages, pageOrder, recentlyUsedTools } = useRouter();
const { t } = useI18n(['features']); const { t } = useI18n(['features']);
const [searchQuery, setSearchQuery] = useState('');
const visibleSet = useMemo(() => new Set<string>(visiblePages), [visiblePages]); const visibleSet = useMemo(() => new Set<string>(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 ( return (
<div <div className={cn('flex flex-col gap-4 p-3.5 w-full h-auto select-none')}>
{/* 搜索栏 */}
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground/60" />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder={t('dashboard_searchPlaceholder')}
className={cn( className={cn(
'grid grid-cols-1 sm:grid-cols-[repeat(auto-fill,minmax(290px,1fr))] auto-rows-auto gap-3.5 p-3.5 w-full h-auto', 'w-full h-9 pl-9 pr-3 rounded-lg border border-border/70 bg-background text-sm',
'select-none', 'placeholder:text-muted-foreground/50 outline-none',
'focus:border-primary/50 focus:ring-1 focus:ring-primary/20',
'transition-colors',
)}
/>
</div>
{/* 最近使用 */}
{showRecent && (
<div className="flex flex-col gap-2">
<h3 className="text-xs font-semibold text-muted-foreground/80 uppercase tracking-wider">
{t('dashboard_recentlyUsed')}
</h3>
<div className="flex flex-wrap gap-2">
{recentFeatures.map(({ key, feature }) => {
const IconComponent = feature!.icon!;
return (
<button
key={key}
type="button"
onClick={() => navigateTo(key as PageType)}
className={cn(
'inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium',
'border border-border/60 bg-card text-card-foreground',
'hover:bg-muted/40 hover:border-primary/30',
'transition-colors cursor-pointer',
)} )}
> >
{pageOrder.map((key) => { <IconComponent className="h-3.5 w-3.5 text-muted-foreground/70" />
if (!visibleSet.has(key)) return null; {t(feature!.labelKey)}
</button>
const feature = getFeatureByKey(key);
if (!feature?.themeColorKey || feature.icon == null) return null;
return (
<ToolCard
key={key}
title={t(feature.labelKey)}
description={t(feature.descriptionKey)}
colorKey={feature.themeColorKey}
icon={feature.icon}
onNavigate={() => navigateTo(key as PageType)}
/>
); );
})} })}
</div> </div>
</div>
)}
{/* 全部工具 */}
<div className="flex flex-col gap-2">
<h3 className="text-xs font-semibold text-muted-foreground/80 uppercase tracking-wider">
{t('dashboard_allTools')}
</h3>
<div
className={cn(
'grid grid-cols-1 sm:grid-cols-[repeat(auto-fill,minmax(290px,1fr))] auto-rows-auto gap-3.5',
)}
>
{filteredFeatures.map(({ key, feature }) => (
<ToolCard
key={key}
title={t(feature!.labelKey)}
description={t(feature!.descriptionKey)}
colorKey={feature!.themeColorKey!}
icon={feature!.icon!}
onNavigate={() => navigateTo(key as PageType)}
/>
))}
</div>
</div>
</div>
); );
} }
+33
View File
@@ -17,6 +17,8 @@ import {
} from '@/config/features'; } from '@/config/features';
import { CONTEXT_MENU_DATA_EXPIRY_MS, saveContextMenuData } from '@/utils/useContextMenuData'; import { CONTEXT_MENU_DATA_EXPIRY_MS, saveContextMenuData } from '@/utils/useContextMenuData';
const MAX_RECENTLY_USED = 3;
/** /**
* 校验是否为合法的页面类型 * 校验是否为合法的页面类型
*/ */
@@ -48,6 +50,7 @@ interface RouterContextType {
currentPage: PageType; currentPage: PageType;
visiblePages: PageType[]; visiblePages: PageType[];
pageOrder: PageType[]; pageOrder: PageType[];
recentlyUsedTools: PageType[];
isLoaded: boolean; isLoaded: boolean;
navigateTo: (page: PageType) => void; navigateTo: (page: PageType) => void;
goBack: () => void; goBack: () => void;
@@ -118,6 +121,10 @@ export function RouterProvider({
return mergeWithDefaults(snapshot, getDefaultPageOrder()); return mergeWithDefaults(snapshot, getDefaultPageOrder());
}); });
const [recentlyUsedTools, setRecentlyUsedTools] = useState<PageType[]>(() =>
getSyncSnapshot('app/recentlyUsedTools', [], isValidPageList),
);
const [isLoaded, setIsLoaded] = useState(() => { const [isLoaded, setIsLoaded] = useState(() => {
const snapshotKey = localStorage.getItem(`snapshot/${syncKey as string}`); const snapshotKey = localStorage.getItem(`snapshot/${syncKey as string}`);
const snapshotVisible = localStorage.getItem(`snapshot/${visiblePagesKey as string}`); const snapshotVisible = localStorage.getItem(`snapshot/${visiblePagesKey as string}`);
@@ -136,6 +143,7 @@ export function RouterProvider({
getDefaultVisibleFeatureKeys(), getDefaultVisibleFeatureKeys(),
); );
const savedPageOrder = await storageUtil.get(pageOrderKey, getDefaultPageOrder()); const savedPageOrder = await storageUtil.get(pageOrderKey, getDefaultPageOrder());
const savedRecentTools = await storageUtil.get('app/recentlyUsedTools', []);
if (isValidPage(savedRoute) && syncRoute) { if (isValidPage(savedRoute) && syncRoute) {
setCurrentPage(savedRoute); setCurrentPage(savedRoute);
@@ -146,6 +154,9 @@ export function RouterProvider({
if (isValidPageList(savedPageOrder) && savedPageOrder.length > 0) { if (isValidPageList(savedPageOrder) && savedPageOrder.length > 0) {
setPageOrder(mergeWithDefaults(savedPageOrder, getDefaultPageOrder())); setPageOrder(mergeWithDefaults(savedPageOrder, getDefaultPageOrder()));
} }
if (isValidPageList(savedRecentTools)) {
setRecentlyUsedTools(savedRecentTools);
}
setIsLoaded(true); setIsLoaded(true);
} catch (error) { } catch (error) {
console.error('[Router Init Error] Core data fetch failed:', error); console.error('[Router Init Error] Core data fetch failed:', error);
@@ -234,6 +245,17 @@ export function RouterProvider({
} }
}, [pageOrder, isLoaded, pageOrderKey]); }, [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); const currentPageRef = useRef(currentPage);
useEffect(() => { useEffect(() => {
currentPageRef.current = currentPage; currentPageRef.current = currentPage;
@@ -261,6 +283,12 @@ export function RouterProvider({
setPageOrder(newOrder); setPageOrder(newOrder);
} }
} }
if (changes['app/recentlyUsedTools']) {
const newRecent = changes['app/recentlyUsedTools'].newValue;
if (isValidPageList(newRecent)) {
setRecentlyUsedTools(newRecent);
}
}
if (changes['contextMenu/pendingData']) { if (changes['contextMenu/pendingData']) {
const newData = changes['contextMenu/pendingData'] const newData = changes['contextMenu/pendingData']
.newValue as ContextMenuPendingData | null; .newValue as ContextMenuPendingData | null;
@@ -282,6 +310,10 @@ export function RouterProvider({
const navigateTo = (page: PageType) => { const navigateTo = (page: PageType) => {
setCurrentPage(page); setCurrentPage(page);
setRecentlyUsedTools((prev) => {
const filtered = prev.filter((p) => p !== page);
return [page, ...filtered].slice(0, MAX_RECENTLY_USED);
});
}; };
const goBack = () => { const goBack = () => {
@@ -294,6 +326,7 @@ export function RouterProvider({
currentPage, currentPage,
visiblePages, visiblePages,
pageOrder, pageOrder,
recentlyUsedTools,
isLoaded, isLoaded,
navigateTo, navigateTo,
goBack, goBack,
+2
View File
@@ -113,6 +113,8 @@ export interface StorageSchema {
'app/language': string; 'app/language': string;
/** 右键菜单待处理数据 */ /** 右键菜单待处理数据 */
'contextMenu/pendingData': ContextMenuPendingData; 'contextMenu/pendingData': ContextMenuPendingData;
/** 最近使用的工具列表(最多保留 3 个) */
'app/recentlyUsedTools': PageType[];
} }
/** /**