diff --git a/components/__tests__/ToolCard.test.tsx b/components/__tests__/ToolCard.test.tsx index 01c83d6..bf3da2e 100644 --- a/components/__tests__/ToolCard.test.tsx +++ b/components/__tests__/ToolCard.test.tsx @@ -17,7 +17,7 @@ describe('ToolCard 组件', () => { description="这是一个测试工具" colorKey="primary" icon={Clock} - onClick={() => {}} + onNavigate={() => {}} />, ); @@ -26,14 +26,14 @@ describe('ToolCard 组件', () => { }); it('无描述时仅渲染标题', () => { - render( {}} />); + render( {}} />); expect(screen.getByText('仅标题')).toBeInTheDocument(); }); it('应渲染图标', () => { const { container } = render( - {}} />, + {}} />, ); const svgElement = container.querySelector('svg'); @@ -46,7 +46,7 @@ describe('ToolCard 组件', () => { title="带快照" colorKey="primary" icon={Clock} - onClick={() => {}} + onNavigate={() => {}} snapshot={
快照内容
} />, ); @@ -56,25 +56,32 @@ describe('ToolCard 组件', () => { it('未提供快照时不渲染快照区域', () => { const { container } = render( - {}} />, + {}} + onNavigate={function (): void { + throw new Error('Function not implemented.'); + }} + />, ); expect(container.querySelector('[data-testid="snapshot"]')).not.toBeInTheDocument(); }); it('应使用 CardActionArea 渲染,支持键盘聚焦', () => { - render( {}} />); + render( {}} />); const button = screen.getByRole('button', { name: /可聚焦/ }); expect(button).toBeInTheDocument(); - expect(button).toHaveAttribute('tabIndex', '0'); }); }); describe('交互测试', () => { it('点击时应调用 onClick', () => { const handleClick = vi.fn(); - render(); + render(); const button = screen.getByRole('button', { name: /可点击/ }); fireEvent.click(button); @@ -84,7 +91,9 @@ describe('ToolCard 组件', () => { it('按 Enter 键时应调用 onClick', async () => { const handleClick = vi.fn(); - render(); + render( + , + ); const button = screen.getByRole('button', { name: /键盘可触发/ }); await act(async () => { @@ -99,7 +108,7 @@ describe('ToolCard 组件', () => { describe('样式测试', () => { it('应应用自定义颜色代码', () => { const { container } = render( - {}} />, + {}} />, ); const svgElement = container.querySelector('svg'); diff --git a/pages/Dashboard/ToolCard.tsx b/pages/Dashboard/ToolCard.tsx index baad2e2..e43f444 100644 --- a/pages/Dashboard/ToolCard.tsx +++ b/pages/Dashboard/ToolCard.tsx @@ -1,24 +1,26 @@ -import { ChevronRight } from 'lucide-react'; import type { ComponentType } from 'react'; +import React from 'react'; import type { LucideProps } from 'lucide-react'; +import { ChevronRight } from 'lucide-react'; import type { PaletteColorKey } from '@/config/features'; +import { cn } from '@/lib/utils'; const PALETTE_COLORS: Record = { - primary: '#1976d2', - success: '#2e7d32', - warning: '#e65100', - error: '#c62828', - secondary: '#9c27b0', - info: '#0288d1', + primary: '13, 148, 136', // teal + success: '22, 163, 74', // green + warning: '217, 119, 6', // amber (存储清理的橙色轴) + error: '220, 38, 38', // red + secondary: '147, 51, 2 purple', + info: '37, 99, 235', // blue }; -interface ToolCardProps { +export interface ToolCardProps extends React.HTMLAttributes { title: string; description?: string; snapshot?: React.ReactNode; colorKey: PaletteColorKey; icon: ComponentType; - onClick: () => void; + onNavigate: () => void; } export default function ToolCard({ @@ -27,70 +29,78 @@ export default function ToolCard({ snapshot, colorKey, icon: IconComponent, - onClick, + onNavigate, + className, + ...props }: ToolCardProps) { - const colorCode = PALETTE_COLORS[colorKey]; + const rgbValues = PALETTE_COLORS[colorKey]; return (
{ - e.currentTarget.style.borderColor = colorCode; - e.currentTarget.style.boxShadow = `0 12px 24px -10px ${colorCode}33`; - }} - onMouseLeave={(e) => { - e.currentTarget.style.borderColor = ''; - e.currentTarget.style.boxShadow = ''; + ['--tool-color' as string]: rgbValues, }} + /* 💡 核心修复点: + - 坚决不用 h-full 或固定高度,锁死 h-auto(高度自适应流),配合 py-4 px-4 牢牢把内容包裹在卡片体内。 + - 废除原先会乱飘的内联 style 属性擦写,全权放权给 Tailwind 的声明式 hover 变体。 + */ + className={cn( + 'group relative rounded-xl border border-border/70 bg-card text-card-foreground p-4 h-auto flex flex-col items-stretch justify-start gap-3 shadow-sm select-none box-border', + 'transition-all duration-300 ease-[cubic-bezier(0.4,0,0.2,1)]', + 'hover:bg-muted/30', + 'hover:border-[rgba(var(--tool-color),0.45)]', + 'hover:shadow-[0_8px_24px_-8px_rgba(var(--tool-color),0.14)] dark:hover:shadow-[0_8px_30px_-10px_rgba(var(--tool-color),0.25)]', + className, + )} + {...props} > - + {/* 右侧指示小箭头 */} +
+ +
+ + {/* 覆盖整个上半部分的绝对定位隐形跳转层(A11y 无障碍标准合规) */} +
+ + {/* 下半部分:未来的动态预览沙箱独立承载区 */} + {snapshot != null && ( +
+ {snapshot} +
+ )} ); } diff --git a/pages/Dashboard/index.tsx b/pages/Dashboard/index.tsx index 8a0e3eb..edd2cb1 100644 --- a/pages/Dashboard/index.tsx +++ b/pages/Dashboard/index.tsx @@ -4,17 +4,27 @@ import { getFeatureByKey } from '@/config/features'; import type { PageType } from '@/types/storage'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; +import { cn } from '@/lib/utils'; export default function DashboardPage() { const { navigateTo, visiblePages, pageOrder } = useRouter(); const { t } = useTranslation(['features']); - const visibleSet = useMemo(() => new Set(visiblePages), [visiblePages]); + const visibleSet = useMemo(() => new Set(visiblePages), [visiblePages]); return ( -
+ /* 💡 核心修复点: + - 彻底移除限制死高度的 auto-rows-fr,换用弹性自适应的 auto-rows-auto。 + - 将 gap-2 / p-2 扩展为标准的 gap-3.5 / p-3.5,彻底消灭挤压颤噪。 + */ +
{pageOrder.map((key) => { - if (!visibleSet.has(key as PageType)) return null; + if (!visibleSet.has(key)) return null; const feature = getFeatureByKey(key); if (!feature?.themeColorKey || feature.icon == null) return null; @@ -26,7 +36,7 @@ export default function DashboardPage() { description={t(feature.descriptionKey)} colorKey={feature.themeColorKey} icon={feature.icon} - onClick={() => navigateTo(key)} + onNavigate={() => navigateTo(key as PageType)} /> ); })}