diff --git a/components/Button.tsx b/components/Button.tsx deleted file mode 100644 index 1b42d61..0000000 --- a/components/Button.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { Button as MuiButton, ButtonProps as MuiButtonProps } from '@mui/material'; - -/** - * 按钮属性类型 - * 继承自 MUI ButtonProps,支持所有 MUI Button 的属性 - */ -export type ButtonProps = MuiButtonProps; - -/** - * Button - 自定义按钮组件 - * - * 基于 MUI Button 的二次封装,提供统一的项目风格: - * - 禁用阴影和涟漪效果 - * - 圆角设计 (borderRadius: 4) - * - 固定高度和字体大小 - * - hover 时轻微上浮效果 - * - 支持 sx 数组合并 - * - * @example - * ```tsx - * - * ``` - * - * @param sx - 自定义样式,支持数组或单个样式对象 - * @param props - 其他 MUI Button 属性 - * @returns 按钮组件 - */ -export function Button({ sx = [], ...props }: ButtonProps) { - return ( - - ); -} - -export default Button; diff --git a/components/CopyButton.tsx b/components/CopyButton.tsx index 2e77700..e03791d 100644 --- a/components/CopyButton.tsx +++ b/components/CopyButton.tsx @@ -1,7 +1,5 @@ import React, { useEffect, useRef, useState } from 'react'; -import { IconButton, Tooltip } from '@mui/material'; -import ContentCopyIcon from '@mui/icons-material/ContentCopy'; -import CheckIcon from '@mui/icons-material/Check'; +import { Copy, Check } from 'lucide-react'; import { copyTextToClipboard } from '@/utils/clipboard'; import type { SnackbarOptions } from '@/components/GlobalSnackbar'; @@ -66,34 +64,41 @@ export const CopyButton: React.FC = ({ } }; + const sizeClasses = { + small: 'h-8 w-8', + medium: 'h-10 w-10', + large: 'h-12 w-12', + }; + + const iconSize = size === 'small' ? 14 : size === 'medium' ? 16 : 18; + + const colorClasses: Record = { + primary: 'text-blue-600 hover:bg-blue-50', + secondary: 'text-gray-600 hover:bg-gray-50', + success: 'text-green-600 hover:bg-green-50', + error: 'text-red-600 hover:bg-red-50', + info: 'text-blue-600 hover:bg-blue-50', + warning: 'text-amber-600 hover:bg-amber-50', + }; + + const colorClass = colorClasses[color] || `text-[${color}] hover:bg-gray-50`; + return ( - - - `0 2px 8px ${theme.palette.mode === 'dark' ? 'rgba(0,0,0,0.3)' : 'rgba(0,0,0,0.05)'}`, - '&:hover': { - bgcolor: copied - ? 'success.main' - : !['primary', 'secondary', 'success', 'error', 'info', 'warning'].includes(color) - ? color - : `${color}.main`, - color: 'background.paper', - }, - }} - > - {copied ? ( - - ) : ( - - )} - - + ); }; diff --git a/components/DecodeResultPaper.tsx b/components/DecodeResultPaper.tsx index a668307..1673f18 100644 --- a/components/DecodeResultPaper.tsx +++ b/components/DecodeResultPaper.tsx @@ -2,13 +2,13 @@ * DecodeResultPaper * * FileMode 与 ImageMode 通用的 decode 结果展示组件。 - * 提取了二者 decode 输出区完全一致的 Paper 结构: + * 提取了二者 decode 输出区完全一致的结构: * 标题 → 可选预览(children)→ 文件信息 → 文件名输入 → 下载按钮 * * FileMode 直接使用,ImageMode 通过 children 传入图片预览。 */ -import { alpha, Button, Paper, Stack, TextField, Typography } from '@mui/material'; -import DownloadIcon from '@mui/icons-material/Download'; +import { Download } from 'lucide-react'; +import { Button } from '@/components/ui/button'; import { formatFileSize } from '@/utils/base64Converter'; import { useTranslation } from 'react-i18next'; @@ -41,59 +41,46 @@ export default function DecodeResultPaper({ const { t } = useTranslation('base64Converter'); return ( - alpha(theme.palette.info.main, 0.04), - border: '1px solid', - borderColor: (theme) => alpha(theme.palette.info.main, 0.15), - }} - > +
{/* 标题 */} - - {title} - + {title} {/* 可选预览内容(ImageMode 的图片) */} {children} {/* 文件信息 */} - - +
+ {t('inferredMimeType')}: {mimeType} - - + + {t('decodedSize')}: {formatFileSize(blobSize)} - - + +
{/* 文件名输入 */} - onFileNameChange(e.target.value)} - sx={{ mb: 1.5 }} - /> +
+ + onFileNameChange(e.target.value)} + className="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
{/* 下载按钮 */} - +
); } diff --git a/components/ErrorBoundary.tsx b/components/ErrorBoundary.tsx index c319a50..8966697 100644 --- a/components/ErrorBoundary.tsx +++ b/components/ErrorBoundary.tsx @@ -1,8 +1,6 @@ import { Component, ErrorInfo, ReactNode } from 'react'; -import { Box, Button, Container, Paper, Typography } from '@mui/material'; -import type { Theme } from '@mui/material/styles'; -import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; -import RefreshIcon from '@mui/icons-material/Refresh'; +import { AlertCircle, RefreshCw } from 'lucide-react'; +import { Button } from '@/components/ui/button'; interface Props { children: ReactNode; @@ -43,63 +41,30 @@ export class ErrorBoundary extends Component { render() { if (this.state.hasError) { return ( - - - - - 糟糕,出了点问题 - - +
+
+ +

糟糕,出了点问题

+

应用遇到了一些意外错误。您可以尝试刷新页面或重置应用。 - +

{this.state.error && ( - - theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.05)' : 'grey.100', - borderRadius: 2, - textAlign: 'left', - maxHeight: '200px', - overflow: 'auto', - }} - > - +
+
                   {this.state.error.toString()}
-                
-              
+                
+
)} - - +
+
); } diff --git a/components/GlobalSnackbar.tsx b/components/GlobalSnackbar.tsx index 1622054..37b28b9 100644 --- a/components/GlobalSnackbar.tsx +++ b/components/GlobalSnackbar.tsx @@ -38,12 +38,14 @@ import { JSX, useState, + useRef, createContext, useContext, + useEffect, type ReactNode, type SyntheticEvent, } from 'react'; -import { Snackbar, Alert, type SxProps, type Theme, alpha, Portal } from '@mui/material'; +import { CheckCircle, Info, AlertTriangle, XCircle } from 'lucide-react'; /** * Snackbar 消息严重程度类型 @@ -80,9 +82,9 @@ export interface GlobalSnackbarProps { /** 是否隐藏 Alert 图标,默认 false */ hideIcon?: boolean; /** 自定义样式,透传给外层 Snackbar 组件 */ - sx?: SxProps; + sx?: React.CSSProperties; /** 自定义样式,透传给内层 Alert 组件(仅 showAlert=true 时生效) */ - alertSx?: SxProps; + alertSx?: React.CSSProperties; } /** @@ -130,11 +132,20 @@ const defaultProps: Required< hideIcon: false, }; +const severityConfig: Record< + SnackbarSeverity, + { icon: React.ElementType; bgClass: string; textClass: string } +> = { + success: { icon: CheckCircle, bgClass: 'bg-green-500', textClass: 'text-white' }, + info: { icon: Info, bgClass: 'bg-blue-500', textClass: 'text-white' }, + warning: { icon: AlertTriangle, bgClass: 'bg-amber-500', textClass: 'text-white' }, + error: { icon: XCircle, bgClass: 'bg-red-500', textClass: 'text-white' }, +}; + /** * GlobalSnackbar 组件 * * 全局消息提示的展示组件,支持受控和非受控两种使用模式。 - * 使用 MUI Snackbar 和 Alert 组件实现消息提示功能。 * * @param {GlobalSnackbarProps} props - 组件属性 * @returns {JSX.Element} @@ -145,55 +156,42 @@ export function GlobalSnackbar({ onClose, severity = defaultProps.severity, autoHideDuration = defaultProps.autoHideDuration, - anchorOrigin = defaultProps.anchorOrigin, showAlert = defaultProps.showAlert, hideIcon = defaultProps.hideIcon, -}: GlobalSnackbarProps): JSX.Element { +}: GlobalSnackbarProps): JSX.Element | null { + const timerRef = useRef | null>(null); + + useEffect(() => { + if (open && autoHideDuration > 0) { + timerRef.current = setTimeout(() => { + onClose(); + }, autoHideDuration); + return () => { + if (timerRef.current) clearTimeout(timerRef.current); + }; + } + return undefined; + }, [open, autoHideDuration, onClose]); + + if (!open) return null; + + const config = severityConfig[severity]; + const IconComponent = config.icon; + return ( - - - {showAlert ? ( - - `0 12px 32px ${alpha(theme.palette[severity].main, 0.35)}`, - '& .MuiAlert-icon': { mr: 0.5, fontSize: '1.1rem' }, - '& .MuiAlert-message': { padding: '6px 0' }, - }} - > - {message} - - ) : ( -
{message}
- )} -
-
+
+ {showAlert ? ( +
+ {!hideIcon && } + {message} +
+ ) : ( +
{message}
+ )} +
); } diff --git a/components/PageErrorBoundary.tsx b/components/PageErrorBoundary.tsx index 4e6c592..143e88d 100644 --- a/components/PageErrorBoundary.tsx +++ b/components/PageErrorBoundary.tsx @@ -1,8 +1,6 @@ import { Component, ErrorInfo, ReactNode } from 'react'; -import { Box, Button, Paper, Typography } from '@mui/material'; -import type { Theme } from '@mui/material/styles'; -import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; -import RefreshIcon from '@mui/icons-material/Refresh'; +import { AlertCircle, RefreshCw } from 'lucide-react'; +import { Button } from '@/components/ui/button'; interface Props { children: ReactNode; @@ -45,75 +43,30 @@ export class PageErrorBoundary extends Component { render() { if (this.state.hasError) { return ( - - - - - 该页面加载失败 - - +
+
+ +

该页面加载失败

+

页面在加载或渲染时遇到错误,您可以重试或切换到其他工具。 - +

{this.state.error && ( - - theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.05)' : 'grey.100', - borderRadius: 2, - textAlign: 'left', - maxHeight: '160px', - overflow: 'auto', - }} - > - +
+
                   {this.state.error.toString()}
-                
-              
+                
+
)} - -
+
+
); } diff --git a/components/PageHeader.tsx b/components/PageHeader.tsx index b08050d..7a14865 100644 --- a/components/PageHeader.tsx +++ b/components/PageHeader.tsx @@ -1,4 +1,3 @@ -import { alpha, Box, Stack, SxProps, Theme, Typography, useTheme } from '@mui/material'; import { ReactNode, useMemo } from 'react'; import { getEntryPointType } from '@/config/features'; @@ -17,13 +16,13 @@ export interface PageHeaderProps { /** 在标题右侧显示的徽章/标签组件(可选) */ badge?: ReactNode; /** 图标容器的自定义样式 */ - iconSx?: SxProps; + iconSx?: React.CSSProperties; /** 标题文本的自定义样式 */ - titleSx?: SxProps; + titleSx?: React.CSSProperties; /** 副标题文本的自定义样式 */ - subtitleSx?: SxProps; + subtitleSx?: React.CSSProperties; /** 整个组件的自定义样式 */ - sx?: SxProps; + sx?: React.CSSProperties; } /** @@ -54,7 +53,7 @@ export interface PageHeaderProps { */ export default function PageHeader({ icon, - iconColor, + iconColor = '#3b82f6', title, subtitle, badge, @@ -63,8 +62,6 @@ export default function PageHeader({ subtitleSx, sx, }: PageHeaderProps) { - const theme = useTheme(); - const resolvedIconColor = iconColor ?? theme.palette.primary.main; const entryPointType = useMemo(() => getEntryPointType(), []); if (entryPointType === 'popup') { @@ -72,44 +69,34 @@ export default function PageHeader({ } return ( - +
{/* 图标容器 */} - {icon} - +
{/* 标题区域 */} - +
{/* 标题行(含徽章) */} - - +
+ {title} - + {badge} - +
{/* 副标题 */} {subtitle && ( - + {subtitle} - + )} - -
+
+ ); } diff --git a/components/PageSkeleton.tsx b/components/PageSkeleton.tsx index 6f6b44a..65c2239 100644 --- a/components/PageSkeleton.tsx +++ b/components/PageSkeleton.tsx @@ -4,9 +4,6 @@ * 用于 Suspense fallback 和初始加载状态,提供平滑的视觉过渡 * 避免白屏闪烁,减少布局偏移 */ -import { Box, Skeleton, Stack, useTheme } from '@mui/material'; -import { alpha } from '@mui/material'; - interface PageSkeletonProps { /** 骨架屏类型 */ variant?: 'dashboard' | 'tool'; @@ -16,30 +13,19 @@ interface PageSkeletonProps { * 仪表盘卡片骨架屏 */ function DashboardCardSkeleton() { - const theme = useTheme(); - const borderColor = alpha(theme.palette.divider, 0.5); - return ( - - - - - - - - - - - - +
+
+
+
+
+
+
+
+
+
+
+
); } @@ -48,24 +34,24 @@ function DashboardCardSkeleton() { */ function ToolPageSkeleton() { return ( - +
{/* 标题区域 */} - +
{/* 输入区域 */} - +
{/* 控制栏 */} - - - - - - +
+
+
+
+
+
{/* 结果区域 */} - - +
+
); } @@ -81,22 +67,11 @@ export default function PageSkeleton({ variant = 'dashboard' }: PageSkeletonProp } return ( - +
{Array.from({ length: 6 }).map((_, index) => ( ))} - +
); } diff --git a/components/RouterContainer.tsx b/components/RouterContainer.tsx index d7a71be..97f9277 100644 --- a/components/RouterContainer.tsx +++ b/components/RouterContainer.tsx @@ -1,4 +1,3 @@ -import { Box } from '@mui/material'; import { FEATURES, getEntryPointType } from '@/config/features'; import { useRouter } from '@/providers/RouterProvider'; import { Suspense, useMemo } from 'react'; @@ -24,23 +23,15 @@ export default function RouterContainer() { const Component = currentFeature ? currentFeature.components[entryPointType] : null; return ( - } > {Component && } - +
); } diff --git a/components/SwitchButtonGroup.tsx b/components/SwitchButtonGroup.tsx index 66df814..0ff653c 100644 --- a/components/SwitchButtonGroup.tsx +++ b/components/SwitchButtonGroup.tsx @@ -1,5 +1,3 @@ -import { ToggleButton, ToggleButtonGroup, type SxProps, type Theme } from '@mui/material'; - export interface SwitchOption { value: T; label: React.ReactNode; @@ -9,9 +7,9 @@ export interface SwitchButtonGroupProps { value: T; options: SwitchOption[]; onChange: (value: T) => void; - sx?: SxProps; + sx?: React.CSSProperties; size?: 'small' | 'medium' | 'large'; - buttonSx?: SxProps; + buttonSx?: React.CSSProperties; } export default function SwitchButtonGroup({ @@ -19,53 +17,37 @@ export default function SwitchButtonGroup({ options, onChange, sx, - size, + size = 'medium', buttonSx, }: SwitchButtonGroupProps) { + const sizeClasses = { + small: 'text-xs', + medium: 'text-sm', + large: 'text-base', + }; + return ( - v && onChange(v)} - sx={{ - width: '100%', - mb: 2, - borderRadius: 4, - bgcolor: (theme: Theme) => (theme.palette.mode === 'light' ? 'grey.100' : 'grey.900'), - border: '1px solid', - borderColor: 'divider', - p: 0.6, - '& .MuiToggleButtonGroup-grouped': { - flex: 1, - border: 'none', - borderRadius: 3.5, - mx: 0.3, - fontWeight: 800, - color: 'text.secondary', - transition: 'color 0.3s', - '&:not(:first-of-type)': { - borderLeft: 'none', - marginLeft: 0.6, - }, - '&.Mui-selected': { - bgcolor: 'background.paper', - color: 'primary.main', - boxShadow: '0 4px 12px rgba(0,0,0,0.05)', - }, - }, - ...sx, - }} +
{options.map((option) => ( - onChange(option.value)} + className={`flex-1 px-3 py-1.5 rounded-lg font-bold whitespace-nowrap transition-all ${ + sizeClasses[size] + } ${ + value === option.value + ? 'bg-white text-blue-600 shadow-sm' + : 'text-gray-500 hover:text-gray-700 hover:bg-white/50' + }`} + style={buttonSx} > {option.label} - + ))} - +
); } diff --git a/components/TextInputArea.tsx b/components/TextInputArea.tsx index d11ce5e..ec5973f 100644 --- a/components/TextInputArea.tsx +++ b/components/TextInputArea.tsx @@ -30,19 +30,7 @@ */ import { useRef, useState, useCallback, forwardRef, RefObject } from 'react'; -import { - Box, - Button, - IconButton, - TextField, - Tooltip, - Typography, - alpha, - type SxProps, -} from '@mui/material'; -import type { Theme } from '@mui/material/styles'; -import CloseIcon from '@mui/icons-material/Close'; -import ContentCopyIcon from '@mui/icons-material/ContentCopy'; +import { X, Copy } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import type { SnackbarOptions } from '@/components/GlobalSnackbar'; @@ -99,7 +87,7 @@ export interface TextInputAreaProps { /** 外层容器样式 */ style?: React.CSSProperties; /** 外层容器 sx */ - sx?: SxProps; + sx?: React.CSSProperties; /** 是否显示字符计数 */ showCount?: boolean; @@ -155,7 +143,7 @@ function ActionButton({ action, value, globalDisabled, - variant = 'text', + variant: _variant = 'text', onAction, size = 'small', compact, @@ -163,42 +151,31 @@ function ActionButton({ const isBtnDisabled = typeof action.disabled === 'function' ? action.disabled(value) : action.disabled || !value; - const typeStyles: Record = {}; + const typeClasses: Record = { + primary: 'bg-blue-600 text-white hover:bg-blue-700', + danger: 'text-red-600 hover:bg-red-50', + default: 'text-gray-500 hover:bg-gray-50', + }; - if (action.type === 'primary') { - if (variant !== 'contained') { - typeStyles.bgcolor = 'primary.main'; - typeStyles.color = 'primary.contrastText'; - typeStyles['&:hover'] = { bgcolor: 'primary.dark' }; - } - } else if (action.type === 'danger') { - typeStyles.color = 'error.main'; - typeStyles['&:hover'] = { - bgcolor: (theme: Theme) => alpha(theme.palette.error.main, 0.08), - }; - } else { - typeStyles.color = 'text.secondary'; - typeStyles['&:hover'] = { - bgcolor: (theme: Theme) => alpha(theme.palette.grey[500], 0.1), - }; - } + const sizeClasses = { + small: 'text-xs px-2 py-1', + medium: 'text-sm px-3 py-1.5', + }; return ( - + ); } @@ -352,27 +329,15 @@ const TextInputArea = forwardRef((props const hasTopBar = title || showCount || topActions.length > 0 || topExtra; return ( - +
{hasTopBar && ( - - - {title && ( - - {title} - - )} +
+
+ {title && {title}} {topExtra} - +
- +
{topActions.map((action) => ( ((props /> ))} {showCount && ( - + {value.length} {maxLength ? ` / ${maxLength}` : ''} - + )} - - +
+
)} - - +