From b3dd7146ad9e2f907507b8263ae5c6622e141335 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=A8=E9=9C=96=E9=93=83?= Date: Fri, 22 May 2026 20:51:13 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=20CopyButton=20?= =?UTF-8?q?=E4=B8=8E=20RouterContainer=20=E7=BB=84=E4=BB=B6=EF=BC=8C?= =?UTF-8?q?=E7=A7=BB=E9=99=A4=20GlobalSnackbar=20=E5=B9=B6=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E4=BD=BF=E7=94=A8=20sonner=20toast=20=E5=92=8C=20cn?= =?UTF-8?q?=20=E5=B7=A5=E5=85=B7=E5=87=BD=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/CopyButton.tsx | 108 ++++++++++------------ components/RouterContainer.tsx | 36 +++++++- pages/QrCode/components/GeneratePanel.tsx | 3 - pages/QrCode/components/ParsePanel.tsx | 3 +- 4 files changed, 81 insertions(+), 69 deletions(-) diff --git a/components/CopyButton.tsx b/components/CopyButton.tsx index 7cc6bd9..77c89b9 100644 --- a/components/CopyButton.tsx +++ b/components/CopyButton.tsx @@ -1,43 +1,25 @@ import React, { useEffect, useRef, useState } from 'react'; -import { Copy, Check } from 'lucide-react'; +import { Check, Copy } from 'lucide-react'; import { copyTextToClipboard } from '@/utils/clipboard'; -import type { SnackbarOptions } from '@/components/GlobalSnackbar'; +import { cn } from '@/lib/utils'; // 1. 必须使用 cn 工具函数 +import { toast } from 'sonner'; // 2. 推荐使用 shadcn 默认的全局 toast -/** - * 复制按钮组件属性 - * @param text 要复制的文本 - * @param tooltip 提示信息 - * @param size 按钮大小 - * @param color 按钮颜色 - * @param style 自定义样式 - * @param showMessage 消息提示函数,用于显示复制成功或失败的消息 - */ -interface CopyButtonProps { +// 3. 继承原生按钮属性,允许外部自由扩展 className、variant 等 +interface CopyButtonProps extends React.ButtonHTMLAttributes { text: string; tooltip?: string; size?: 'small' | 'medium' | 'large'; - color?: 'primary' | 'secondary' | 'success' | 'error' | 'info' | 'warning' | string; - style?: React.CSSProperties; - showMessage?: (message: string, options?: SnackbarOptions) => void; + // 移除复杂的自定义颜色变体,交由 Tailwind 类名或 shadcn 的 variant 解决 + variant?: 'default' | 'secondary' | 'ghost' | 'outline'; } -/** - * 复制按钮组件 - * @param text 要复制的文本 - * @param tooltip 提示信息 - * @param size 按钮大小 - * @param color 按钮颜色 - * @param style 自定义样式 - * @param showMessage 消息提示函数,用于显示复制成功或失败的消息 - * @returns 复制按钮组件 - */ export const CopyButton: React.FC = ({ text, tooltip = '复制', size = 'small', - color = 'primary', - style, - showMessage, + variant = 'ghost', + className, + ...props }) => { const [copied, setCopied] = useState(false); const timerRef = useRef | null>(null); @@ -48,55 +30,61 @@ export const CopyButton: React.FC = ({ }; }, []); - const handleCopy = async () => { - if (text) { - const success = await copyTextToClipboard(text); - if (success) { - showMessage?.('复制成功', { severity: 'success' }); - setCopied(true); - if (timerRef.current) clearTimeout(timerRef.current); - timerRef.current = setTimeout(() => setCopied(false), 1500); - } else { - showMessage?.('复制失败', { severity: 'error' }); - } + const handleCopy = async (e: React.MouseEvent) => { + e.stopPropagation(); // 基础组件防冒泡,避免触发父级点击事件 + + if (!text) { + toast.error('无内容可复制'); + return; + } + + const success = await copyTextToClipboard(text); + if (success) { + toast.success('复制成功'); + setCopied(true); + if (timerRef.current) clearTimeout(timerRef.current); + timerRef.current = setTimeout(() => setCopied(false), 1500); } else { - showMessage?.('无内容可复制', { severity: 'error' }); + toast.error('复制失败'); } }; + // 4. 将控制尺寸的类名标准化 const sizeClasses = { - small: 'h-8 w-8', - medium: 'h-10 w-10', - large: 'h-12 w-12', + small: 'h-8 w-8 text-xs', + medium: 'h-10 w-10 text-sm', + large: 'h-12 w-12 text-base', }; - const iconSize = size === 'small' ? 14 : size === 'medium' ? 16 : 18; - - const colorClasses: Record = { - primary: 'text-primary hover:bg-primary/10', - secondary: 'text-foreground hover:bg-muted', - success: 'text-green-600 hover:bg-green-500/10', - error: 'text-red-600 hover:bg-red-500/10', - info: 'text-primary hover:bg-primary/10', - warning: 'text-amber-600 hover:bg-amber-50', + // 5. 映射 shadcn 的底层通用 Variant 类名 + const variantClasses = { + default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90', + secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80', + ghost: 'hover:bg-accent hover:text-accent-foreground', + outline: + 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground', }; - const colorClass = colorClasses[color] || `text-[${color}] hover:bg-muted`; - return ( ); diff --git a/components/RouterContainer.tsx b/components/RouterContainer.tsx index 97f9277..fbcb3ce 100644 --- a/components/RouterContainer.tsx +++ b/components/RouterContainer.tsx @@ -3,10 +3,13 @@ import { useRouter } from '@/providers/RouterProvider'; import { Suspense, useMemo } from 'react'; import PageErrorBoundary from '@/components/PageErrorBoundary'; import PageSkeleton from '@/components/PageSkeleton'; +import { cn } from '@/lib/utils'; // 1. 引入标准的 shadcn 工具函数 +import { AlertTriangle } from 'lucide-react'; // 用于标准的 404 异常展示 export default function RouterContainer() { const { currentPage, isLoaded } = useRouter(); + // 2. 稳定的动态动画类名映射 const animationClass = useMemo(() => { return currentPage === 'dashboard' ? 'page-transition-dashboard' : 'page-transition-enter'; }, [currentPage]); @@ -15,22 +18,47 @@ export default function RouterContainer() { return getEntryPointType(); }, []); + // 骨架屏加载状态守卫 if (!isLoaded) { return ; } + // 3. 严格的路由查找与类型安全的组件分发 const currentFeature = FEATURES.find((f) => f.key === currentPage); - const Component = currentFeature ? currentFeature.components[entryPointType] : null; + const MatchedComponent = currentFeature?.components?.[entryPointType]; return (
} > - {Component && } + + {/* + 4. 路由防御拦截: + 如果组件存在则正常流式渲染,如果由于版本更迭或非法路径导致找不到对应组件, + 渲染一个优雅且符合 shadcn 风格的中性 404 提示页,而不是死白屏。 + */} + {MatchedComponent ? ( + + ) : ( +
+
+ +
+

页面未找到

+

+ 该功能在当前运行环境({entryPointType})下不可用或已被移除。 +

+
+ )} +
); diff --git a/pages/QrCode/components/GeneratePanel.tsx b/pages/QrCode/components/GeneratePanel.tsx index 3e7e29a..b705f9c 100644 --- a/pages/QrCode/components/GeneratePanel.tsx +++ b/pages/QrCode/components/GeneratePanel.tsx @@ -1,12 +1,10 @@ import TextInputArea from '@/components/TextInputArea'; import QrCodePreview from '@/components/QrCodePreview'; -import { useSnackbar } from '@/components/GlobalSnackbar'; import { useLazyTranslation } from '@/utils/useLazyTranslation'; import { useQrCodeContext } from '../contexts/QrCodeContext'; export default function GeneratePanel() { const { t } = useLazyTranslation('qrCode'); - const { showMessage } = useSnackbar(); const { generatorState, setTextToEncode, downloadQrCode, copyQrCode } = useQrCodeContext(); return ( @@ -21,7 +19,6 @@ export default function GeneratePanel() { showClear allowCopy externalError={generatorState.inputError} - showMessage={showMessage} />
diff --git a/pages/QrCode/components/ParsePanel.tsx b/pages/QrCode/components/ParsePanel.tsx index c8d6e57..1a7243b 100644 --- a/pages/QrCode/components/ParsePanel.tsx +++ b/pages/QrCode/components/ParsePanel.tsx @@ -1,4 +1,4 @@ -import { useEffect, useCallback } from 'react'; +import { useCallback, useEffect } from 'react'; import TextInputArea from '@/components/TextInputArea'; import ImageUploader from '@/components/ImageUploader'; import { useSnackbar } from '@/components/GlobalSnackbar'; @@ -77,7 +77,6 @@ export default function ParsePanel() { allowCopy placeholder="" externalError={parserState.parseError} - showMessage={showMessage} />