refactor: 统一功能组件结构,简化特性配置
This commit is contained in:
@@ -677,7 +677,7 @@ src/pages/StorageCleaner/useStorageCleaner.ts — 页面级 Hook
|
|||||||
label: '时间戳转换',
|
label: '时间戳转换',
|
||||||
description: '日期与时间戳互转',
|
description: '日期与时间戳互转',
|
||||||
defaultVisible: true,
|
defaultVisible: true,
|
||||||
components: { popup: TimestampPage, sidepanel: TimestampPage, tab: TimestampPage },
|
component: TimestampPage,
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -764,7 +764,7 @@ const [themeMode, setThemeMode, isInitialized] = useStorageState(
|
|||||||
Chrome Storage 读取是异步的。项目通过 `localStorage` 快照(键名 `snapshot/{storageKey}`)提供同步初始值,消除首屏闪烁。
|
Chrome Storage 读取是异步的。项目通过 `localStorage` 快照(键名 `snapshot/{storageKey}`)提供同步初始值,消除首屏闪烁。
|
||||||
|
|
||||||
| 模块 | 快照工具 | 防覆盖机制 |
|
| 模块 | 快照工具 | 防覆盖机制 |
|
||||||
| ---- | -------- | ---------- |
|
| ------------------- | ------------------ | ----------------------------------------------------------------------------------------- |
|
||||||
| `RouterProvider` | `syncSnapshot.ts` | `canPersistRef`(加载成功后才写入)、`hasUserNavigatedRef`(用户导航后不被 storage 覆盖) |
|
| `RouterProvider` | `syncSnapshot.ts` | `canPersistRef`(加载成功后才写入)、`hasUserNavigatedRef`(用户导航后不被 storage 覆盖) |
|
||||||
| `useStorageState` | `syncSnapshot.ts` | `loadSucceededRef` 或 `userModifiedRef` 为 true 时才写入 |
|
| `useStorageState` | `syncSnapshot.ts` | `loadSucceededRef` 或 `userModifiedRef` 为 true 时才写入 |
|
||||||
| `ThemeModeProvider` | `themeSnapshot.ts` | `hasUserSetMode`(用户切换主题后不被 storage 覆盖) |
|
| `ThemeModeProvider` | `themeSnapshot.ts` | `hasUserSetMode`(用户切换主题后不被 storage 覆盖) |
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FEATURES, getEntryPointType } from '@/config/features';
|
import { FEATURES } from '@/config/features';
|
||||||
import { useRouter } from '@/providers/RouterProvider';
|
import { useRouter } from '@/providers/RouterProvider';
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
import PageErrorBoundary from '@/components/PageErrorBoundary';
|
import PageErrorBoundary from '@/components/PageErrorBoundary';
|
||||||
@@ -6,8 +6,6 @@ import PageSkeleton from '@/components/PageSkeleton';
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { AlertTriangle } from 'lucide-react';
|
import { AlertTriangle } from 'lucide-react';
|
||||||
|
|
||||||
const entryPointType = getEntryPointType();
|
|
||||||
|
|
||||||
export default function RouterContainer() {
|
export default function RouterContainer() {
|
||||||
const { currentPage, isLoaded } = useRouter();
|
const { currentPage, isLoaded } = useRouter();
|
||||||
|
|
||||||
@@ -19,7 +17,7 @@ export default function RouterContainer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const currentFeature = FEATURES.find((f) => f.key === currentPage);
|
const currentFeature = FEATURES.find((f) => f.key === currentPage);
|
||||||
const MatchedComponent = currentFeature?.components?.[entryPointType];
|
const MatchedComponent = currentFeature?.component;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -43,7 +41,7 @@ export default function RouterContainer() {
|
|||||||
</div>
|
</div>
|
||||||
<h3 className="text-sm font-semibold text-foreground">页面未找到</h3>
|
<h3 className="text-sm font-semibold text-foreground">页面未找到</h3>
|
||||||
<p className="text-xs text-muted-foreground mt-1 max-w-[240px]">
|
<p className="text-xs text-muted-foreground mt-1 max-w-[240px]">
|
||||||
该功能在当前运行环境({entryPointType.toUpperCase()})下不可用或已被移除。
|
该功能不存在或已被移除。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ describe('features 懒加载', () => {
|
|||||||
const { render, waitFor } = await import('@testing-library/react');
|
const { render, waitFor } = await import('@testing-library/react');
|
||||||
const { FEATURES } = await import('@/config/features');
|
const { FEATURES } = await import('@/config/features');
|
||||||
|
|
||||||
const DashboardPage = FEATURES.find((f) => f.key === 'dashboard')!.components.popup;
|
const DashboardPage = FEATURES.find((f) => f.key === 'dashboard')!.component;
|
||||||
|
|
||||||
render(
|
render(
|
||||||
React.createElement(React.Suspense, { fallback: null }, React.createElement(DashboardPage)),
|
React.createElement(React.Suspense, { fallback: null }, React.createElement(DashboardPage)),
|
||||||
|
|||||||
@@ -19,15 +19,13 @@ describe('features', () => {
|
|||||||
expect(feature).toHaveProperty('label');
|
expect(feature).toHaveProperty('label');
|
||||||
expect(feature).toHaveProperty('description');
|
expect(feature).toHaveProperty('description');
|
||||||
expect(feature).toHaveProperty('defaultVisible');
|
expect(feature).toHaveProperty('defaultVisible');
|
||||||
expect(feature).toHaveProperty('components');
|
expect(feature).toHaveProperty('component');
|
||||||
expect(typeof feature.key).toBe('string');
|
expect(typeof feature.key).toBe('string');
|
||||||
expect(typeof feature.label).toBe('string');
|
expect(typeof feature.label).toBe('string');
|
||||||
expect(typeof feature.description).toBe('string');
|
expect(typeof feature.description).toBe('string');
|
||||||
expect(typeof feature.defaultVisible).toBe('boolean');
|
expect(typeof feature.defaultVisible).toBe('boolean');
|
||||||
expect(typeof feature.components).toBe('object');
|
expect(feature.component).toBeDefined();
|
||||||
expect(feature.components).toHaveProperty('popup');
|
expect(['function', 'object']).toContain(typeof feature.component);
|
||||||
expect(feature.components).toHaveProperty('sidepanel');
|
|
||||||
expect(feature.components).toHaveProperty('tab');
|
|
||||||
|
|
||||||
if (feature.key !== 'dashboard') {
|
if (feature.key !== 'dashboard') {
|
||||||
expect(feature).toHaveProperty('icon');
|
expect(feature).toHaveProperty('icon');
|
||||||
|
|||||||
+11
-55
@@ -34,11 +34,7 @@ export interface FeatureConfig {
|
|||||||
themeColorKey?: PaletteColorKey;
|
themeColorKey?: PaletteColorKey;
|
||||||
icon?: ComponentType<LucideProps>;
|
icon?: ComponentType<LucideProps>;
|
||||||
defaultVisible: boolean;
|
defaultVisible: boolean;
|
||||||
components: {
|
component: ComponentType;
|
||||||
popup: ComponentType;
|
|
||||||
sidepanel: ComponentType;
|
|
||||||
tab: ComponentType;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FEATURES: FeatureConfig[] = [
|
export const FEATURES: FeatureConfig[] = [
|
||||||
@@ -47,11 +43,7 @@ export const FEATURES: FeatureConfig[] = [
|
|||||||
label: '仪表盘',
|
label: '仪表盘',
|
||||||
description: '',
|
description: '',
|
||||||
defaultVisible: true,
|
defaultVisible: true,
|
||||||
components: {
|
component: DashboardPage,
|
||||||
popup: DashboardPage,
|
|
||||||
sidepanel: DashboardPage,
|
|
||||||
tab: DashboardPage,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'timestamp',
|
key: 'timestamp',
|
||||||
@@ -60,11 +52,7 @@ export const FEATURES: FeatureConfig[] = [
|
|||||||
themeColorKey: 'primary',
|
themeColorKey: 'primary',
|
||||||
icon: Clock,
|
icon: Clock,
|
||||||
defaultVisible: true,
|
defaultVisible: true,
|
||||||
components: {
|
component: TimestampPage,
|
||||||
popup: TimestampPage,
|
|
||||||
sidepanel: TimestampPage,
|
|
||||||
tab: TimestampPage,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'storageCleaner',
|
key: 'storageCleaner',
|
||||||
@@ -73,11 +61,7 @@ export const FEATURES: FeatureConfig[] = [
|
|||||||
themeColorKey: 'warning',
|
themeColorKey: 'warning',
|
||||||
icon: Database,
|
icon: Database,
|
||||||
defaultVisible: true,
|
defaultVisible: true,
|
||||||
components: {
|
component: StorageCleanerPage,
|
||||||
popup: StorageCleanerPage,
|
|
||||||
sidepanel: StorageCleanerPage,
|
|
||||||
tab: StorageCleanerPage,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'qrCode',
|
key: 'qrCode',
|
||||||
@@ -86,11 +70,7 @@ export const FEATURES: FeatureConfig[] = [
|
|||||||
themeColorKey: 'success',
|
themeColorKey: 'success',
|
||||||
icon: QrCode,
|
icon: QrCode,
|
||||||
defaultVisible: true,
|
defaultVisible: true,
|
||||||
components: {
|
component: QrCodePage,
|
||||||
popup: QrCodePage,
|
|
||||||
sidepanel: QrCodePage,
|
|
||||||
tab: QrCodePage,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'textStatistics',
|
key: 'textStatistics',
|
||||||
@@ -99,11 +79,7 @@ export const FEATURES: FeatureConfig[] = [
|
|||||||
themeColorKey: 'secondary',
|
themeColorKey: 'secondary',
|
||||||
icon: FileText,
|
icon: FileText,
|
||||||
defaultVisible: true,
|
defaultVisible: true,
|
||||||
components: {
|
component: TextStatisticsPage,
|
||||||
popup: TextStatisticsPage,
|
|
||||||
sidepanel: TextStatisticsPage,
|
|
||||||
tab: TextStatisticsPage,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'jwt',
|
key: 'jwt',
|
||||||
@@ -112,11 +88,7 @@ export const FEATURES: FeatureConfig[] = [
|
|||||||
themeColorKey: 'info',
|
themeColorKey: 'info',
|
||||||
icon: Key,
|
icon: Key,
|
||||||
defaultVisible: true,
|
defaultVisible: true,
|
||||||
components: {
|
component: JwtPage,
|
||||||
popup: JwtPage,
|
|
||||||
sidepanel: JwtPage,
|
|
||||||
tab: JwtPage,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'jsonTools',
|
key: 'jsonTools',
|
||||||
@@ -125,11 +97,7 @@ export const FEATURES: FeatureConfig[] = [
|
|||||||
themeColorKey: 'primary',
|
themeColorKey: 'primary',
|
||||||
icon: GitCompareArrows,
|
icon: GitCompareArrows,
|
||||||
defaultVisible: true,
|
defaultVisible: true,
|
||||||
components: {
|
component: JsonToolsPage,
|
||||||
popup: JsonToolsPage,
|
|
||||||
sidepanel: JsonToolsPage,
|
|
||||||
tab: JsonToolsPage,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'base64Converter',
|
key: 'base64Converter',
|
||||||
@@ -138,11 +106,7 @@ export const FEATURES: FeatureConfig[] = [
|
|||||||
themeColorKey: 'info',
|
themeColorKey: 'info',
|
||||||
icon: ArrowLeftRight,
|
icon: ArrowLeftRight,
|
||||||
defaultVisible: true,
|
defaultVisible: true,
|
||||||
components: {
|
component: Base64ConverterPage,
|
||||||
popup: Base64ConverterPage,
|
|
||||||
sidepanel: Base64ConverterPage,
|
|
||||||
tab: Base64ConverterPage,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'rightClickRestorer',
|
key: 'rightClickRestorer',
|
||||||
@@ -151,11 +115,7 @@ export const FEATURES: FeatureConfig[] = [
|
|||||||
themeColorKey: 'success',
|
themeColorKey: 'success',
|
||||||
icon: MousePointerClick,
|
icon: MousePointerClick,
|
||||||
defaultVisible: true,
|
defaultVisible: true,
|
||||||
components: {
|
component: RightClickRestorerPage,
|
||||||
popup: RightClickRestorerPage,
|
|
||||||
sidepanel: RightClickRestorerPage,
|
|
||||||
tab: RightClickRestorerPage,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'testDataGenerator',
|
key: 'testDataGenerator',
|
||||||
@@ -164,11 +124,7 @@ export const FEATURES: FeatureConfig[] = [
|
|||||||
themeColorKey: 'warning',
|
themeColorKey: 'warning',
|
||||||
icon: FileSpreadsheet,
|
icon: FileSpreadsheet,
|
||||||
defaultVisible: true,
|
defaultVisible: true,
|
||||||
components: {
|
component: TestDataGeneratorPage,
|
||||||
popup: TestDataGeneratorPage,
|
|
||||||
sidepanel: TestDataGeneratorPage,
|
|
||||||
tab: TestDataGeneratorPage,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user