diff --git a/src/components/EmptyPlaceholder.tsx b/src/components/EmptyPlaceholder.tsx new file mode 100644 index 0000000..5ffacc1 --- /dev/null +++ b/src/components/EmptyPlaceholder.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { cn } from '@/lib/utils'; + +export interface EmptyPlaceholderProps extends React.HTMLAttributes { + children: React.ReactNode; + messageClassName?: string; +} + +const CONTAINER_CLASSES = + 'rounded-xl bg-muted/30 border border-dashed border-border/80 text-center flex flex-col items-center justify-center select-none p-8 min-h-[120px]'; + +const MESSAGE_CLASSES = + 'text-xs font-semibold text-muted-foreground/80 tracking-wide max-w-[240px] leading-relaxed'; + +export default function EmptyPlaceholder({ + children, + className, + messageClassName, + ...props +}: EmptyPlaceholderProps) { + return ( +
+ {typeof children === 'string' || typeof children === 'number' ? ( +

{children}

+ ) : ( + children + )} +
+ ); +} diff --git a/src/components/README.md b/src/components/README.md index 2d594b8..c8759a0 100644 --- a/src/components/README.md +++ b/src/components/README.md @@ -8,6 +8,7 @@ | ----------------------- | ------------------------------------------------------------------------------ | | `RouterContainer.tsx` | 路由容器,根据当前路由动态渲染对应页面组件,集成错误边界和骨架屏 | | `SwitchButtonGroup.tsx` | 通用切换按钮组,支持 `small/medium/large` 三种尺寸,用于页面子模式切换 | +| `EmptyPlaceholder.tsx` | 虚线边框空状态占位,统一工具页「暂无结果」提示样式 | | `TextInputArea.tsx` | 增强文本输入区域,支持校验规则、工具栏操作、字符计数、清空 | | `CopyButton.tsx` | 一键复制按钮,支持复制成功状态动画,封装 `copyTextToClipboard` 和 `toast` 反馈 | | `ImageUploader.tsx` | 图片上传组件,支持拖拽上传、文件选择和预览 | diff --git a/src/components/__tests__/EmptyPlaceholder.test.tsx b/src/components/__tests__/EmptyPlaceholder.test.tsx new file mode 100644 index 0000000..e72fbb8 --- /dev/null +++ b/src/components/__tests__/EmptyPlaceholder.test.tsx @@ -0,0 +1,48 @@ +import { describe, expect, it } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import EmptyPlaceholder from '@/components/EmptyPlaceholder'; + +describe('EmptyPlaceholder 组件', () => { + it('应渲染字符串提示文本', () => { + render({'请输入内容'}); + + expect(screen.getByText('请输入内容')).toBeInTheDocument(); + }); + + it('应应用规范空状态容器样式', () => { + const { container } = render({'提示'}); + + const placeholder = container.firstChild; + expect(placeholder).toHaveClass( + 'rounded-xl', + 'bg-muted/30', + 'border-dashed', + 'border-border/80', + 'min-h-[120px]', + ); + }); + + it('应支持 className 自定义容器样式', () => { + const { container } = render( + {'提示'}, + ); + + expect(container.firstChild).toHaveClass('flex-1', 'min-h-[320px]'); + }); + + it('应支持 messageClassName 自定义文本样式', () => { + render({'提示'}); + + expect(screen.getByText('提示')).toHaveClass('text-sm', 'max-w-none'); + }); + + it('应支持 ReactNode 类型的 children', () => { + render( + + 自定义内容 + , + ); + + expect(screen.getByTestId('custom-content')).toBeInTheDocument(); + }); +}); diff --git a/src/pages/JsonTools/components/JsonConvertSection.tsx b/src/pages/JsonTools/components/JsonConvertSection.tsx index 3d5489b..5e7c162 100644 --- a/src/pages/JsonTools/components/JsonConvertSection.tsx +++ b/src/pages/JsonTools/components/JsonConvertSection.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useMemo, useState } from 'react'; import { formatBytes } from '@/utils/format'; import { CopyButton } from '@/components/CopyButton'; +import EmptyPlaceholder from '@/components/EmptyPlaceholder'; import TextInputArea from '@/components/TextInputArea'; import { validateJson } from '@/utils/jsonFormatter'; import { cn } from '@/lib/utils'; @@ -132,11 +133,9 @@ export default function JsonConvertSection({ ) : ( -
-

- {error ? '请修正上方 JSON 的语法错误以开启实时流式格式化' : labels.emptyHint} -

-
+ + {error ? '请修正上方 JSON 的语法错误以开启实时流式格式化' : labels.emptyHint} + )} ); diff --git a/src/pages/JsonTools/components/JsonFormatSection.tsx b/src/pages/JsonTools/components/JsonFormatSection.tsx index 0c8fd0d..0eef1ca 100644 --- a/src/pages/JsonTools/components/JsonFormatSection.tsx +++ b/src/pages/JsonTools/components/JsonFormatSection.tsx @@ -7,6 +7,7 @@ import { } from '@/utils/jsonFormatter'; import { formatBytes } from '@/utils/format'; import { CopyButton } from '@/components/CopyButton'; +import EmptyPlaceholder from '@/components/EmptyPlaceholder'; import SwitchButtonGroup from '@/components/SwitchButtonGroup'; import TextInputArea from '@/components/TextInputArea'; import { Checkbox } from '@/components/ui/checkbox'; @@ -148,12 +149,9 @@ export default function JsonFormatSection() { ) : ( - /* 空状态指示引导区 */ -
-

- {error ? '请修正上方 JSON 的语法错误以开启实时流式格式化' : '输入 JSON 后点击格式化'} -

-
+ + {error ? '请修正上方 JSON 的语法错误以开启实时流式格式化' : '输入 JSON 后点击格式化'} + )} ); diff --git a/src/pages/JsonTools/index.tsx b/src/pages/JsonTools/index.tsx index de68b49..910eca2 100644 --- a/src/pages/JsonTools/index.tsx +++ b/src/pages/JsonTools/index.tsx @@ -4,6 +4,7 @@ import DiffNavigator from './components/DiffNavigator'; import JsonFormatSection from './components/JsonFormatSection'; import JsonConvertSection from './components/JsonConvertSection'; import SwitchButtonGroup from '@/components/SwitchButtonGroup'; +import EmptyPlaceholder from '@/components/EmptyPlaceholder'; import { useJsonTools } from './useJsonTools'; import type { JsonToolsPageMode } from '@/types/storage'; import type { ViewMode } from './types'; @@ -95,13 +96,11 @@ export default function Index() { ) : ( -
-

- {leftError || rightError - ? '请修正上方 JSON 的语法错误以开启实时流式比对' - : '输入两侧 JSON 后点击比较'} -

-
+ + {leftError || rightError + ? '请修正上方 JSON 的语法错误以开启实时流式比对' + : '输入两侧 JSON 后点击比较'} + )} ) : pageMode === 'format' ? ( diff --git a/src/pages/Jwt/index.tsx b/src/pages/Jwt/index.tsx index 5d77565..ee75e12 100644 --- a/src/pages/Jwt/index.tsx +++ b/src/pages/Jwt/index.tsx @@ -1,5 +1,6 @@ import TextInputArea from '@/components/TextInputArea'; import { CopyButton } from '@/components/CopyButton'; +import EmptyPlaceholder from '@/components/EmptyPlaceholder'; import JwtSection from './JwtSection'; import { useJwt } from './useJwt'; @@ -57,9 +58,7 @@ export default function Index() { )} {result?.error && ( -
-

{'无效的 JSON 格式'}

-
+ {'无效的 JSON 格式'} )} diff --git a/src/pages/QrCode/components/QrCodePreview.tsx b/src/pages/QrCode/components/QrCodePreview.tsx index 5528ead..e7808a7 100644 --- a/src/pages/QrCode/components/QrCodePreview.tsx +++ b/src/pages/QrCode/components/QrCodePreview.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { Copy, Download } from 'lucide-react'; +import EmptyPlaceholder from '@/components/EmptyPlaceholder'; import { cn } from '@/lib/utils'; import { Button } from '@/components/ui/button'; @@ -26,17 +27,13 @@ const QrCodePreview = ({ // 空状态下的虚线骨架屏 if (!qrCodeDataUrl) { return ( -
-

- {placeholderText || '二维码将显示在这里'} -

-
+ {placeholderText || '二维码将显示在这里'} + ); } diff --git a/src/pages/Timestamp/components/LiveClock.tsx b/src/pages/Timestamp/components/LiveClock.tsx index a50133a..2207a31 100644 --- a/src/pages/Timestamp/components/LiveClock.tsx +++ b/src/pages/Timestamp/components/LiveClock.tsx @@ -5,6 +5,7 @@ import { CopyButton } from '@/components/CopyButton'; import type { UnitType } from '../constants'; import { msToUnit } from '../constants'; import { cn } from '@/lib/utils'; +import { Button } from '@/components/ui/button'; interface LiveClockProps extends React.HTMLAttributes { unit: UnitType; @@ -40,17 +41,19 @@ export default function LiveClock({ unit, onUseNow, className, ...props }: LiveC {text} - + diff --git a/src/pages/Timestamp/components/ResultView.tsx b/src/pages/Timestamp/components/ResultView.tsx index 364207d..06d70ba 100644 --- a/src/pages/Timestamp/components/ResultView.tsx +++ b/src/pages/Timestamp/components/ResultView.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { CopyButton } from '@/components/CopyButton'; +import EmptyPlaceholder from '@/components/EmptyPlaceholder'; import { cn } from '@/lib/utils'; interface ResultViewProps extends React.HTMLAttributes { @@ -16,15 +17,13 @@ export default function ResultView({ if (!result) { if (!showEmptyPlaceholder) return null; return ( -
{'请输入并点击转换'} -
+ ); } @@ -41,7 +40,9 @@ export default function ResultView({ diff --git a/src/pages/Timestamp/constants.ts b/src/pages/Timestamp/constants.ts index 6a84613..8077d34 100644 --- a/src/pages/Timestamp/constants.ts +++ b/src/pages/Timestamp/constants.ts @@ -11,12 +11,10 @@ export type ZoneType = (typeof ZONES)[number]; const DIVISORS: Record = { ms: 1, s: 1000 }; -/** Convert milliseconds to display value based on unit. */ export function msToUnit(ms: number, unit: UnitType): number { return Math.floor(ms / DIVISORS[unit]); } -/** Build a dayjs object from a numeric timestamp according to unit. */ export function dayjsFromTimestamp(ts: number, unit: UnitType): Dayjs { return unit === 'ms' ? dayjs(ts) : dayjs.unix(ts); }