feat: optimize dashboard/search UX and simplify extension architecture

Redesign Dashboard with compact tool grid and recently used tools
Improve TopBar search UX with Cmd/Ctrl+K shortcut and better history navigation
Reorganize project structure into src/
Migrate i18n from react-i18next to chrome.i18n
Remove runtime language switch and settings page
Remove HTML/Markdown conversion tools
Clean up unused code, dead animations, redundant comments, and imports
Improve component consistency with shadcn/ui patterns
Replace hardcoded strings/colors with i18n tokens and theme tokens
Add comprehensive project documentation and coding standards
Fix CI artifact upload workflow and multiple TypeScript/test issues

Includes various refactors, UI polish, i18n cleanup, CI improvements,
and maintenance updates across the codebase.
This commit is contained in:
LingandRX
2026-05-28 22:39:25 +08:00
committed by GitHub
parent d4c29a1bf4
commit 945780def8
200 changed files with 1557 additions and 4380 deletions
+44
View File
@@ -0,0 +1,44 @@
# config/
应用级配置目录,存放功能特性的注册中心。
## 文件说明
| 文件 | 用途 |
| -------------- | ---------------------------------------- |
| `features.tsx` | 核心配置文件,定义所有工具功能的注册信息 |
## features.tsx
`FEATURES` 数组是路由和功能元数据的**单一事实来源**,每个功能定义包含:
- `key`:页面类型标识(`PageType`
- `labelKey` / `descriptionKey`i18n 翻译键
- `themeColorKey`:主题色(`primary/success/warning/error/secondary/info`
- `icon`lucide-react 图标组件
- `defaultVisible`:默认是否可见
- `components`:三种渲染模式的懒加载组件(`popup``sidepanel``tab`
## 已注册功能(11 个)
| key | 图标 | 说明 |
| -------------------- | ----------------- | ----------------- |
| `dashboard` | — | 仪表盘首页 |
| `timestamp` | Clock | 时间戳转换工具 |
| `storageCleaner` | Database | 存储清理工具 |
| `qrCode` | QrCode | 二维码工具 |
| `textStatistics` | FileText | 文本统计工具 |
| `jwt` | Key | JWT 解析工具 |
| `jsonDiff` | GitCompareArrows | JSON 差异比较工具 |
| `base64Converter` | ArrowLeftRight | Base64 转换器 |
| `markdownToHtml` | Code | Markdown 转 HTML |
| `htmlToMarkdown` | File | HTML 转 Markdown |
| `rightClickRestorer` | MousePointerClick | 右键菜单恢复工具 |
## 导出函数
- `getFeatureByKey(key)` — 根据 key 获取功能配置
- `getDefaultVisibleFeatureKeys()` — 获取默认可见的功能 key 列表
- `getAllFeatureKeys()` — 获取所有功能 key 列表
- `getDefaultPageOrder()` — 获取默认页面排序(不含 dashboard)
- `getEntryPointType()` — 判断当前入口类型(popup/sidepanel/tab
+129
View File
@@ -0,0 +1,129 @@
import { describe, expect, it } from 'vitest';
import {
FEATURES,
getAllFeatureKeys,
getDefaultPageOrder,
getDefaultVisibleFeatureKeys,
getFeatureByKey,
} from '@/config/features';
describe('features', () => {
describe('FEATURES', () => {
it('should have 9 features defined', () => {
expect(FEATURES).toHaveLength(9);
});
it('should have all required properties for each feature', () => {
FEATURES.forEach((feature) => {
expect(feature).toHaveProperty('key');
expect(feature).toHaveProperty('labelKey');
expect(feature).toHaveProperty('descriptionKey');
expect(feature).toHaveProperty('defaultVisible');
expect(feature).toHaveProperty('components');
expect(typeof feature.key).toBe('string');
expect(typeof feature.labelKey).toBe('string');
expect(typeof feature.descriptionKey).toBe('string');
expect(typeof feature.defaultVisible).toBe('boolean');
expect(typeof feature.components).toBe('object');
expect(feature.components).toHaveProperty('popup');
expect(feature.components).toHaveProperty('sidepanel');
expect(feature.components).toHaveProperty('tab');
// Optional UI properties for non-hidden features
if (feature.key !== 'dashboard') {
expect(feature).toHaveProperty('icon');
expect(feature).toHaveProperty('themeColorKey');
expect(typeof feature.themeColorKey).toBe('string');
}
});
});
it('should have unique keys for each feature', () => {
const keys = FEATURES.map((f) => f.key);
const uniqueKeys = new Set(keys);
expect(uniqueKeys.size).toBe(keys.length);
});
});
describe('getFeatureByKey', () => {
it('should return dashboard feature', () => {
const feature = getFeatureByKey('dashboard');
expect(feature).toBeDefined();
expect(feature?.key).toBe('dashboard');
expect(feature?.labelKey).toBe('dashboard_title');
});
it('should return timestamp feature', () => {
const feature = getFeatureByKey('timestamp');
expect(feature).toBeDefined();
expect(feature?.key).toBe('timestamp');
expect(feature?.labelKey).toBe('timestamp_title');
expect(feature?.themeColorKey).toBeDefined();
});
it('should return storageCleaner feature', () => {
const feature = getFeatureByKey('storageCleaner');
expect(feature).toBeDefined();
expect(feature?.key).toBe('storageCleaner');
expect(feature?.labelKey).toBe('storageCleaner_title');
});
it('should return undefined for invalid key', () => {
const feature = getFeatureByKey('invalid' as any);
expect(feature).toBeUndefined();
});
});
describe('getDefaultVisibleFeatureKeys', () => {
it('should return only visible features', () => {
const visibleKeys = getDefaultVisibleFeatureKeys();
visibleKeys.forEach((key) => {
const feature = getFeatureByKey(key);
expect(feature?.defaultVisible).toBe(true);
});
});
it('should include dashboard, timestamp, storageCleaner, qrCode', () => {
const visibleKeys = getDefaultVisibleFeatureKeys();
expect(visibleKeys).toContain('dashboard');
expect(visibleKeys).toContain('timestamp');
expect(visibleKeys).toContain('storageCleaner');
expect(visibleKeys).toContain('qrCode');
});
});
describe('getAllFeatureKeys', () => {
it('should return all feature keys', () => {
const allKeys = getAllFeatureKeys();
expect(allKeys).toHaveLength(9);
expect(allKeys).toContain('dashboard');
expect(allKeys).toContain('timestamp');
expect(allKeys).toContain('storageCleaner');
expect(allKeys).toContain('qrCode');
expect(allKeys).toContain('textStatistics');
expect(allKeys).toContain('jwt');
expect(allKeys).toContain('jsonDiff');
expect(allKeys).toContain('base64Converter');
expect(allKeys).toContain('rightClickRestorer');
});
});
describe('getDefaultPageOrder', () => {
it('should exclude dashboard from page order', () => {
const pageOrder = getDefaultPageOrder();
expect(pageOrder).not.toContain('dashboard');
});
it('should include timestamp, storageCleaner, qrCode in page order', () => {
const pageOrder = getDefaultPageOrder();
expect(pageOrder).toContain('timestamp');
expect(pageOrder).toContain('storageCleaner');
expect(pageOrder).toContain('qrCode');
});
it('should have 8 items in page order', () => {
const pageOrder = getDefaultPageOrder();
expect(pageOrder).toHaveLength(8);
});
});
});
+185
View File
@@ -0,0 +1,185 @@
import { type ComponentType, lazy } from 'react';
import type { LucideProps } from 'lucide-react';
import type { PageType } from '@/types/storage';
import {
Clock,
Database,
QrCode,
FileText,
Key,
GitCompareArrows,
ArrowLeftRight,
MousePointerClick,
} from 'lucide-react';
export type PaletteColorKey = 'primary' | 'success' | 'warning' | 'error' | 'secondary' | 'info';
// 懒加载页面组件
const DashboardPage = lazy(() => import('@/pages/Dashboard'));
const TimestampPage = lazy(() => import('@/pages/Timestamp'));
const StorageCleanerPage = lazy(() => import('@/pages/StorageCleaner'));
const QrCodePage = lazy(() => import('@/pages/QrCode'));
const TextStatisticsPage = lazy(() => import('@/pages/TextStatistics'));
const JwtPage = lazy(() => import('@/pages/Jwt'));
const JsonToolsPage = lazy(() => import('@/pages/JsonTools'));
const Base64ConverterPage = lazy(() => import('@/pages/Base64Converter'));
const RightClickRestorerPage = lazy(() => import('@/pages/RightClickRestorer'));
export interface FeatureConfig {
key: PageType;
labelKey: string;
descriptionKey: string;
themeColorKey?: PaletteColorKey;
icon?: ComponentType<LucideProps>;
defaultVisible: boolean;
components: {
popup: ComponentType;
sidepanel: ComponentType;
tab: ComponentType;
};
}
export const FEATURES: FeatureConfig[] = [
{
key: 'dashboard',
labelKey: 'dashboard_title',
descriptionKey: '',
defaultVisible: true,
components: {
popup: DashboardPage,
sidepanel: DashboardPage,
tab: DashboardPage,
},
},
{
key: 'timestamp',
labelKey: 'timestamp_title',
descriptionKey: 'timestamp_description',
themeColorKey: 'primary',
icon: Clock,
defaultVisible: true,
components: {
popup: TimestampPage,
sidepanel: TimestampPage,
tab: TimestampPage,
},
},
{
key: 'storageCleaner',
labelKey: 'storageCleaner_title',
descriptionKey: 'storageCleaner_description',
themeColorKey: 'warning',
icon: Database,
defaultVisible: true,
components: {
popup: StorageCleanerPage,
sidepanel: StorageCleanerPage,
tab: StorageCleanerPage,
},
},
{
key: 'qrCode',
labelKey: 'qrCode_title',
descriptionKey: 'qrCode_description',
themeColorKey: 'success',
icon: QrCode,
defaultVisible: true,
components: {
popup: QrCodePage,
sidepanel: QrCodePage,
tab: QrCodePage,
},
},
{
key: 'textStatistics',
labelKey: 'textStatistics_title',
descriptionKey: 'textStatistics_description',
themeColorKey: 'secondary',
icon: FileText,
defaultVisible: true,
components: {
popup: TextStatisticsPage,
sidepanel: TextStatisticsPage,
tab: TextStatisticsPage,
},
},
{
key: 'jwt',
labelKey: 'jwt_title',
descriptionKey: 'jwt_description',
themeColorKey: 'info',
icon: Key,
defaultVisible: true,
components: {
popup: JwtPage,
sidepanel: JwtPage,
tab: JwtPage,
},
},
{
key: 'jsonDiff',
labelKey: 'jsonDiff_title',
descriptionKey: 'jsonDiff_description',
themeColorKey: 'primary',
icon: GitCompareArrows,
defaultVisible: true,
components: {
popup: JsonToolsPage,
sidepanel: JsonToolsPage,
tab: JsonToolsPage,
},
},
{
key: 'base64Converter',
labelKey: 'base64Converter_title',
descriptionKey: 'base64Converter_description',
themeColorKey: 'info',
icon: ArrowLeftRight,
defaultVisible: true,
components: {
popup: Base64ConverterPage,
sidepanel: Base64ConverterPage,
tab: Base64ConverterPage,
},
},
{
key: 'rightClickRestorer',
labelKey: 'rightClickRestorer_title',
descriptionKey: 'rightClickRestorer_description',
themeColorKey: 'success',
icon: MousePointerClick,
defaultVisible: true,
components: {
popup: RightClickRestorerPage,
sidepanel: RightClickRestorerPage,
tab: RightClickRestorerPage,
},
},
];
export function getFeatureByKey(key: PageType): FeatureConfig | undefined {
return FEATURES.find((f) => f.key === key);
}
export function getDefaultVisibleFeatureKeys(): PageType[] {
return FEATURES.filter((f) => f.defaultVisible).map((f) => f.key);
}
export function getAllFeatureKeys(): PageType[] {
return FEATURES.map((f) => f.key);
}
export function getDefaultPageOrder(): PageType[] {
return FEATURES.filter((f) => f.key !== 'dashboard').map((f) => f.key);
}
export function getEntryPointType(): 'popup' | 'sidepanel' | 'tab' {
const pathname = window.location.pathname;
if (pathname.includes('sidepanel')) {
return 'sidepanel';
}
if (new URLSearchParams(window.location.search).get('mode') === 'tab') {
return 'tab';
}
return 'popup';
}