feat: 添加 EmptyPlaceholder 组件并更新相关页面

- 新增 EmptyPlaceholder 组件,用于统一处理空状态提示,提升用户体验。
- 更新多个页面(JsonTools、Jwt、QrCode、Timestamp)以使用 EmptyPlaceholder 组件,简化空状态的实现。
- 更新 README.md,添加 EmptyPlaceholder 组件的说明。
This commit is contained in:
雨霖铃
2026-06-19 10:16:59 +08:00
parent 3ab7194501
commit ca0143f3bd
11 changed files with 115 additions and 42 deletions
@@ -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({
</div>
</div>
) : (
<div className="p-8 rounded-xl bg-muted/30 border border-dashed border-border/80 text-center flex flex-col items-center justify-center min-h-[120px] select-none">
<p className="text-xs font-semibold text-muted-foreground/80 tracking-wide max-w-[240px] leading-relaxed">
{error ? '请修正上方 JSON 的语法错误以开启实时流式格式化' : labels.emptyHint}
</p>
</div>
<EmptyPlaceholder>
{error ? '请修正上方 JSON 的语法错误以开启实时流式格式化' : labels.emptyHint}
</EmptyPlaceholder>
)}
</div>
);
@@ -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() {
</div>
</div>
) : (
/* 空状态指示引导区 */
<div className="p-8 rounded-xl bg-muted/30 border border-dashed border-border/80 text-center flex flex-col items-center justify-center min-h-[120px] select-none">
<p className="text-xs font-semibold text-muted-foreground/80 tracking-wide max-w-[240px] leading-relaxed">
{error ? '请修正上方 JSON 的语法错误以开启实时流式格式化' : '输入 JSON 后点击格式化'}
</p>
</div>
<EmptyPlaceholder>
{error ? '请修正上方 JSON 的语法错误以开启实时流式格式化' : '输入 JSON 后点击格式化'}
</EmptyPlaceholder>
)}
</div>
);
+6 -7
View File
@@ -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() {
<DiffResult result={diffResult} viewMode={viewMode} activePath={activePath} />
</div>
) : (
<div className="p-8 rounded-xl bg-muted/30 border border-dashed border-border/80 text-center flex flex-col items-center justify-center min-h-[140px]">
<p className="text-xs font-semibold text-muted-foreground/80 tracking-wide max-w-[260px] leading-relaxed">
{leftError || rightError
? '请修正上方 JSON 的语法错误以开启实时流式比对'
: '输入两侧 JSON 后点击比较'}
</p>
</div>
<EmptyPlaceholder className="min-h-[140px]" messageClassName="max-w-[260px]">
{leftError || rightError
? '请修正上方 JSON 的语法错误以开启实时流式比对'
: '输入两侧 JSON 后点击比较'}
</EmptyPlaceholder>
)}
</div>
) : pageMode === 'format' ? (
+2 -3
View File
@@ -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 && (
<div className="p-6 rounded-xl bg-muted/30 border border-dashed border-border text-center">
<p className="text-xs font-semibold text-muted-foreground/80">{'无效的 JSON 格式'}</p>
</div>
<EmptyPlaceholder className="p-6 min-h-0">{'无效的 JSON 格式'}</EmptyPlaceholder>
)}
</div>
</div>
@@ -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 (
<div
className={cn(
'flex flex-col justify-center items-center min-h-[200px] border border-dashed border-input rounded-xl p-4 bg-muted/40',
className,
)}
<EmptyPlaceholder
className={cn('min-h-[200px] p-4', className)}
messageClassName="text-sm text-muted-foreground max-w-none"
{...props}
>
<p className="text-sm text-muted-foreground text-center">
{placeholderText || '二维码将显示在这里'}
</p>
</div>
{placeholderText || '二维码将显示在这里'}
</EmptyPlaceholder>
);
}
+6 -3
View File
@@ -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<HTMLDivElement> {
unit: UnitType;
@@ -40,17 +41,19 @@ export default function LiveClock({ unit, onUseNow, className, ...props }: LiveC
{text}
</span>
<button
<Button
type="button"
onClick={() => {
onUseNow(rawTime);
toast.success('已使用当前时间戳');
}}
title={'填充到下方'}
className="flex h-7 w-7 items-center justify-center rounded-md border border-input bg-background text-muted-foreground shadow-sm hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
variant="ghost"
size="icon"
className="h-7 w-7 text-muted-foreground hover:text-foreground"
>
<Clock className="w-3.5 h-3.5" />
</button>
</Button>
<CopyButton text={text} tooltip={'复制时间戳'} className="h-7 w-7 rounded-md border" />
</div>
@@ -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<HTMLDivElement> {
@@ -16,15 +17,13 @@ export default function ResultView({
if (!result) {
if (!showEmptyPlaceholder) return null;
return (
<div
className={cn(
'flex-1 flex items-center justify-center text-sm font-medium border border-dashed border-border/60 rounded-xl py-12 px-4 text-center text-muted-foreground bg-muted/20 min-h-[320px]',
className,
)}
<EmptyPlaceholder
className={cn('flex-1 min-h-[320px]', className)}
messageClassName="text-sm font-medium text-muted-foreground max-w-none"
{...props}
>
{'请输入并点击转换'}
</div>
</EmptyPlaceholder>
);
}
@@ -41,7 +40,9 @@ export default function ResultView({
<CopyButton
text={result}
tooltip={'复制结果'}
className="h-8 w-8 rounded-md shrink-0 border"
variant="ghost"
size="icon"
className="h-7 w-7 text-muted-foreground hover:text-foreground"
/>
</div>
</div>
-2
View File
@@ -11,12 +11,10 @@ export type ZoneType = (typeof ZONES)[number];
const DIVISORS: Record<UnitType, number> = { 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);
}