refactor: 移除所有页面的 PageHeader 组件
删除 PageHeader 组件及其测试文件,清理相关不再使用的导入和变量。
This commit is contained in:
@@ -1,104 +0,0 @@
|
||||
import { ReactNode, useMemo } from 'react';
|
||||
import { getEntryPointType } from '@/config/features';
|
||||
import { cn } from '@/lib/utils'; // shadcn 核心类名合并工具
|
||||
|
||||
export interface PageHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
/** 要显示的图标组件 */
|
||||
icon: ReactNode;
|
||||
/**
|
||||
* 图标的颜色,支持:
|
||||
* 1. Tailwind 颜色类名 (如 'text-blue-500', 'text-primary') -> 推荐
|
||||
* 2. 原生颜色值 (如 '#3b82f6')
|
||||
*/
|
||||
iconColor?: string;
|
||||
/** 主标题文本 */
|
||||
title: string;
|
||||
/** 副标题文本(可选) */
|
||||
subtitle?: string;
|
||||
/** 在标题右侧显示的徽章/标签组件(可选) */
|
||||
badge?: ReactNode;
|
||||
/** 覆盖图标容器的类名 */
|
||||
iconClassName?: string;
|
||||
/** 覆盖主标题的类名 */
|
||||
titleClassName?: string;
|
||||
/** 覆盖副标题的类名 */
|
||||
subtitleClassName?: string;
|
||||
}
|
||||
|
||||
export default function PageHeader({
|
||||
icon,
|
||||
iconColor = 'text-blue-500', // 默认改用类名,若需保持 Hex 可写 "#3b82f6"
|
||||
title,
|
||||
subtitle,
|
||||
badge,
|
||||
iconClassName,
|
||||
titleClassName,
|
||||
subtitleClassName,
|
||||
className,
|
||||
...props
|
||||
}: PageHeaderProps) {
|
||||
const entryPointType = useMemo(() => getEntryPointType(), []);
|
||||
|
||||
// 扩展环境判断:如果是 popup 形式则不渲染头部
|
||||
if (entryPointType === 'popup') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 判断传入的是否是 Hex/RGB 等原生颜色值
|
||||
const isRawColor =
|
||||
iconColor.startsWith('#') || iconColor.startsWith('rgb') || iconColor.startsWith('hsl');
|
||||
|
||||
return (
|
||||
<div className={cn('flex items-center gap-3 mb-6', className)} {...props}>
|
||||
{/* 图标容器 */}
|
||||
<div
|
||||
className={cn(
|
||||
'p-2 rounded-lg flex items-center justify-center shrink-0',
|
||||
// 如果不是原生颜色,直接当作 Tailwind 类名注入
|
||||
!isRawColor && iconColor,
|
||||
iconClassName,
|
||||
)}
|
||||
style={
|
||||
isRawColor
|
||||
? {
|
||||
color: iconColor,
|
||||
// 使用 CSS inline 变量或 color-mix 安全处理透明度,不再暴力拼接 "15"
|
||||
backgroundColor: `color-mix(in srgb, ${iconColor} 8%, transparent)`,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{/* 确保图标大小可控,通过子元素选择器约束 SVG 宽高 */}
|
||||
<div className="[&>svg]:h-5 [&>svg]:w-5">{icon}</div>
|
||||
</div>
|
||||
|
||||
{/* 标题区域 */}
|
||||
<div className="flex-1 min-w-0 flex flex-col gap-0.5">
|
||||
{/* 标题行(含徽章) */}
|
||||
<div className="flex justify-between items-center gap-2">
|
||||
<h1
|
||||
className={cn(
|
||||
'text-base font-extrabold tracking-tight leading-tight text-foreground truncate',
|
||||
titleClassName,
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
{badge && <div className="shrink-0">{badge}</div>}
|
||||
</div>
|
||||
|
||||
{/* 副标题 - 使用 p 标签(block)保证换行 */}
|
||||
{subtitle && (
|
||||
<p
|
||||
className={cn(
|
||||
'text-xs font-semibold text-muted-foreground truncate',
|
||||
subtitleClassName,
|
||||
)}
|
||||
>
|
||||
{subtitle}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import PageHeader, { type PageHeaderProps } from '@/components/PageHeader';
|
||||
|
||||
vi.mock('@/config/features', () => ({
|
||||
getEntryPointType: vi.fn(() => 'sidepanel'),
|
||||
}));
|
||||
|
||||
describe('PageHeader 组件系统', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
const defaultProps: PageHeaderProps = {
|
||||
icon: <span data-testid="test-icon">⏰</span>,
|
||||
title: '时间戳转换',
|
||||
subtitle: 'Unix 毫秒数转换与格式化',
|
||||
};
|
||||
|
||||
describe('PageHeader UI 渲染', () => {
|
||||
it('应渲染页面标题栏&副标题', () => {
|
||||
render(<PageHeader {...defaultProps} />);
|
||||
expect(screen.getByText('时间戳转换')).toBeInTheDocument();
|
||||
expect(screen.getByText('Unix 毫秒数转换与格式化')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('应渲染图标', () => {
|
||||
render(<PageHeader {...defaultProps} />);
|
||||
expect(screen.getByTestId('test-icon')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('应渲染自定义图标&图标颜色', () => {
|
||||
render(
|
||||
<PageHeader
|
||||
{...defaultProps}
|
||||
icon={<span data-testid="custom-icon">X</span>}
|
||||
iconColor="#FF0000"
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByTestId('custom-icon')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('应默认使用蓝色作为 primary 色', () => {
|
||||
render(<PageHeader {...defaultProps} />);
|
||||
const iconContainer = screen.getByTestId('test-icon').parentElement?.parentElement;
|
||||
expect(iconContainer).toHaveClass('text-blue-500');
|
||||
});
|
||||
|
||||
it('应渲染 badge 组件', () => {
|
||||
const badge = <span data-testid="test-badge">New</span>;
|
||||
render(<PageHeader {...defaultProps} badge={badge} />);
|
||||
expect(screen.getByTestId('test-badge')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('应支持自定义 iconClassName', () => {
|
||||
render(<PageHeader {...defaultProps} iconClassName="custom-icon-class" />);
|
||||
const iconContainer = screen.getByTestId('test-icon').parentElement?.parentElement;
|
||||
expect(iconContainer).toHaveClass('custom-icon-class');
|
||||
});
|
||||
|
||||
it('应支持自定义 titleClassName', () => {
|
||||
render(<PageHeader {...defaultProps} titleClassName="custom-title-class" />);
|
||||
const title = screen.getByText('时间戳转换');
|
||||
expect(title).toHaveClass('custom-title-class');
|
||||
});
|
||||
|
||||
it('应支持自定义 subtitleClassName', () => {
|
||||
render(<PageHeader {...defaultProps} subtitleClassName="custom-subtitle-class" />);
|
||||
const subtitle = screen.getByText('Unix 毫秒数转换与格式化');
|
||||
expect(subtitle).toHaveClass('custom-subtitle-class');
|
||||
});
|
||||
|
||||
it('应支持自定义 className', () => {
|
||||
const { container } = render(<PageHeader {...defaultProps} className="custom-page-header" />);
|
||||
const outerElement = container.firstChild;
|
||||
expect(outerElement).toHaveClass('custom-page-header');
|
||||
});
|
||||
|
||||
it('无副标题时不渲染副标题区域', () => {
|
||||
const { container } = render(<PageHeader icon={defaultProps.icon} title="仅标题" />);
|
||||
const subtitles = container.querySelectorAll('.text-muted-foreground');
|
||||
expect(subtitles.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PageHeader 入口点隐藏', () => {
|
||||
it('popup 模式下应返回 null', async () => {
|
||||
const { getEntryPointType } = await import('@/config/features');
|
||||
vi.mocked(getEntryPointType).mockReturnValue('popup');
|
||||
|
||||
const { container } = render(<PageHeader {...defaultProps} />);
|
||||
expect(container.innerHTML).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SyntheticEvent, useEffect, useMemo, useState } from 'react';
|
||||
import { GripVertical, RefreshCw, Settings } from 'lucide-react';
|
||||
import { GripVertical, RefreshCw } from 'lucide-react';
|
||||
import {
|
||||
closestCenter,
|
||||
DndContext,
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
} from '@/config/features';
|
||||
import GlobalSnackbar, { useSnackbarState } from '@/components/GlobalSnackbar';
|
||||
import PageErrorBoundary from '@/components/PageErrorBoundary';
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const PALETTE_COLORS: Record<PaletteColorKey, string> = {
|
||||
@@ -317,12 +316,6 @@ export default function App() {
|
||||
{/* 顶部标题与导航栏 */}
|
||||
<div className="w-full bg-background border-b border-border pt-8 sm:pt-12 pb-0 px-4 sm:px-8">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<PageHeader
|
||||
icon={<Settings size={20} />}
|
||||
iconColor="#1976d2"
|
||||
title="应用设置"
|
||||
subtitle="针对不同窗口类型独立配置 Dashboard 中显示的功能及其排序"
|
||||
/>
|
||||
{/* Tab 与恢复按钮同行 */}
|
||||
<div className="flex items-end justify-between border-b-0">
|
||||
<div className="flex-1 flex">
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import { Image as ImageIcon, Type, Upload } from 'lucide-react';
|
||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
|
||||
import { useStorageState } from '@/utils/useStorageState';
|
||||
import type { Base64ConverterPageMode } from '@/types/storage';
|
||||
import TextMode from './TextMode';
|
||||
@@ -22,22 +19,8 @@ export default function Index() {
|
||||
isValidPageMode,
|
||||
);
|
||||
|
||||
const modeIcon: Record<PageMode, React.ReactNode> = {
|
||||
text: <Type className="h-4 w-4" />,
|
||||
file: <Upload className="h-4 w-4" />,
|
||||
image: <ImageIcon className="h-4 w-4" />,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 w-full flex flex-col space-y-4 min-h-[520px] select-none animate-in fade-in duration-300">
|
||||
<PageHeader
|
||||
title={t('base64Converter:pageTitle')}
|
||||
subtitle={t('base64Converter:pageSubtitle')}
|
||||
icon={modeIcon[pageMode]}
|
||||
iconColor="text-indigo-800"
|
||||
className="pb-1"
|
||||
/>
|
||||
|
||||
<SwitchButtonGroup
|
||||
value={pageMode}
|
||||
options={[
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { Code, Download, Trash2 } from 'lucide-react';
|
||||
import { Download, Trash2 } from 'lucide-react';
|
||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import CopyButton from '@/components/CopyButton';
|
||||
import SwitchButtonGroup from '@/components/SwitchButtonGroup';
|
||||
import { Button } from '@/components/ui/button'; // 💡 1. 全面回归规范:引入原生的 shadcn 原子 Button
|
||||
@@ -51,12 +50,6 @@ export default function HtmlToMarkdownPage() {
|
||||
- 统一收拢为我们先前在 Dashboard 页、JSON 工具箱制定的 p-4 space-y-4 标准极客桌面规格。
|
||||
*/
|
||||
<div className="p-4 w-full flex flex-col space-y-4 select-none animate-in fade-in duration-300">
|
||||
<PageHeader
|
||||
title={t('pageTitle')}
|
||||
subtitle={t('pageSubtitle')}
|
||||
icon={<Code className="h-4 w-4" />}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* 工具栏集成区 */}
|
||||
<div className="flex flex-col sm:flex-row gap-3 justify-between items-stretch sm:items-center bg-secondary/40 rounded-xl border border-border/60 px-1.5 py-1.5 sm:h-12">
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { ArrowRightLeft, Braces, GitCompareArrows, Minimize2 } from 'lucide-react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import JsonDiffInput from './JsonDiffInput';
|
||||
import DiffResult from './DiffResult';
|
||||
import DiffNavigator from './DiffNavigator';
|
||||
@@ -99,22 +97,6 @@ export default function Index() {
|
||||
|
||||
const activePath = diffResult && total > 0 ? diffResult.diffPaths[currentDiffIndex] : undefined;
|
||||
|
||||
const modeTitles: Record<PageMode, { title: string; subtitle: string }> = {
|
||||
diff: { title: 'jsonDiff:pageTitle', subtitle: 'jsonDiff:pageSubtitle' },
|
||||
format: { title: 'jsonFormat:formatTitle', subtitle: 'jsonFormat:formatSubtitle' },
|
||||
yaml: { title: 'jsonFormat:yamlTitle', subtitle: 'jsonFormat:yamlSubtitle' },
|
||||
toml: { title: 'jsonFormat:tomlTitle', subtitle: 'jsonFormat:tomlSubtitle' },
|
||||
minify: { title: 'jsonFormat:minifyTitle', subtitle: 'jsonFormat:minifySubtitle' },
|
||||
};
|
||||
|
||||
const modeIcon: Record<PageMode, React.ReactNode> = {
|
||||
diff: <GitCompareArrows className="h-4 w-4" />,
|
||||
format: <Braces className="h-4 w-4" />,
|
||||
yaml: <ArrowRightLeft className="h-4 w-4" />,
|
||||
toml: <ArrowRightLeft className="h-4 w-4" />,
|
||||
minify: <Minimize2 className="h-4 w-4" />,
|
||||
};
|
||||
|
||||
const yamlConvert: ConvertFunction = useCallback((text: string) => {
|
||||
const r = jsonToYaml(text);
|
||||
return { output: r.output, originalBytes: r.originalBytes, outputBytes: r.outputBytes };
|
||||
@@ -132,14 +114,6 @@ export default function Index() {
|
||||
|
||||
return (
|
||||
<div className="p-4 w-full flex flex-col space-y-4 min-h-[500px] select-none animate-in fade-in duration-300">
|
||||
<PageHeader
|
||||
title={t(modeTitles[pageMode].title)}
|
||||
subtitle={t(modeTitles[pageMode].subtitle)}
|
||||
icon={modeIcon[pageMode]}
|
||||
iconColor="#3b82f6"
|
||||
className="pb-1"
|
||||
/>
|
||||
|
||||
<SwitchButtonGroup
|
||||
value={pageMode}
|
||||
onChange={(v: PageMode) => setPageMode(v)}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Key } from 'lucide-react';
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import { parseJwt, stringifyJson } from '@/utils/jwt';
|
||||
import CopyButton from '@/components/CopyButton';
|
||||
import TextInputArea from '@/components/TextInputArea';
|
||||
@@ -73,12 +71,6 @@ export default function Index() {
|
||||
|
||||
return (
|
||||
<div className="p-4 w-full flex flex-col space-y-4 animate-in fade-in duration-300 select-none">
|
||||
<PageHeader
|
||||
title={t('jwt:pageTitle')}
|
||||
subtitle={t('jwt:pageSubtitle')}
|
||||
icon={<Key className="h-4 w-4" />} // 💡 规范锁死 Icon 宽高,抹杀闪烁
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* 输入终端 */}
|
||||
<TextInputArea
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Code, Download, Printer, Trash2 } from 'lucide-react';
|
||||
import { Download, Printer, Trash2 } from 'lucide-react';
|
||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import CopyButton from '@/components/CopyButton';
|
||||
import SwitchButtonGroup from '@/components/SwitchButtonGroup';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -177,13 +176,6 @@ export default function MarkdownToHtmlPage() {
|
||||
|
||||
return (
|
||||
<div className="p-4 w-full flex flex-col space-y-4 select-none animate-in fade-in duration-300">
|
||||
<PageHeader
|
||||
title={t('pageTitle')}
|
||||
subtitle={t('pageSubtitle')}
|
||||
icon={<Code className="h-4 w-4" />}
|
||||
className="pb-1"
|
||||
/>
|
||||
|
||||
<div className="flex flex-col space-y-4">
|
||||
{/* 工具集成控制中枢 */}
|
||||
<div className="flex flex-col sm:flex-row gap-3 justify-between items-stretch sm:items-center bg-secondary/40 rounded-xl border border-border/60 px-1.5 py-1.5 sm:h-12">
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { QrCode as QrCodeIcon } from 'lucide-react'; // 💡 别名规整,防止与页面组件发生重名误判
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import SwitchButtonGroup from '@/components/SwitchButtonGroup';
|
||||
|
||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
||||
@@ -26,15 +24,6 @@ export default function Index() {
|
||||
- 替换为标准的 p-4 呼吸内边距配合 flex flex-col space-y-4,接管系统级重排!
|
||||
*/}
|
||||
<div className="p-4 w-full flex flex-col space-y-4 min-h-[500px] select-none animate-in fade-in duration-300">
|
||||
{/* 标题控制栏:追加微调 py-0.5,防范文字边缘截断 */}
|
||||
<PageHeader
|
||||
title={t('qrCode:pageTitle')}
|
||||
subtitle={t('qrCode:pageSubtitle')}
|
||||
icon={<QrCodeIcon className="h-4 w-4" />} // 💡 规范对齐:强制锁死 Icon 宽高,抹杀闪烁
|
||||
iconColor="text-green-700"
|
||||
className="pb-1"
|
||||
/>
|
||||
|
||||
{/* 流式中央控制切流卡:注入 sm 断点防御,防范单栏状态下发生变形 */}
|
||||
<div className="w-full sm:w-fit pt-0.5">
|
||||
<SwitchButtonGroup
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { FileText } from 'lucide-react';
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import TextInputArea from '@/components/TextInputArea';
|
||||
import { formatByteSize, getTextStats } from '@/utils/textStatistics';
|
||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
||||
@@ -29,14 +27,6 @@ export default function Index() {
|
||||
|
||||
return (
|
||||
<div className="p-4 w-full space-y-4 animate-in fade-in duration-300">
|
||||
{/* 头部区域 */}
|
||||
<PageHeader
|
||||
title={t('textStatistics:pageTitle')}
|
||||
subtitle={t('textStatistics:pageSubtitle')}
|
||||
icon={<FileText />}
|
||||
iconColor="text-purple-500"
|
||||
/>
|
||||
|
||||
{/* 文本输入区域 */}
|
||||
<TextInputArea
|
||||
value={text}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { Clock } from 'lucide-react';
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import SwitchButtonGroup from '@/components/SwitchButtonGroup';
|
||||
import { ZONES } from './constants';
|
||||
import LiveClock from './LiveClock';
|
||||
@@ -39,13 +37,6 @@ export default function Index() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground antialiased selection:bg-primary/20">
|
||||
<div className="max-w-7xl mx-auto p-4 sm:p-6 space-y-4">
|
||||
{/* Header */}
|
||||
<PageHeader
|
||||
title={t('timestamp:pageTitle')}
|
||||
subtitle={t('timestamp:pageSubtitle')}
|
||||
icon={<Clock />}
|
||||
/>
|
||||
|
||||
{/* 动态参考信息时钟条 */}
|
||||
<LiveClock unit={unit} onUseNow={handleUseNow} />
|
||||
|
||||
|
||||
Reference in New Issue
Block a user