refactor(代码重复): 抽取共享工具函数与 UI 组件,消除多处重复实现

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
雨霖铃
2026-06-19 20:55:39 +08:00
parent 47d74f999e
commit 0f3060dcf3
24 changed files with 386 additions and 309 deletions
@@ -1,8 +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 JsonResultPanel from './JsonResultPanel';
import { validateJson } from '@/utils/jsonFormatter';
import { cn } from '@/lib/utils';
import type { ConvertFunction, ConvertResult } from '../types';
@@ -98,40 +97,13 @@ export default function JsonConvertSection({
{/* Result display */}
{result && result.output ? (
<div className="relative rounded-xl border border-border bg-card text-card-foreground shadow-sm overflow-hidden">
<div className="flex h-9 items-center justify-between px-4 border-b border-border bg-muted/50 select-none">
<div className="flex gap-4 items-center">
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground/90">
{labels.outputLabel}
</span>
<div className="hidden sm:flex gap-3 items-center font-mono text-[10px] text-muted-foreground/70 tabular-nums">
<span>
{'原始大小'}:{' '}
<span className="font-semibold text-foreground/80">
{formatBytes(result.originalBytes)}
</span>
</span>
<span className="text-border/60">|</span>
<span>
{'格式化后大小'}:{' '}
<span className="font-semibold text-foreground/80">
{formatBytes(result.outputBytes)}
</span>
</span>
</div>
</div>
<CopyButton
text={result.output}
className="h-6 w-6 rounded-md border text-muted-foreground"
/>
</div>
<div className="p-4 font-mono text-xs text-foreground/90 whitespace-pre-wrap break-all max-h-[380px] overflow-y-auto leading-relaxed select-text">
{result.output}
</div>
</div>
<JsonResultPanel
title={labels.outputLabel}
content={result.output}
originalBytes={result.originalBytes}
outputBytes={result.outputBytes}
maxHeight="380px"
/>
) : (
<EmptyPlaceholder>
{error ? '请修正上方 JSON 的语法错误以开启实时流式格式化' : labels.emptyHint}
@@ -5,11 +5,10 @@ import {
type JsonFormatResult,
validateJson,
} 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 JsonResultPanel from './JsonResultPanel';
import { Checkbox } from '@/components/ui/checkbox';
import { Label } from '@/components/ui/label';
@@ -113,41 +112,12 @@ export default function JsonFormatSection() {
{/* 格式化结果流面板展示 */}
{result && result.formatted ? (
<div className="relative rounded-xl border border-border bg-card text-card-foreground shadow-sm overflow-hidden">
{/* 结果栏头部 */}
<div className="flex h-9 items-center justify-between px-4 border-b border-border bg-muted/50 select-none">
<div className="flex gap-4 items-center">
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground/90">
{'格式化结果'}
</span>
<div className="hidden sm:flex gap-3 items-center font-mono text-[10px] text-muted-foreground/70 tabular-nums">
<span>
{'原始大小'}:{' '}
<span className="font-semibold text-foreground/80">
{formatBytes(result.originalBytes)}
</span>
</span>
<span className="text-border/60">|</span>
<span>
{'格式化后大小'}:{' '}
<span className="font-semibold text-foreground/80">
{formatBytes(result.formattedBytes)}
</span>
</span>
</div>
</div>
<CopyButton
text={result.formatted}
className="h-6 w-6 rounded-md border text-muted-foreground"
/>
</div>
<div className="p-4 font-mono text-xs text-foreground/90 whitespace-pre-wrap break-all max-h-[420px] overflow-y-auto leading-relaxed select-text">
{result.formatted}
</div>
</div>
<JsonResultPanel
title="格式化结果"
content={result.formatted}
originalBytes={result.originalBytes}
outputBytes={result.formattedBytes}
/>
) : (
<EmptyPlaceholder>
{error ? '请修正上方 JSON 的语法错误以开启实时流式格式化' : '输入 JSON 后点击格式化'}
@@ -0,0 +1,53 @@
import { formatBytes } from '@/utils/format';
import { CopyButton } from '@/components/CopyButton';
export interface JsonResultPanelProps {
title: string;
content: string;
originalBytes: number;
outputBytes: number;
outputSizeLabel?: string;
maxHeight?: string;
}
export default function JsonResultPanel({
title,
content,
originalBytes,
outputBytes,
outputSizeLabel = '格式化后大小',
maxHeight = '420px',
}: JsonResultPanelProps) {
return (
<div className="relative rounded-xl border border-border bg-card text-card-foreground shadow-sm overflow-hidden">
<div className="flex h-9 items-center justify-between px-4 border-b border-border bg-muted/50 select-none">
<div className="flex gap-4 items-center">
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground/90">
{title}
</span>
<div className="hidden sm:flex gap-3 items-center font-mono text-[10px] text-muted-foreground/70 tabular-nums">
<span>
{'原始大小'}:{' '}
<span className="font-semibold text-foreground/80">{formatBytes(originalBytes)}</span>
</span>
<span className="text-border/60">|</span>
<span>
{outputSizeLabel}:{' '}
<span className="font-semibold text-foreground/80">{formatBytes(outputBytes)}</span>
</span>
</div>
</div>
<CopyButton text={content} className="h-6 w-6 rounded-md border text-muted-foreground" />
</div>
<div
className="p-4 font-mono text-xs text-foreground/90 whitespace-pre-wrap break-all overflow-y-auto leading-relaxed select-text"
style={{ maxHeight }}
>
{content}
</div>
</div>
);
}
@@ -1,25 +1,8 @@
import { useEffect, useState } from 'react';
import { MessageAction, sendMessageToContent } from '@/utils/messages';
import { isUnsupportedPageUrl } from '@/utils/restrictedUrls';
import type { RestorerStatus } from './constants';
const UNSUPPORTED_PROTOCOLS = new Set([
'chrome:',
'chrome-extension:',
'about:',
'edge:',
'brave:',
]);
function isUnsupportedPage(url: string | undefined): boolean {
if (!url) return true;
try {
const protocol = new URL(url).protocol;
return UNSUPPORTED_PROTOCOLS.has(protocol);
} catch {
return true;
}
}
function deriveStatus(isUnsupported: boolean, isUnlocked: boolean): RestorerStatus {
if (isUnsupported) return 'unsupported';
return isUnlocked ? 'unlocked' : 'locked';
@@ -46,7 +29,7 @@ export function useRightClickRestorer(): UseRightClickRestorerReturn {
setDomain(url ? new URL(url).hostname : '');
if (isUnsupportedPage(url)) {
if (isUnsupportedPageUrl(url)) {
setIsUnsupported(true);
return;
}
@@ -2,17 +2,9 @@ import React from 'react';
import { formatBytes } from '@/utils/format';
import { cn } from '@/lib/utils';
import type { StorageSizeInfo } from '../useStorageCleaner';
import { OPTION_LABELS } from '../constants';
import { Checkbox } from '@/components/ui/checkbox';
const OPTION_LABELS: Record<string, string> = {
localStorage: 'Local Storage',
sessionStorage: 'Session Storage',
indexedDB: '站点存储',
cookies: 'Cookies',
cacheStorage: 'Cache Storage',
serviceWorkers: 'Service Workers',
};
interface OptionItemProps extends React.HTMLAttributes<HTMLDivElement> {
labelKey: string;
checked: boolean;
@@ -30,7 +22,8 @@ export default function OptionItem({
}: OptionItemProps) {
const sizeValue = sizeInfo?.value;
const isCount = sizeInfo?.displayType === 'count';
const label = OPTION_LABELS[labelKey] || labelKey;
const label =
labelKey in OPTION_LABELS ? OPTION_LABELS[labelKey as keyof typeof OPTION_LABELS] : labelKey;
return (
<div
@@ -10,6 +10,7 @@ import {
import { Badge } from '@/components/ui/badge';
import { AlertTriangle } from 'lucide-react';
import type { StorageCleanerOptions } from '@/types/storage';
import { OPTION_LABELS } from '../constants';
import { cn } from '@/lib/utils';
export interface StorageCleanerConfirmProps {
@@ -19,15 +20,6 @@ export interface StorageCleanerConfirmProps {
options: StorageCleanerOptions;
}
const OPTION_LABELS: Record<string, string> = {
localStorage: 'Local Storage',
sessionStorage: 'Session Storage',
indexedDB: '站点存储',
cookies: 'Cookies',
cacheStorage: 'Cache Storage',
serviceWorkers: 'Service Workers',
};
export function StorageCleanerConfirm({
open,
onClose,
@@ -36,7 +28,9 @@ export function StorageCleanerConfirm({
}: StorageCleanerConfirmProps) {
const selectedOptions = Object.entries(options)
.filter(([_, value]) => value)
.map(([key, _]) => OPTION_LABELS[key] || key);
.map(([key]) =>
key in OPTION_LABELS ? OPTION_LABELS[key as keyof typeof OPTION_LABELS] : key,
);
return (
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && onClose()}>
+19
View File
@@ -0,0 +1,19 @@
import type { StorageCleanerOptions } from '@/types/storage';
export const CLEAN_OPTION_KEYS = [
'localStorage',
'sessionStorage',
'indexedDB',
'cookies',
'cacheStorage',
'serviceWorkers',
] as const satisfies readonly (keyof StorageCleanerOptions)[];
export const OPTION_LABELS: Record<(typeof CLEAN_OPTION_KEYS)[number], string> = {
localStorage: 'Local Storage',
sessionStorage: 'Session Storage',
indexedDB: '站点存储',
cookies: 'Cookies',
cacheStorage: 'Cache Storage',
serviceWorkers: 'Service Workers',
};