Files
testing-tool/pages/JsonTools/JsonConvertSection.tsx
T
2026-05-22 00:07:56 +08:00

153 lines
4.8 KiB
TypeScript

import { useEffect, useMemo, useState } from 'react';
import { Button } from '@/components/ui/button';
import { useLazyTranslation } from '@/utils/useLazyTranslation';
import { formatByteSize } from '@/utils/textStatistics';
import { useSnackbar } from '@/components/GlobalSnackbar';
import CopyButton from '@/components/CopyButton';
import TextInputArea from '@/components/TextInputArea';
import { validateJson } from '@/utils/jsonFormatter';
/** 转换结果通用接口 */
export interface ConvertResult {
/** 转换后的输出字符串 */
output: string;
/** 原始输入的字节大小 */
originalBytes: number;
/** 转换后的字节大小 */
outputBytes: number;
}
/** 转换函数类型 */
export type ConvertFunction = (text: string) => ConvertResult;
/**
* JSON 转换工具区域组件属性
*/
interface JsonConvertSectionProps {
/** i18n 命名空间内翻译键的前缀,如 'yamlMode' / 'tomlMode' / 'minifyMode' */
translationPrefix: string;
/** 转换函数 */
convertFunction: ConvertFunction;
/** 转换按钮的翻译键后缀,默认 'convertButton' */
convertButtonKey?: string;
}
/**
* JSON 转换工具共享组件
*
* 适用于 JSON->YAML、JSON->TOML、JSON 压缩等场景,
* 提供输入区域、转换按钮和结果展示(含一键复制)。
*/
export default function JsonConvertSection({
translationPrefix,
convertFunction,
convertButtonKey = 'convertButton',
}: JsonConvertSectionProps) {
const { t } = useLazyTranslation('jsonFormat');
const { showMessage } = useSnackbar();
const [input, setInput] = useState('');
const [error, setError] = useState<string | null>(null);
const [result, setResult] = useState<ConvertResult | null>(null);
const pk = translationPrefix;
// 防抖校验输入
useEffect(() => {
const handle = setTimeout(() => {
setError(validateJson(input));
}, 300);
return () => clearTimeout(handle);
}, [input]);
const canConvert = useMemo(() => {
return input.trim() !== '' && !error;
}, [input, error]);
const handleConvert = () => {
const validationError = validateJson(input);
if (validationError) {
setError(validationError);
setResult(null);
return;
}
try {
const convertResult = convertFunction(input);
setResult(convertResult);
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
setResult(null);
}
};
const handleClear = () => {
setInput('');
setError(null);
setResult(null);
};
return (
<div className="flex flex-col gap-6">
{/* 工具栏 */}
<div className="flex flex-col sm:flex-row gap-3 justify-between items-stretch sm:items-center">
<div />
<div className="flex gap-2">
<Button variant="outline" onClick={handleClear} className="rounded-lg">
{t('jsonFormat:clearButton')}
</Button>
<Button
variant="default"
disabled={!canConvert}
onClick={handleConvert}
className="rounded-lg font-bold px-4"
>
{t(`jsonFormat:${convertButtonKey}`)}
</Button>
</div>
</div>
{/* 输入区 */}
<TextInputArea
placeholder={t(`jsonFormat:${pk}InputPlaceholder`)}
value={input}
onChange={setInput}
externalError={error || undefined}
onClear={() => {
setResult(null);
}}
/>
{/* 转换结果 */}
{result && result.output ? (
<div className="relative rounded-lg bg-white border border-gray-200 overflow-hidden">
{/* 结果头部 */}
<div className="flex justify-between items-center px-4 py-2 border-b border-gray-200 bg-gray-50">
<div className="flex gap-4 items-center">
<span className="text-[11px] font-extrabold text-gray-500">
{t(`jsonFormat:${pk}OutputLabel`)}
</span>
<span className="text-[10px] text-gray-400">
{t('jsonFormat:originalSize')}: {formatByteSize(result.originalBytes)}
</span>
<span className="text-[10px] text-gray-400">
{t('jsonFormat:formattedSize')}: {formatByteSize(result.outputBytes)}
</span>
</div>
<CopyButton text={result.output} showMessage={showMessage} />
</div>
{/* 转换内容 */}
<div className="p-4 font-mono text-sm whitespace-pre-wrap break-all max-h-[400px] overflow-y-auto leading-relaxed">
{result.output}
</div>
</div>
) : (
<div className="p-4 rounded-lg bg-gray-50 border border-dashed border-gray-300 text-center">
<p className="text-sm font-semibold text-gray-500">{t(`jsonFormat:${pk}EmptyHint`)}</p>
</div>
)}
</div>
);
}