diff --git a/components/PageHeader.tsx b/components/PageHeader.tsx index 61377ab..3bd9fac 100644 --- a/components/PageHeader.tsx +++ b/components/PageHeader.tsx @@ -1,13 +1,15 @@ import { ReactNode, useMemo } from 'react'; import { getEntryPointType } from '@/config/features'; +import { cn } from '@/lib/utils'; // shadcn 核心类名合并工具 -/** - * PageHeader 组件属性接口 - */ -export interface PageHeaderProps { +export interface PageHeaderProps extends React.HTMLAttributes { /** 要显示的图标组件 */ icon: ReactNode; - /** 图标的颜色,默认使用主题 primary.main 色 */ + /** + * 图标的颜色,支持: + * 1. Tailwind 颜色类名 (如 'text-blue-500', 'text-primary') -> 推荐 + * 2. 原生颜色值 (如 '#3b82f6') + */ iconColor?: string; /** 主标题文本 */ title: string; @@ -15,86 +17,86 @@ export interface PageHeaderProps { subtitle?: string; /** 在标题右侧显示的徽章/标签组件(可选) */ badge?: ReactNode; - /** 图标容器的自定义样式 */ - iconSx?: React.CSSProperties; - /** 标题文本的自定义样式 */ - titleSx?: React.CSSProperties; - /** 副标题文本的自定义样式 */ - subtitleSx?: React.CSSProperties; - /** 整个组件的自定义样式 */ - sx?: React.CSSProperties; + /** 覆盖图标容器的类名 */ + iconClassName?: string; + /** 覆盖主标题的类名 */ + titleClassName?: string; + /** 覆盖副标题的类名 */ + subtitleClassName?: string; } -/** - * PageHeader - 通用页面标题栏组件 - * - * 用于显示带图标的页面标题,支持自定义颜色、副标题、徽章等功能 - * - * @example - * ```tsx - * } - * iconColor="#1976d2" - * title="时间戳转换" - * subtitle="Unix 毫秒数转换与格式化" - * /> - * ``` - * - * @example - * ```tsx - * } - * iconColor={storageCleanerPageStyles.warningColor} - * title="存储清理" - * subtitle={domain} - * badge={已占用 {size}} - * /> - * ``` - */ export default function PageHeader({ icon, - iconColor = '#3b82f6', + iconColor = 'text-blue-500', // 默认改用类名,若需保持 Hex 可写 "#3b82f6" title, subtitle, badge, - iconSx, - titleSx, - subtitleSx, - sx, + 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 ( -
+
{/* 图标容器 */}
- {icon} + {/* 确保图标大小可控,通过子元素选择器约束 SVG 宽高 */} +
{icon}
+ {/* 标题区域 */} -
+
{/* 标题行(含徽章) */} -
- +
+

{title} - - {badge} +

+ {badge &&
{badge}
}
- {/* 副标题 */} + + {/* 副标题 - 使用 p 标签(block)保证换行 */} {subtitle && ( - +

{subtitle} - +

)}
diff --git a/components/__tests__/PageHeader.test.tsx b/components/__tests__/PageHeader.test.tsx index 55fab2b..0b4a8f0 100644 --- a/components/__tests__/PageHeader.test.tsx +++ b/components/__tests__/PageHeader.test.tsx @@ -47,9 +47,8 @@ describe('PageHeader 组件系统', () => { it('应默认使用蓝色作为 primary 色', () => { render(); - const iconContainer = screen.getByTestId('test-icon').parentElement; - expect(iconContainer).toHaveStyle('background-color: #3b82f615'); - expect(iconContainer).toHaveStyle('color: #3b82f6'); + const iconContainer = screen.getByTestId('test-icon').parentElement?.parentElement; + expect(iconContainer).toHaveClass('text-blue-500'); }); it('应渲染 badge 组件', () => { @@ -58,28 +57,28 @@ describe('PageHeader 组件系统', () => { expect(screen.getByTestId('test-badge')).toBeInTheDocument(); }); - it('应支持自定义 iconSx', () => { - render(); - const iconContainer = screen.getByTestId('test-icon').parentElement; - expect(iconContainer).toHaveStyle('border-radius: 8px'); + it('应支持自定义 iconClassName', () => { + render(); + const iconContainer = screen.getByTestId('test-icon').parentElement?.parentElement; + expect(iconContainer).toHaveClass('custom-icon-class'); }); - it('应支持自定义 titleSx', () => { - render(); + it('应支持自定义 titleClassName', () => { + render(); const title = screen.getByText('时间戳转换'); - expect(title).toHaveStyle('font-size: 1.2rem'); + expect(title).toHaveClass('custom-title-class'); }); - it('应支持自定义 subtitleSx', () => { - render(); + it('应支持自定义 subtitleClassName', () => { + render(); const subtitle = screen.getByText('Unix 毫秒数转换与格式化'); - expect(subtitle).toHaveStyle('color: rgb(255, 0, 0)'); + expect(subtitle).toHaveClass('custom-subtitle-class'); }); - it('应支持自定义 sx', () => { - const { container } = render(); + it('应支持自定义 className', () => { + const { container } = render(); const outerElement = container.firstChild; - expect(outerElement).toHaveStyle('margin-bottom: 2rem'); + expect(outerElement).toHaveClass('custom-page-header'); }); it('无副标题时不渲染副标题区域', () => {