重构导航逻辑,去除dashboard页面相关功能,增加侧边栏导航功能

This commit is contained in:
2026-07-05 22:34:12 +08:00
53 changed files with 774 additions and 1328 deletions
+2 -39
View File
@@ -4,30 +4,6 @@
* 用于 Suspense fallback 和初始加载状态,提供平滑的视觉过渡
* 避免白屏闪烁,减少布局偏移
*/
interface PageSkeletonProps {
/** 骨架屏类型 */
variant?: 'dashboard' | 'tool';
}
/**
* 仪表盘卡片骨架屏
*/
function DashboardCardSkeleton() {
return (
<div className="rounded-xl border border-border p-5 h-[100px]">
<div className="flex justify-between items-start">
<div className="flex gap-3 items-center">
<div className="w-10 h-10 rounded-lg bg-muted animate-pulse" />
<div>
<div className="w-24 h-5 bg-muted rounded animate-pulse" />
<div className="w-32 h-3.5 bg-muted rounded animate-pulse mt-1.5" />
</div>
</div>
<div className="w-3 h-3 rounded-full bg-muted animate-pulse" />
</div>
</div>
);
}
/**
* 工具页面骨架屏
@@ -57,22 +33,9 @@ function ToolPageSkeleton() {
/**
* 页面加载骨架屏
*
* @param props - PageSkeletonProps
* @returns 骨架屏 JSX 元素
*/
export default function PageSkeleton({ variant = 'dashboard' }: PageSkeletonProps) {
if (variant === 'tool') {
return <ToolPageSkeleton />;
}
return (
<div className="grid grid-cols-1 sm:grid-cols-[repeat(auto-fill,minmax(300px,1fr))] auto-rows-fr gap-4 p-4">
{Array.from({ length: 6 }).map((_, index) => (
<DashboardCardSkeleton key={index} />
))}
</div>
);
export default function PageSkeleton() {
return <ToolPageSkeleton />;
}
PageSkeleton.displayName = 'PageSkeleton';
+2 -6
View File
@@ -30,7 +30,7 @@ function LoadedPage({ pageKey }: { pageKey: PageType }) {
}, [pageKey]);
if (!Page) {
return <PageSkeleton variant={pageKey === 'dashboard' ? 'dashboard' : 'tool'} />;
return <PageSkeleton />;
}
return <Page />;
@@ -39,9 +39,6 @@ function LoadedPage({ pageKey }: { pageKey: PageType }) {
export default function RouterContainer() {
const { currentPage } = useRouter();
const animationClass =
currentPage === 'dashboard' ? 'page-transition-dashboard' : 'page-transition-enter';
const currentFeature = getFeatureByKey(currentPage);
return (
@@ -49,8 +46,7 @@ export default function RouterContainer() {
key={currentPage}
className={cn(
'flex-1 flex flex-col overflow-x-hidden overflow-y-auto',
'motion-reduce:transition-none',
animationClass,
'motion-reduce:transition-none page-transition-enter',
)}
>
<PageErrorBoundary resetKey={currentPage}>
+5 -37
View File
@@ -4,41 +4,17 @@ import PageSkeleton from '@/components/PageSkeleton';
describe('PageSkeleton 组件', () => {
describe('渲染测试', () => {
it('默认应渲染 dashboard 骨架', () => {
it('应渲染工具页面骨架', () => {
const { container } = render(<PageSkeleton />);
// dashboard 骨架屏包含 6 个卡片
const cards = container.querySelectorAll('.rounded-xl');
expect(cards.length).toBe(6);
});
it('variant 为 dashboard 时应渲染仪表盘卡片骨架', () => {
const { container } = render(<PageSkeleton variant="dashboard" />);
// 每个卡片有 2 个骨架元素(图标、文本),6 个卡片共 12 个
const cards = container.querySelectorAll('.rounded-xl');
expect(cards.length).toBe(6);
});
it('variant 为 tool 时应渲染工具页面骨架', () => {
const { container } = render(<PageSkeleton variant="tool" />);
// tool 骨架屏包含标题、输入区、控制栏 3 个按钮、结果区
const skeletons = container.querySelectorAll('.animate-pulse');
expect(skeletons.length).toBeGreaterThan(0);
});
});
describe('布局结构测试', () => {
it('dashboard 骨架屏应使用 grid 布局', () => {
const { container } = render(<PageSkeleton variant="dashboard" />);
const gridContainer = container.firstChild as HTMLElement;
expect(gridContainer).toHaveClass('grid');
});
it('tool 骨架屏应有内边距', () => {
const { container } = render(<PageSkeleton variant="tool" />);
it('工具骨架屏应有内边距', () => {
const { container } = render(<PageSkeleton />);
const toolContainer = container.firstChild as HTMLElement;
expect(toolContainer).toHaveClass('p-5');
@@ -46,16 +22,8 @@ describe('PageSkeleton 组件', () => {
});
describe('骨架屏元素测试', () => {
it('dashboard 骨架屏应包含圆角和边框样式', () => {
const { container } = render(<PageSkeleton variant="dashboard" />);
// 获取第一个卡片容器
const card = container.querySelector('.rounded-xl.border');
expect(card).toBeInTheDocument();
});
it('tool 骨架屏应包含动画脉冲效果', () => {
const { container } = render(<PageSkeleton variant="tool" />);
it('工具骨架屏应包含动画脉冲效果', () => {
const { container } = render(<PageSkeleton />);
const skeletons = container.querySelectorAll('.animate-pulse');
expect(skeletons.length).toBeGreaterThan(0);
@@ -6,12 +6,10 @@ import type { PageType } from '@/types/storage';
import React from 'react';
const mockRouterValue = {
currentPage: 'dashboard' as PageType,
visiblePages: ['dashboard', 'timestamp'] as PageType[],
currentPage: 'timestamp' as PageType,
visiblePages: ['timestamp', 'jwt'] as PageType[],
pageOrder: ['timestamp'] as PageType[],
navigateTo: vi.fn(),
syncNavigation: vi.fn(),
goHome: vi.fn(),
setVisiblePages: vi.fn(),
setPageOrder: vi.fn(),
recentlyUsedTools: [] as PageType[],
@@ -34,21 +32,14 @@ describe('RouterContainer 组件', () => {
describe('渲染测试', () => {
it('mount 后应直接渲染页面结构(不等待 storage 加载)', () => {
mockRouterValue.currentPage = 'dashboard';
mockRouterValue.currentPage = 'timestamp';
const { container } = renderWithProvider(<RouterContainer />);
expect(container.querySelector('.page-transition-dashboard')).toBeInTheDocument();
expect(container.querySelector('.page-transition-enter')).toBeInTheDocument();
});
});
describe('动画类测试', () => {
it('在 dashboard 页面应应用 dashboard 动画类', () => {
mockRouterValue.currentPage = 'dashboard';
renderWithProvider(<RouterContainer />);
const box = document.querySelector('.page-transition-dashboard');
expect(box).toBeInTheDocument();
});
it('在非 dashboard 页面应应用 enter 动画类', () => {
it('应应用 enter 动画类', () => {
mockRouterValue.currentPage = 'timestamp';
renderWithProvider(<RouterContainer />);
const box = document.querySelector('.page-transition-enter');
@@ -60,7 +51,7 @@ describe('RouterContainer 组件', () => {
it('currentPage 变化时应更新', () => {
const { rerender } = renderWithProvider(<RouterContainer />);
mockRouterValue.currentPage = 'timestamp';
mockRouterValue.currentPage = 'jwt';
rerender(<RouterProvider>{<RouterContainer />}</RouterProvider>);
const box = document.querySelector('.page-transition-enter');
@@ -70,11 +61,10 @@ describe('RouterContainer 组件', () => {
describe('页面级错误隔离', () => {
it('PageErrorBoundary 应包裹页面内容', () => {
mockRouterValue.currentPage = 'dashboard';
mockRouterValue.currentPage = 'timestamp';
const { container } = renderWithProvider(<RouterContainer />);
// 验证 RouterContainer 的 Box 结构存在
const routerBox = container.querySelector('.page-transition-dashboard');
const routerBox = container.querySelector('.page-transition-enter');
expect(routerBox).toBeInTheDocument();
});
});