用 Tailwind CSS 重写 Dashboard/index.tsx 和 ToolCard.tsx
This commit is contained in:
+56
-112
@@ -4,36 +4,29 @@
|
||||
* 用于在仪表盘中展示各个工具功能的卡片组件,支持图标、标题、描述、
|
||||
* 快照内容展示,具备悬停动画效果。
|
||||
*/
|
||||
import { alpha, Box, Card, CardActionArea, Stack, Typography, useTheme } from '@mui/material';
|
||||
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
|
||||
import type { SvgIconProps } from '@mui/material/SvgIcon';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
import type { ComponentType } from 'react';
|
||||
import type { SvgIconProps } from '@mui/material/SvgIcon';
|
||||
import type { PaletteColorKey } from '@/config/features';
|
||||
|
||||
/**
|
||||
* ToolCard 组件属性接口
|
||||
*/
|
||||
const PALETTE_COLORS: Record<PaletteColorKey, string> = {
|
||||
primary: '#1976d2',
|
||||
success: '#2e7d32',
|
||||
warning: '#e65100',
|
||||
error: '#c62828',
|
||||
secondary: '#9c27b0',
|
||||
info: '#0288d1',
|
||||
};
|
||||
|
||||
interface ToolCardProps {
|
||||
/** 工具卡片标题 */
|
||||
title: string;
|
||||
/** 工具卡片描述文本(可选) */
|
||||
description?: string;
|
||||
/** 快照内容,用于在卡片底部展示额外信息(可选) */
|
||||
snapshot?: React.ReactNode;
|
||||
/** 主题色键,映射到 theme.palette[key].main */
|
||||
colorKey: PaletteColorKey;
|
||||
/** 图标组件引用 */
|
||||
icon: ComponentType<SvgIconProps>;
|
||||
/** 卡片点击事件处理函数 */
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* ToolCard 组件
|
||||
*
|
||||
* @param props - ToolCardProps 属性对象
|
||||
* @returns 工具卡片 JSX 元素
|
||||
*/
|
||||
export default function ToolCard({
|
||||
title,
|
||||
description,
|
||||
@@ -42,120 +35,71 @@ export default function ToolCard({
|
||||
icon: IconComponent,
|
||||
onClick,
|
||||
}: ToolCardProps) {
|
||||
const theme = useTheme();
|
||||
const colorCode = theme.palette[colorKey].main;
|
||||
const colorCode = PALETTE_COLORS[colorKey];
|
||||
|
||||
return (
|
||||
<Card
|
||||
elevation={0}
|
||||
sx={{
|
||||
position: 'relative',
|
||||
borderRadius: 4,
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
height: '100%',
|
||||
boxSizing: 'border-box',
|
||||
transition:
|
||||
'border-color 0.3s cubic-bezier(0.4, 0, 0.2, 1), transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
'&:hover': {
|
||||
borderColor: colorCode,
|
||||
transform: 'translateY(-4px)',
|
||||
boxShadow: `0 12px 24px -10px ${alpha(colorCode, 0.2)}`,
|
||||
},
|
||||
<div
|
||||
className="group relative rounded-2xl border border-gray-200 h-full box-border transition-all duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] hover:-translate-y-1"
|
||||
style={{
|
||||
borderColor: undefined,
|
||||
['--hover-color' as string]: colorCode,
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
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 = '';
|
||||
}}
|
||||
>
|
||||
<CardActionArea
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
onClick={onClick}
|
||||
sx={{
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'stretch',
|
||||
justifyContent: 'flex-start',
|
||||
p: 2.5,
|
||||
gap: 1.5,
|
||||
'&:hover .arrow-icon': {
|
||||
transform: 'translateX(4px)',
|
||||
color: colorCode,
|
||||
},
|
||||
}}
|
||||
className="h-full flex flex-col items-stretch justify-start p-5 gap-3 w-full text-left cursor-pointer bg-transparent border-none"
|
||||
>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="flex-start" width="100%">
|
||||
<Stack direction="row" spacing={1.5} alignItems="center">
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 3,
|
||||
bgcolor: alpha(colorCode, 0.07),
|
||||
<div className="flex justify-between items-start w-full">
|
||||
<div className="flex gap-3 items-center">
|
||||
<div
|
||||
className="flex items-center justify-center w-10 h-10 rounded-xl"
|
||||
style={{
|
||||
backgroundColor: `${colorCode}11`,
|
||||
color: colorCode,
|
||||
}}
|
||||
>
|
||||
<IconComponent sx={{ fontSize: 20 }} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
lineHeight: 1.2,
|
||||
color: 'text.primary',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.5,
|
||||
}}
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-bold text-sm leading-tight text-gray-900 flex items-center gap-1">
|
||||
{title}
|
||||
</Typography>
|
||||
</span>
|
||||
{description && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
color: 'text.secondary',
|
||||
fontWeight: 500,
|
||||
mt: 0.5,
|
||||
display: '-webkit-box',
|
||||
WebkitBoxOrient: 'vertical',
|
||||
WebkitLineClamp: 1,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
>
|
||||
<span className="block text-xs text-gray-500 font-medium mt-1 overflow-hidden text-ellipsis whitespace-nowrap max-w-[200px]">
|
||||
{description}
|
||||
</Typography>
|
||||
</span>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
<ArrowForwardIosIcon
|
||||
className="arrow-icon"
|
||||
sx={{
|
||||
fontSize: 12,
|
||||
color: 'text.disabled',
|
||||
mt: 0.5,
|
||||
transition: 'all 0.3s ease',
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRight
|
||||
className="w-3 h-3 text-gray-300 mt-1 transition-all duration-300 ease-in-out group-hover:translate-x-1"
|
||||
style={{ color: undefined }}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.color = colorCode;
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.color = '';
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</div>
|
||||
|
||||
{snapshot != null && (
|
||||
<Box
|
||||
sx={{
|
||||
mt: 'auto',
|
||||
pt: 1.5,
|
||||
borderTop: '1px dashed',
|
||||
borderColor: 'divider',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<div className="mt-auto pt-4 border-t border-dashed border-gray-200 w-full">
|
||||
{snapshot}
|
||||
</Box>
|
||||
</div>
|
||||
)}
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { Box } from '@mui/material';
|
||||
import { useRouter } from '@/providers/RouterProvider';
|
||||
import ToolCard from '@/pages/Dashboard/ToolCard';
|
||||
import { getFeatureByKey } from '@/config/features';
|
||||
import type { PageType } from '@/types/storage';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { dashboardPageStyles } from '@/config/pageTheme';
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { navigateTo, visiblePages, pageOrder } = useRouter();
|
||||
@@ -14,7 +12,7 @@ export default function DashboardPage() {
|
||||
const visibleSet = useMemo(() => new Set(visiblePages), [visiblePages]);
|
||||
|
||||
return (
|
||||
<Box sx={dashboardPageStyles.GRID_CONTAINER}>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-[repeat(auto-fill,minmax(300px,1fr))] auto-rows-fr gap-2 p-2">
|
||||
{pageOrder.map((key) => {
|
||||
if (!visibleSet.has(key as PageType)) return null;
|
||||
|
||||
@@ -32,6 +30,6 @@ export default function DashboardPage() {
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user