Develop (#54)
* fix(ci): fix release workflow artifact upload issue - Split artifact upload into 3 separate steps (Chrome, Firefox, Source) - Update download steps to match new artifact names - Add if-no-files-found: error for better error handling - Fix Node.js 20 compatibility issue with upload-artifact@v4 glob pattern * docs: 更新 AGENTS.md,添加测试工具和翻译文件结构说明 * docs: 更新 AGENTS.md,完善 CI 步骤和存储键名格式说明 * fix: remove unnecessary animations from StorageCleaner page - Remove entrance animations (animate-in, fade-in, zoom-in) from all components - Remove scale effect (active:scale-[0.99]) from clean button - Remove transition effects (transition-all, transition-colors) from OptionItem, StorageOptionsGrid, AutoRefreshToggle - Remove bouncing animation from error icon - Remove pulsing animation from warning banner - Keep animate-spin on button loading spinner as functional indicator * fix: remove unnecessary animations from RightClickRestorer page * fix: remove all unnecessary animations from all pages - Remove dead code animations (animate-in, fade-in, slide-in, zoom-in, shake) from tailwindcss-animate plugin (not installed) - Remove decorative transition effects (transition-all, transition-colors) from all page components - Remove scale effects (active:scale-95) from buttons - Remove bounce animations (animate-bounce) from icons - Keep functional animate-spin on loading spinners as they provide essential loading feedback Affected pages: Dashboard, Timestamp, Jwt, JsonTools, Base64Converter, HtmlToMarkdown, MarkdownToHtml, QrCode, TextStatistics * docs: 添加项目文档(copilot-instructions、编码规范、各目录 README) - 新增 .github/copilot-instructions.md:Copilot 指令文件,涵盖构建命令、CI 流水线、项目架构、关键规范 - 新增 .github/CODING_STANDARDS.md:完整的代码编写规范文档(TypeScript、React、样式、错误处理、命名、测试、Hook、i18n、存储等 12 个章节) - 新增 11 个目录的 README.md:components、components/ui、config、entrypoints、i18n、lib、pages、providers、public、src、types、utils - 更新 AGENTS.md:补充页面组件模式说明和 components/ui 目录 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(CopyButton): use shadcn buttonVariants instead of duplicated variant classes Remove hand-written variantClasses/sizeClasses that duplicated shadcn's buttonVariants. Now extends ButtonProps and imports buttonVariants from components/ui/button for consistent styling. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(ErrorBoundary): use shadcn destructive tokens instead of hardcoded red colors Replace hardcoded red-200/red-50/red-600 with border-destructive, bg-destructive/5, text-destructive. Use variant='destructive' on Button. Align error log styling with PageErrorBoundary. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(TextInputArea): replace hardcoded Chinese strings with i18n t() calls Use existing translation keys for clear success, copy success, and copy error messages instead of hardcoded Chinese text. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(QrCodePreview): use shadcn Button instead of manual button styles Replace hand-written button class strings with shadcn Button component using outline and default variants. Removes ~15 lines of duplicated Tailwind classes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(GlobalSnackbar): clarify relationship with sonner toast Add note explaining when to use GlobalSnackbar (provider context, severity levels, custom positioning) vs sonner toast (simple one-off messages). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(ImageUploader): wrap handleClearFile with useCallback Prevents unnecessary re-renders by memoizing the callback, consistent with handleFileChange which already uses useCallback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(SwitchButtonGroup): remove redundant containerPadding conditional Both branches of the ternary returned 'p-1'. Inline the constant directly into the className string. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(TopBar): add missing openExtensionPage import The function was called in handleOpenInTab but never imported from utils/chromeTabs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(TopBar): add selectedIndex boundary protection in keyboard nav Guard against out-of-bounds access when search results change during keyboard navigation. Check selectedIndex < totalItems before accessing arrays. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(RouterContainer): remove unnecessary empty-dep useMemo getEntryPointType() is a simple config getter. Replace useMemo(() => ..., []) with a direct call — the empty dep array made the memo pointless. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(i18n): add missing translation keys for error boundaries and copy messages Add keys for errorBoundary, pageErrorBoundary, router, and messages.copyEmpty in both zh and en locales. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(CopyButton): replace hardcoded Chinese strings with i18n t() calls Use useTranslation('common') for tooltip, copy empty/success/error toast messages. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(ErrorBoundary): replace hardcoded Chinese with i18n via withTranslation HOC Use react-i18next withTranslation HOC for class component to translate title, description, and refresh button text. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(PageErrorBoundary): replace hardcoded Chinese with i18n via withTranslation HOC Use react-i18next withTranslation HOC for class component to translate title, description, and retry button text. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(RouterContainer): replace hardcoded Chinese with i18n t() calls Use useTranslation('common') for 404 page title and description with entryPointType interpolation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(pages): 修复 TypeScript 错误、硬编码字符串,移除死代码 - 修复 LiveClock.tsx CopyButton size 属性类型错误 (small → sm) - 修复 ToolCard.tsx 紫色 RGB 值拼写错误 (147,51,2 purple → 147,51,232) - 替换 4 处硬编码中文为 i18n 调用 (StorageCleaner, JsonTools) - MarkdownToHtml 预览链接色改为 CSS 变量以支持暗黑模式 - 移除 Base64Converter 中已被 Base64ConverterSection 替代的死代码 (FileMode.tsx, ImageMode.tsx 及其测试文件) - 更新 README.md 移除对已删除文件的引用 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(CopyButton, LiveClock): 修改按钮大小为 'icon',移除硬编码颜色配置 * refactor(TextInputArea): 复用 CopyButton 组件替换内联复制逻辑 * fix(test): 修复 CopyButton mock 未捕获 clipboard writeText 异常 --------- Co-authored-by: Ubuntu <ubuntu@localhost.localdomain> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
+17
-37
@@ -1,26 +1,25 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Check, Copy } from 'lucide-react';
|
||||
import { copyTextToClipboard } from '@/utils/clipboard';
|
||||
import { cn } from '@/lib/utils'; // 1. 必须使用 cn 工具函数
|
||||
import { toast } from 'sonner'; // 2. 推荐使用 shadcn 默认的全局 toast
|
||||
import { cn } from '@/lib/utils';
|
||||
import { buttonVariants, type ButtonProps } from '@/components/ui/button';
|
||||
import { toast } from 'sonner';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
// 3. 继承原生按钮属性,允许外部自由扩展 className、variant 等
|
||||
interface CopyButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
interface CopyButtonProps extends Omit<ButtonProps, 'children' | 'onClick'> {
|
||||
text: string;
|
||||
tooltip?: string;
|
||||
size?: 'small' | 'medium' | 'large';
|
||||
// 移除复杂的自定义颜色变体,交由 Tailwind 类名或 shadcn 的 variant 解决
|
||||
variant?: 'default' | 'secondary' | 'ghost' | 'outline';
|
||||
}
|
||||
|
||||
export const CopyButton: React.FC<CopyButtonProps> = ({
|
||||
text,
|
||||
tooltip = '复制',
|
||||
size = 'small',
|
||||
tooltip,
|
||||
variant = 'ghost',
|
||||
size = 'icon',
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
const { t } = useTranslation('common');
|
||||
const [copied, setCopied] = useState(false);
|
||||
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
@@ -31,53 +30,34 @@ export const CopyButton: React.FC<CopyButtonProps> = ({
|
||||
}, []);
|
||||
|
||||
const handleCopy = async (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation(); // 基础组件防冒泡,避免触发父级点击事件
|
||||
e.stopPropagation();
|
||||
|
||||
if (!text) {
|
||||
toast.error('无内容可复制');
|
||||
toast.error(t('messages.copyEmpty'));
|
||||
return;
|
||||
}
|
||||
|
||||
const success = await copyTextToClipboard(text);
|
||||
if (success) {
|
||||
toast.success('复制成功');
|
||||
toast.success(t('messages.copySuccess'));
|
||||
setCopied(true);
|
||||
if (timerRef.current) clearTimeout(timerRef.current);
|
||||
timerRef.current = setTimeout(() => setCopied(false), 1500);
|
||||
} else {
|
||||
toast.error('复制失败');
|
||||
toast.error(t('messages.copyError'));
|
||||
}
|
||||
};
|
||||
|
||||
// 4. 将控制尺寸的类名标准化
|
||||
const sizeClasses = {
|
||||
small: 'h-8 w-8 text-xs',
|
||||
medium: 'h-10 w-10 text-sm',
|
||||
large: 'h-12 w-12 text-base',
|
||||
};
|
||||
|
||||
// 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',
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
title={tooltip}
|
||||
// 6. 使用 cn() 合并类名,并完美支持暗黑模式的语义化变量 (destructive/muted等)
|
||||
title={tooltip ?? t('buttons.copy')}
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
|
||||
sizeClasses[size],
|
||||
copied
|
||||
? 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400' // 兼顾暗黑模式的成功色
|
||||
: variantClasses[variant],
|
||||
className, // 允许外部直接传入 text-red-500 等覆盖样式
|
||||
buttonVariants({ variant, size }),
|
||||
copied &&
|
||||
'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 hover:bg-emerald-500/10',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Component, ErrorInfo, ReactNode } from 'react';
|
||||
import { withTranslation, type WithTranslation } from 'react-i18next';
|
||||
import { AlertCircle, RefreshCw } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
interface Props {
|
||||
interface Props extends WithTranslation {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
@@ -11,10 +12,7 @@ interface State {
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误边界组件:捕获子组件树中的 JavaScript 错误
|
||||
*/
|
||||
export class ErrorBoundary extends Component<Props, State> {
|
||||
class ErrorBoundaryBase extends Component<Props, State> {
|
||||
state: State = {
|
||||
hasError: false,
|
||||
error: null,
|
||||
@@ -39,29 +37,32 @@ export class ErrorBoundary extends Component<Props, State> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div className="mt-16 mx-auto max-w-md">
|
||||
<div className="p-6 text-center rounded-xl border border-red-200 bg-red-50">
|
||||
<AlertCircle className="h-16 w-16 text-red-500 mx-auto mb-4" />
|
||||
<h2 className="text-xl font-extrabold text-red-600 mb-2">糟糕,出了点问题</h2>
|
||||
<p className="text-sm text-muted-foreground mb-6">
|
||||
应用遇到了一些意外错误。您可以尝试刷新页面或重置应用。
|
||||
</p>
|
||||
<div className="flex flex-col items-center justify-center mt-16 mx-auto max-w-md">
|
||||
<div className="p-6 text-center rounded-xl border border-destructive/20 bg-destructive/5 shadow-sm">
|
||||
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-destructive/10 text-destructive mx-auto mb-4">
|
||||
<AlertCircle className="h-8 w-8" />
|
||||
</div>
|
||||
<h2 className="text-xl font-extrabold text-destructive mb-2">
|
||||
{t('errorBoundary.title')}
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground mb-6">{t('errorBoundary.description')}</p>
|
||||
{this.state.error && (
|
||||
<div className="mb-6 p-4 bg-muted rounded-lg text-left max-h-[200px] overflow-auto">
|
||||
<pre className="font-mono text-xs whitespace-pre-wrap break-all text-red-700">
|
||||
<div className="mb-6 p-4 rounded-lg bg-zinc-950 dark:bg-zinc-900 text-left max-h-[200px] overflow-auto border border-border/40">
|
||||
<pre className="font-mono text-xs whitespace-pre-wrap break-all text-zinc-200 selection:bg-zinc-700">
|
||||
{this.state.error.toString()}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
variant="default"
|
||||
variant="destructive"
|
||||
onClick={this.handleReset}
|
||||
className="rounded-lg font-bold bg-red-600 hover:bg-red-700 text-white"
|
||||
className="rounded-lg font-bold shadow-sm"
|
||||
>
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
刷新应用
|
||||
{t('errorBoundary.refresh')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -72,4 +73,5 @@ export class ErrorBoundary extends Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
export const ErrorBoundary = withTranslation('common')(ErrorBoundaryBase);
|
||||
export default ErrorBoundary;
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
* 2. 通过 useSnackbarState Hook 使用:在组件内部自动管理状态
|
||||
* 3. 通过 SnackbarProvider 和 useSnackbar Hook 使用:全局单例模式
|
||||
*
|
||||
* NOTE: 项目同时使用 sonner 的 toast 进行简单的一次性提示。
|
||||
* 本组件适用于需要 severity 级别、Provider 上下文、自定义定位等高级场景。
|
||||
* 简单场景(如复制成功、操作提示)优先使用 `import { toast } from 'sonner'`。
|
||||
*
|
||||
* @module GlobalSnackbar
|
||||
* @version 1.1.0
|
||||
*
|
||||
|
||||
@@ -41,7 +41,7 @@ const ImageUploader = ({
|
||||
[onFileChange, onPreviewUrlChange],
|
||||
);
|
||||
|
||||
const handleClearFile = () => {
|
||||
const handleClearFile = useCallback(() => {
|
||||
if (previewUrl) {
|
||||
URL.revokeObjectURL(previewUrl);
|
||||
}
|
||||
@@ -50,7 +50,7 @@ const ImageUploader = ({
|
||||
severity: 'success',
|
||||
autoHideDuration: 1000,
|
||||
});
|
||||
};
|
||||
}, [previewUrl, onClearFile, showMessage, t]);
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files && e.target.files.length > 0) {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Component, ErrorInfo, ReactNode } from 'react';
|
||||
import { withTranslation, type WithTranslation } from 'react-i18next';
|
||||
import { AlertCircle, RefreshCw } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
interface Props {
|
||||
interface Props extends WithTranslation {
|
||||
children: ReactNode;
|
||||
resetKey?: string | number;
|
||||
}
|
||||
@@ -12,11 +13,7 @@ interface State {
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面级错误边界组件:捕获子组件树中的 JavaScript 错误
|
||||
* 完美适配 shadcn/ui 语义化主题与暗黑模式
|
||||
*/
|
||||
export class PageErrorBoundary extends Component<Props, State> {
|
||||
class PageErrorBoundaryBase extends Component<Props, State> {
|
||||
state: State = {
|
||||
hasError: false,
|
||||
error: null,
|
||||
@@ -41,26 +38,22 @@ export class PageErrorBoundary extends Component<Props, State> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center flex-1 p-6 min-h-[300px] animate-in fade-in zoom-in-95 duration-200">
|
||||
{/*
|
||||
1. 适配暗黑模式的容器设计:
|
||||
不再使用 border-red-200 / bg-red-50,改用标准的 border-destructive/20 和 bg-destructive/5,
|
||||
并在黑夜模式下会自动转为深红底色,绝不刺眼。
|
||||
*/}
|
||||
<div className="p-6 text-center rounded-xl border border-destructive/20 bg-destructive/5 max-w-md w-full shadow-sm">
|
||||
{/* 2. 状态符号改用标准的 text-destructive 语义色 */}
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-destructive/10 text-destructive mx-auto mb-4">
|
||||
<AlertCircle className="h-6 w-6" />
|
||||
</div>
|
||||
|
||||
<h3 className="text-base font-semibold text-foreground mb-1.5">该功能运行异常</h3>
|
||||
<h3 className="text-base font-semibold text-foreground mb-1.5">
|
||||
{t('pageErrorBoundary.title')}
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground mb-5">
|
||||
该页面在加载或渲染时遇到了内部脚本错误。您可以尝试重试,或者通过导航菜单切换到其他工具。
|
||||
{t('pageErrorBoundary.description')}
|
||||
</p>
|
||||
|
||||
{/* 3. 错误日志展示:使用与 shadcn 贴合的深色代码块包裹 */}
|
||||
{this.state.error && (
|
||||
<div className="mb-5 p-3 rounded-lg bg-zinc-950 dark:bg-zinc-900 text-left max-h-40 overflow-y-auto border border-border/40">
|
||||
<pre className="font-mono text-[11px] leading-relaxed whitespace-pre-wrap break-all text-zinc-200 selection:bg-zinc-700">
|
||||
@@ -69,11 +62,6 @@ export class PageErrorBoundary extends Component<Props, State> {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/*
|
||||
4. 严谨调用 shadcn 原子 Button:
|
||||
去掉全部手动指定的红底白字类名,直接启用 variant="destructive"。
|
||||
它会自动处理 hover 颜色变化、暗黑模式切换以及无障碍高亮边框。
|
||||
*/}
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
@@ -81,7 +69,7 @@ export class PageErrorBoundary extends Component<Props, State> {
|
||||
className="font-medium shadow-sm"
|
||||
>
|
||||
<RefreshCw className="mr-1.5 h-3.5 w-3.5" />
|
||||
重新尝试
|
||||
{t('errorBoundary.retry')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -92,4 +80,5 @@ export class PageErrorBoundary extends Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
export const PageErrorBoundary = withTranslation('common')(PageErrorBoundaryBase);
|
||||
export default PageErrorBoundary;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
import { Copy, Download } from 'lucide-react';
|
||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
||||
import { cn } from '@/lib/utils'; // 1. 引入标准的 shadcn 工具函数
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
// 继承原生 HTML Div 属性,方便外部无缝扩充类名或监听事件
|
||||
interface QrCodePreviewProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
@@ -59,27 +60,16 @@ const QrCodePreview = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 3. 按钮群全面向 shadcn 官方 Button 视觉规范对齐 */}
|
||||
<div className="flex w-full gap-2 mt-5">
|
||||
{/* 下载按钮:使用标准的次要按钮风格 (Outline) */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onDownload}
|
||||
className="flex-1 inline-flex h-9 items-center justify-center gap-2 px-3 text-sm font-medium rounded-md border border-input bg-background shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
>
|
||||
<Button variant="outline" size="sm" onClick={onDownload} className="flex-1">
|
||||
<Download className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="truncate">{t('qrCode:downloadButton')}</span>
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
{/* 复制按钮:使用标准的主要行动按钮风格 (Default) */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCopy}
|
||||
className="flex-1 inline-flex h-9 items-center justify-center gap-2 px-3 text-sm font-medium rounded-md bg-primary text-primary-foreground shadow transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
>
|
||||
<Button variant="default" size="sm" onClick={onCopy} className="flex-1">
|
||||
<Copy className="w-4 h-4" />
|
||||
<span className="truncate">{t('qrCode:copyQrButton')}</span>
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# components/
|
||||
|
||||
通用业务组件目录,存放跨页面复用的 UI 组件,与具体工具页面解耦。
|
||||
|
||||
## 组件列表
|
||||
|
||||
| 组件 | 用途 |
|
||||
| ----------------------- | ------------------------------------------------------------------------------ |
|
||||
| `TopBar.tsx` | 顶部导航栏,集成搜索(含历史记录)、主题切换、语言切换、返回导航 |
|
||||
| `RouterContainer.tsx` | 路由容器,根据当前路由动态渲染对应页面组件,集成错误边界和骨架屏 |
|
||||
| `SwitchButtonGroup.tsx` | 通用切换按钮组,支持 `small/medium/large` 三种尺寸,用于页面子模式切换 |
|
||||
| `TextInputArea.tsx` | 增强文本输入区域,支持校验规则、工具栏操作、字符计数、清空 |
|
||||
| `CopyButton.tsx` | 一键复制按钮,支持复制成功状态动画,封装 `copyTextToClipboard` 和 `toast` 反馈 |
|
||||
| `ImageUploader.tsx` | 图片上传组件,支持拖拽上传、文件选择和预览 |
|
||||
| `QrCodePreview.tsx` | 二维码预览组件,展示生成的二维码图片,提供复制和下载操作 |
|
||||
| `DecodeResultPaper.tsx` | Base64 解码结果展示面板,显示 MIME 类型、文件大小、文件名输入和下载按钮 |
|
||||
| `GlobalSnackbar.tsx` | 全局消息提示组件 + Context Provider,支持受控/Hook/全局单例三种使用方式 |
|
||||
| `ErrorBoundary.tsx` | 全局错误边界(类组件),捕获子组件树 JS 错误并展示友好错误页面 |
|
||||
| `PageErrorBoundary.tsx` | 页面级错误边界,适配 shadcn 暗黑模式,支持 `resetKey` 自动恢复 |
|
||||
| `PageSkeleton.tsx` | 页面骨架屏,提供 `dashboard` 和 `tool` 两种变体,用于 Suspense fallback |
|
||||
|
||||
## 使用约定
|
||||
|
||||
- 优先使用 `components/ui/` 下的 shadcn/ui 基础组件
|
||||
- 组件使用 `cn()` 合并 Tailwind 类名,支持 `className` 透传
|
||||
- 需要 memo 优化的组件使用 `React.memo` + `displayName`
|
||||
- 需要 ref 转发的组件使用 `React.forwardRef` + `displayName`
|
||||
@@ -1,6 +1,7 @@
|
||||
import { FEATURES, getEntryPointType } from '@/config/features';
|
||||
import { useRouter } from '@/providers/RouterProvider';
|
||||
import { Suspense, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import PageErrorBoundary from '@/components/PageErrorBoundary';
|
||||
import PageSkeleton from '@/components/PageSkeleton';
|
||||
import { cn } from '@/lib/utils'; // 1. 引入标准的 shadcn 工具函数
|
||||
@@ -8,15 +9,14 @@ import { AlertTriangle } from 'lucide-react'; // 用于标准的 404 异常展
|
||||
|
||||
export default function RouterContainer() {
|
||||
const { currentPage, isLoaded } = useRouter();
|
||||
const { t } = useTranslation('common');
|
||||
|
||||
// 2. 稳定的动态动画类名映射
|
||||
const animationClass = useMemo(() => {
|
||||
return currentPage === 'dashboard' ? 'page-transition-dashboard' : 'page-transition-enter';
|
||||
}, [currentPage]);
|
||||
|
||||
const entryPointType = useMemo(() => {
|
||||
return getEntryPointType();
|
||||
}, []);
|
||||
const entryPointType = getEntryPointType();
|
||||
|
||||
// 骨架屏加载状态守卫
|
||||
if (!isLoaded) {
|
||||
@@ -52,9 +52,9 @@ export default function RouterContainer() {
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-destructive/10 text-destructive mb-4">
|
||||
<AlertTriangle className="h-6 w-6" />
|
||||
</div>
|
||||
<h3 className="text-sm font-semibold text-foreground">页面未找到</h3>
|
||||
<h3 className="text-sm font-semibold text-foreground">{t('router.notFound')}</h3>
|
||||
<p className="text-xs text-muted-foreground mt-1 max-w-[240px]">
|
||||
该功能在当前运行环境({entryPointType})下不可用或已被移除。
|
||||
{t('router.notFoundDescription', { entryPointType })}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -34,14 +34,10 @@ export default function SwitchButtonGroup<T extends string | number = string>({
|
||||
large: 'text-base h-11 px-4 py-2 rounded-lg',
|
||||
};
|
||||
|
||||
const containerPadding = size === 'large' ? 'p-1' : 'p-1';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
// 将默认布局设计得更为通用(去掉一刀切的 mb-4,由外部控制布局空间)
|
||||
'inline-flex w-full items-center justify-center rounded-lg bg-muted text-muted-foreground',
|
||||
containerPadding,
|
||||
'inline-flex w-full items-center justify-center rounded-lg bg-muted text-muted-foreground p-1',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react';
|
||||
import { Copy, X } from 'lucide-react';
|
||||
import { X } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { toast } from 'sonner'; // 推荐使用 shadcn 的默认 Toast
|
||||
import { CopyButton } from '@/components/CopyButton';
|
||||
|
||||
export type ValidateRule = {
|
||||
validator: (value: string) => boolean;
|
||||
@@ -183,19 +184,9 @@ const TextInputArea = forwardRef<HTMLTextAreaElement, TextInputAreaProps>((props
|
||||
onChange?.('');
|
||||
setError('');
|
||||
internalRef.current?.focus();
|
||||
toast.success('已清空内容');
|
||||
toast.success(t('textInputArea.cleared'));
|
||||
onClear?.();
|
||||
}, [isControlled, onChange, onClear]);
|
||||
|
||||
const handleCopy = useCallback(async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
toast.success('复制成功');
|
||||
} catch {
|
||||
setError('复制失败');
|
||||
toast.error('复制失败');
|
||||
}
|
||||
}, [value]);
|
||||
}, [isControlled, onChange, onClear, t]);
|
||||
|
||||
const handleAction = useCallback(
|
||||
(action: ToolbarAction) => {
|
||||
@@ -284,14 +275,12 @@ const TextInputArea = forwardRef<HTMLTextAreaElement, TextInputAreaProps>((props
|
||||
{/* 右侧系统按钮组 */}
|
||||
<div className="flex items-center gap-1.5 ml-auto shrink-0">
|
||||
{allowCopy && value && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
aria-label={t('textInputArea.copyContent')}
|
||||
className="p-1 h-7 w-7 flex items-center justify-center rounded-md text-muted-foreground hover:text-foreground hover:bg-background/80 transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
</button>
|
||||
<CopyButton
|
||||
text={value}
|
||||
tooltip={t('textInputArea.copyContent')}
|
||||
size="sm"
|
||||
className="h-7 w-7 p-1"
|
||||
/>
|
||||
)}
|
||||
{showClear && value && !disabled && !readOnly && (
|
||||
<button
|
||||
|
||||
+10
-7
@@ -15,6 +15,7 @@ import { useRouter } from '@/providers/RouterProvider';
|
||||
import { useThemeMode } from '@/providers/ThemeModeProvider';
|
||||
import { FeatureConfig, FEATURES } from '@/config/features';
|
||||
import { storageUtil } from '@/utils/chromeStorage';
|
||||
import { openExtensionPage } from '@/utils/chromeTabs';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { normalizeLanguage, SUPPORTED_LANGUAGES } from '@/i18n';
|
||||
import { cn } from '@/lib/utils'; // 1. 引入 shadcn 核心工具函数
|
||||
@@ -125,17 +126,19 @@ export default function TopBar({ onOpenOptions }: { onOpenOptions: () => void })
|
||||
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : prev));
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
if (selectedIndex >= 0) {
|
||||
if (selectedIndex >= 0 && selectedIndex < totalItems) {
|
||||
if (searchQuery.trim()) {
|
||||
handleSelectFeature(searchResults[selectedIndex]);
|
||||
} else {
|
||||
const selectedQuery = displayedHistory[selectedIndex];
|
||||
setSearchQuery(selectedQuery);
|
||||
setSelectedIndex(-1);
|
||||
const matched = FEATURES.find(
|
||||
(f) => f.key !== 'dashboard' && t(f.labelKey) === selectedQuery,
|
||||
);
|
||||
if (matched) handleSelectFeature(matched);
|
||||
if (selectedQuery) {
|
||||
setSearchQuery(selectedQuery);
|
||||
setSelectedIndex(-1);
|
||||
const matched = FEATURES.find(
|
||||
(f) => f.key !== 'dashboard' && t(f.labelKey) === selectedQuery,
|
||||
);
|
||||
if (matched) handleSelectFeature(matched);
|
||||
}
|
||||
}
|
||||
} else if (searchQuery.trim() && searchResults.length > 0) {
|
||||
handleSelectFeature(searchResults[0]);
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# components/ui/
|
||||
|
||||
shadcn/ui 基础原子组件目录,基于 Radix UI 原语 + Tailwind CSS 实现。
|
||||
|
||||
## 组件列表
|
||||
|
||||
| 组件 | 用途 |
|
||||
| -------------- | -------------------------------------------------------------------------------------------------- |
|
||||
| `button.tsx` | 按钮组件,支持 `default/destructive/outline/secondary/ghost/link` 变体和 `default/sm/lg/icon` 尺寸 |
|
||||
| `input.tsx` | 标准输入框,统一的 ring/focus 样式 |
|
||||
| `select.tsx` | 下拉选择组件,包含 Trigger、Content、Item 等子组件 |
|
||||
| `dialog.tsx` | 对话框组件,包含 Overlay、Content、Header、Footer、Title、Description |
|
||||
| `checkbox.tsx` | 复选框组件 |
|
||||
| `label.tsx` | 标签组件 |
|
||||
| `switch.tsx` | 开关组件 |
|
||||
| `badge.tsx` | 徽章组件,支持 `default/secondary/destructive/outline` 变体 |
|
||||
|
||||
## 使用方式
|
||||
|
||||
```tsx
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
```
|
||||
|
||||
## 添加新组件
|
||||
|
||||
使用 shadcn/ui CLI 添加新组件:
|
||||
|
||||
```bash
|
||||
npx shadcn-ui@latest add <component-name>
|
||||
```
|
||||
|
||||
组件配置在项目根目录的 `components.json` 中定义。
|
||||
Reference in New Issue
Block a user