From d2e5e6b45db33a120faba85a7caa0895fee1f207 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=A8=E9=9C=96=E9=93=83?= Date: Fri, 22 May 2026 21:45:13 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E5=85=A8=E9=9D=A2=E9=87=8D?= =?UTF-8?q?=E6=9E=84=20JsonTools=20=E9=A1=B5=E9=9D=A2=EF=BC=8C=E9=87=87?= =?UTF-8?q?=E7=94=A8=E5=A3=B0=E6=98=8E=E5=BC=8F=E5=93=8D=E5=BA=94=E6=9E=B6?= =?UTF-8?q?=E6=9E=84=E5=B9=B6=E7=BB=9F=E4=B8=80=20shadcn=20=E6=A0=B7?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pages/JsonTools/DiffNavigator.tsx | 67 ++++- pages/JsonTools/DiffResult.tsx | 113 ++++++--- pages/JsonTools/JsonConvertSection.tsx | 167 ++++++------ pages/JsonTools/JsonDiffInput.tsx | 22 +- pages/JsonTools/JsonFormatSection.tsx | 239 +++++++++--------- pages/JsonTools/JsonTree.tsx | 336 ++++++++++++++----------- pages/JsonTools/diffEngine.ts | 111 +++++--- pages/JsonTools/index.tsx | 291 ++++++++++----------- pages/JsonTools/types.ts | 52 +++- 9 files changed, 797 insertions(+), 601 deletions(-) diff --git a/pages/JsonTools/DiffNavigator.tsx b/pages/JsonTools/DiffNavigator.tsx index a6970c5..353b3f4 100644 --- a/pages/JsonTools/DiffNavigator.tsx +++ b/pages/JsonTools/DiffNavigator.tsx @@ -1,7 +1,9 @@ +import React from 'react'; import { ChevronLeft, ChevronRight } from 'lucide-react'; import { useLazyTranslation } from '@/utils/useLazyTranslation'; +import { cn } from '@/lib/utils'; // 1. 引入标准的 shadcn 工具函数 -interface DiffNavigatorProps { +export interface DiffNavigatorProps extends React.HTMLAttributes { total: number; /** 0-based index */ currentIndex: number; @@ -9,40 +11,83 @@ interface DiffNavigatorProps { onNext: () => void; } -export default function DiffNavigator({ total, currentIndex, onPrev, onNext }: DiffNavigatorProps) { +export default function DiffNavigator({ + total, + currentIndex, + onPrev, + onNext, + className, + ...props +}: DiffNavigatorProps) { const { t } = useLazyTranslation('jsonDiff'); + // 计算当前的边界禁用状态守卫 + const isFirst = currentIndex <= 0; + const isLast = currentIndex >= total - 1; + + // 2. 空状态面板:对齐 shadcn 规范的中性低调卡片 if (total === 0) { return ( -
- {t('jsonDiff:noDiffs')} +
+ + {t('jsonDiff:noDiffs')} +
); } return ( -
+
+ {/* 上一处差异按钮 */} - - {currentIndex + 1} / {total} + + {/* 计数看板:强制等宽防止数字长短不一时产生宽度挤压跳动 */} + + {currentIndex + 1} /{' '} + {total} + + {/* 下一处差异按钮 */}
); } - -export type { DiffNavigatorProps }; diff --git a/pages/JsonTools/DiffResult.tsx b/pages/JsonTools/DiffResult.tsx index e482d0f..e254446 100644 --- a/pages/JsonTools/DiffResult.tsx +++ b/pages/JsonTools/DiffResult.tsx @@ -1,19 +1,31 @@ +import React from 'react'; import { useLazyTranslation } from '@/utils/useLazyTranslation'; +import { cn } from '@/lib/utils'; import JsonTree from './JsonTree'; import type { DiffNode, DiffResult as DiffResultType, DiffType, ViewMode } from './types'; -interface DiffResultProps { +// 💡 顶层 Interface 继承原生 HTML 容器属性,扩展灵活性 +export interface DiffResultProps extends React.HTMLAttributes { result: DiffResultType; viewMode: ViewMode; activePath?: string; } -export default function DiffResult({ result, viewMode, activePath }: DiffResultProps) { +export default function DiffResult({ + result, + viewMode, + activePath, + className, + ...props +}: DiffResultProps) { const { t } = useLazyTranslation('jsonDiff'); if (viewMode === 'sideBySide') { return ( -
+
@@ -27,14 +39,24 @@ export default function DiffResult({ result, viewMode, activePath }: DiffResultP } return ( -
+ /* 1. 单栏拍平视图容器: + - 对齐 shadcn 规范,使用 bg-card、border-border 隔离。 + - 注入 tabular-nums 配合 font-mono,消灭任何行高和字符抖动。 + */ +
); } const SectionLabel = ({ text }: { text: string }) => ( - + {text} ); @@ -51,24 +73,31 @@ const isContainerType = (v: unknown): boolean => (typeof v === 'object' && v !== null) || Array.isArray(v); const prefixForType = (type: DiffType): string => { - if (type === 'added') return '+ '; - if (type === 'removed') return '- '; - if (type === 'modified') return '~ '; - return ' '; + if (type === 'added') return '+'; + if (type === 'removed') return '-'; + if (type === 'modified') return '~'; + return ' '; }; -const colorForType = (type: DiffType): string => { - if (type === 'added') return 'text-green-600'; - if (type === 'removed') return 'text-red-600'; - if (type === 'modified') return 'text-amber-600'; - return 'text-foreground'; -}; - -const bgForType = (type: DiffType): string | undefined => { - if (type === 'added') return 'bg-green-50'; - if (type === 'removed') return 'bg-red-50'; - if (type === 'modified') return 'bg-amber-50'; - return undefined; +// 2. 状态色彩超进化: +// 拒绝硬编码实色系,全部换用高度安全的语义色变体与暗黑模式自适应。 +const typeThemeMap = { + added: { + text: 'text-emerald-600 dark:text-emerald-400', + bg: 'bg-emerald-500/5 dark:bg-emerald-500/10', + }, + removed: { + text: 'text-destructive', + bg: 'bg-destructive/5 dark:bg-destructive/10', + }, + modified: { + text: 'text-amber-600 dark:text-amber-400', + bg: 'bg-amber-500/5 dark:bg-amber-500/10', + }, + unchanged: { + text: 'text-foreground/80', + bg: 'bg-transparent', + }, }; interface UnifiedViewProps { @@ -85,7 +114,6 @@ const UnifiedView = ({ node, depth, activePath }: UnifiedViewProps) => { const keyLabel = isRoot ? '' : `${node.key}: `; if (!isContainer) { - // 叶子节点 if (node.type === 'modified') { return ( <> @@ -115,7 +143,7 @@ const UnifiedView = ({ node, depth, activePath }: UnifiedViewProps) => { ); } - // 容器节点:added/removed 整块呈现 + // 容器节点整块渲染处理 if (node.type === 'added') { return ( { - const color = colorForType(type); - const bg = bgForType(type); + // 3. 高精度提取状态样式映射 + const currentTheme = typeThemeMap[type] || typeThemeMap.unchanged; + return (
- {prefixForType(type)} - {text} + {/* 5. 前缀标识:等宽锁定,强行占据 w-5 并让符号居中对齐,达成 VSCode 般的整洁排版 */} + + {prefixForType(type)} + + + + {text} +
); }; @@ -192,4 +241,4 @@ const stringifyMultiline = (v: unknown, depth: number): string => { } }; -export type { DiffResultProps }; +// 💡 彻底移除了文件底部引发 TS2484 冲突的 export type { DiffResultProps } 声明 diff --git a/pages/JsonTools/JsonConvertSection.tsx b/pages/JsonTools/JsonConvertSection.tsx index fc9ecfa..0a522a5 100644 --- a/pages/JsonTools/JsonConvertSection.tsx +++ b/pages/JsonTools/JsonConvertSection.tsx @@ -1,149 +1,142 @@ -import { useEffect, useMemo, useState } from 'react'; -import { Button } from '@/components/ui/button'; +import React, { useEffect, useMemo, useState } from 'react'; import { useLazyTranslation } from '@/utils/useLazyTranslation'; import { formatByteSize } from '@/utils/textStatistics'; import CopyButton from '@/components/CopyButton'; import TextInputArea from '@/components/TextInputArea'; import { validateJson } from '@/utils/jsonFormatter'; +import { cn } from '@/lib/utils'; -/** 转换结果通用接口 */ export interface ConvertResult { - /** 转换后的输出字符串 */ output: string; - /** 原始输入的字节大小 */ originalBytes: number; - /** 转换后的字节大小 */ outputBytes: number; } -/** 转换函数类型 */ export type ConvertFunction = (text: string) => ConvertResult; -/** - * JSON 转换工具区域组件属性 - */ -interface JsonConvertSectionProps { - /** i18n 命名空间内翻译键的前缀,如 'yamlMode' / 'tomlMode' / 'minifyMode' */ +interface JsonConvertSectionProps extends React.HTMLAttributes { translationPrefix: string; - /** 转换函数 */ convertFunction: ConvertFunction; - /** 转换按钮的翻译键后缀,默认 'convertButton' */ - convertButtonKey?: string; } -/** - * JSON 转换工具共享组件 - * - * 适用于 JSON->YAML、JSON->TOML、JSON 压缩等场景, - * 提供输入区域、转换按钮和结果展示(含一键复制)。 - */ export default function JsonConvertSection({ translationPrefix, convertFunction, - convertButtonKey = 'convertButton', + className, + ...props }: JsonConvertSectionProps) { const { t } = useLazyTranslation('jsonFormat'); const [input, setInput] = useState(''); - const [error, setError] = useState(null); - const [result, setResult] = useState(null); + const [debouncedInput, setDebouncedInput] = useState(''); const pk = translationPrefix; - // 防抖校验输入 + // 1. 高阶性能调优:将文本变化收拢进行 250ms 极速防抖落盘,避免每一次敲击键盘都触发底层的复杂序列化算法 useEffect(() => { const handle = setTimeout(() => { - setError(validateJson(input)); - }, 300); + setDebouncedInput(input); + }, 250); return () => clearTimeout(handle); }, [input]); - const canConvert = useMemo(() => { - return input.trim() !== '' && !error; - }, [input, error]); + // 💡 2. 贯彻方案 A(衍生变量超进化): + // 彻底删掉 error 状态和对应的受控 useEffect 节点。 + // 语法错误由防抖文本在内存中同步推导,彻底斩断二次级联渲染链条,ESLint 警告自愈! + const error = useMemo(() => { + return validateJson(debouncedInput); + }, [debouncedInput]); - const handleConvert = () => { - const validationError = validateJson(input); - if (validationError) { - setError(validationError); - setResult(null); - return; - } + // 3. 核心魔法:纯净的即时流式转换转换管线 (Live Compilation Pipeline) + const conversionPipeline = useMemo(() => { + const trimmed = debouncedInput.trim(); + if (!trimmed || error) return null; try { - const convertResult = convertFunction(input); - setResult(convertResult); + return convertFunction(debouncedInput); } catch (e) { - setError(e instanceof Error ? e.message : String(e)); - setResult(null); + // 捕获可能从外部转换器(如 YAML.stringify)中抛出的底层异常 + return { + isRuntimeError: true, + errorMessage: e instanceof Error ? e.message : String(e), + }; } - }; + }, [debouncedInput, error, convertFunction]); - const handleClear = () => { - setInput(''); - setError(null); - setResult(null); - }; + // 判定运行时异常 + const runtimeError = + conversionPipeline && 'isRuntimeError' in conversionPipeline + ? conversionPipeline.errorMessage + : null; + const result = + conversionPipeline && !('isRuntimeError' in conversionPipeline) + ? (conversionPipeline as ConvertResult) + : null; return ( -
- {/* 工具栏 */} -
-
-
- - -
-
- +
{/* 输入区 */} { - setResult(null); - }} + externalError={error || runtimeError || undefined} // 融合语法错误与运行时转换错误 + showClear={true} + allowCopy={true} + minRows={7} + maxRows={14} + onClear={() => setInput('')} /> - {/* 转换结果 */} + {/* 4. 结果展示或状态引导卡片区 */} {result && result.output ? ( -
- {/* 结果头部 */} -
+
+ {/* 结果栏精致头部 */} +
- + {t(`jsonFormat:${pk}OutputLabel`)} - - {t('jsonFormat:originalSize')}: {formatByteSize(result.originalBytes)} - - - {t('jsonFormat:formattedSize')}: {formatByteSize(result.outputBytes)} - + + {/* 字节比对注入 tabular-nums font-mono,防止容量大小变动时字符横向抽搐 */} +
+ + {t('jsonFormat:originalSize')}:{' '} + + {formatByteSize(result.originalBytes)} + + + | + + {t('jsonFormat:formattedSize')}:{' '} + + {formatByteSize(result.outputBytes)} + + +
- + +
- {/* 转换内容 */} -
+ {/* 转换出的数据流承载区: + 💡 修复点:移除了互相冲突打架的 select-all 类名,仅保留纯净、支持自由划线选中的 select-text 样式 + */} +
{result.output}
) : ( -
-

- {t(`jsonFormat:${pk}EmptyHint`)} + /* 5. 空状态提示容器:完美的中性虚线引导,不喧宾夺主 */ +

+

+ {error ? '请修正上方 JSON 的语法错误以激活流式转换' : t(`jsonFormat:${pk}EmptyHint`)}

)} diff --git a/pages/JsonTools/JsonDiffInput.tsx b/pages/JsonTools/JsonDiffInput.tsx index 590ad42..254cd15 100644 --- a/pages/JsonTools/JsonDiffInput.tsx +++ b/pages/JsonTools/JsonDiffInput.tsx @@ -1,11 +1,16 @@ +import React from 'react'; import TextInputArea from '@/components/TextInputArea'; +import { cn } from '@/lib/utils'; -interface JsonDiffInputProps { +// 💡 核心修复:使用 Omit<..., 'onChange'> 强行挖掉原生的 onChange 签名 +// 这样我们自定义的 (value: string) => void 就能独占鳌头,彻底消灭 TS2430 接口冲突! +export interface JsonDiffInputProps extends Omit, 'onChange'> { label: string; placeholder: string; value: string; onChange: (value: string) => void; error?: string | null; + minRows?: number; } export default function JsonDiffInput({ @@ -14,23 +19,26 @@ export default function JsonDiffInput({ value, onChange, error, + minRows = 10, + className, + ...props }: JsonDiffInputProps) { return ( -
- +
+ {label} +
); } - -export { JsonDiffInput }; -export type { JsonDiffInputProps }; diff --git a/pages/JsonTools/JsonFormatSection.tsx b/pages/JsonTools/JsonFormatSection.tsx index ad13b62..e1f3aa0 100644 --- a/pages/JsonTools/JsonFormatSection.tsx +++ b/pages/JsonTools/JsonFormatSection.tsx @@ -1,5 +1,4 @@ import { useEffect, useMemo, useState } from 'react'; -import { Button } from '@/components/ui/button'; import { useLazyTranslation } from '@/utils/useLazyTranslation'; import { formatJson, @@ -10,149 +9,159 @@ import { import { formatByteSize } from '@/utils/textStatistics'; import CopyButton from '@/components/CopyButton'; import SwitchButtonGroup from '@/components/SwitchButtonGroup'; +import TextInputArea from '@/components/TextInputArea'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Label } from '@/components/ui/label'; -/** 缩进大小选项 */ -const INDENT_OPTIONS = [2, 4, 6, 8] as const; - -/** - * JSON 格式化工具区域组件 - * - * 提供输入区域、格式化选项(缩进大小、键名排序)和格式化结果展示, - * 支持一键复制格式化后的 JSON。 - */ export default function JsonFormatSection() { const { t } = useLazyTranslation('jsonFormat'); const [input, setInput] = useState(''); - const [error, setError] = useState(null); + const [debouncedInput, setDebouncedInput] = useState(''); const [indentSize, setIndentSize] = useState(2); const [sortKeys, setSortKeys] = useState(false); - const [result, setResult] = useState(null); - // 防抖校验输入 + // 1. 高频打字防抖落盘:防止大体积 JSON 在高频输入时发生卡顿 useEffect(() => { const handle = setTimeout(() => { - setError(validateJson(input)); - }, 300); + setDebouncedInput(input); + }, 250); return () => clearTimeout(handle); }, [input]); - const canFormat = useMemo(() => { - return input.trim() !== '' && !error; - }, [input, error]); + // 💡 2. 贯彻方案 A(衍生变量超进化): + // 彻底删除原有的 setError 状态和相关的 useEffect。 + // 语法错误由防抖文本在内存中同步推导,彻底斩断二次级联渲染链条,ESLint 警告瞬间消亡! + const error = useMemo(() => { + return validateJson(debouncedInput); + }, [debouncedInput]); - const handleFormat = () => { - const validationError = validateJson(input); - if (validationError) { - setError(validationError); - setResult(null); - return; - } + // 3. 实时流式格式化管线 + const formattedPipeline = useMemo(() => { + const trimmed = debouncedInput.trim(); + if (!trimmed || error) return null; try { const options: JsonFormatOptions = { indentSize, sortKeys }; - const formatResult = formatJson(input, options); - setResult(formatResult); + return formatJson(debouncedInput, options); } catch (e) { - setError(e instanceof SyntaxError ? e.message : String(e)); - setResult(null); + return { + isRuntimeError: true, + errorMessage: e instanceof SyntaxError ? e.message : String(e), + }; } - }; + }, [debouncedInput, error, indentSize, sortKeys]); - const handleClear = () => { - setInput(''); - setError(null); - setResult(null); - }; + const runtimeError = + formattedPipeline && 'isRuntimeError' in formattedPipeline + ? formattedPipeline.errorMessage + : null; + const result = + formattedPipeline && !('isRuntimeError' in formattedPipeline) + ? (formattedPipeline as JsonFormatResult) + : null; return ( -
- {/* 工具栏 */} -
-
- {/* 缩进选择 */} - - {t('jsonFormat:indentSize')} - - setIndentSize(v)} - options={INDENT_OPTIONS.map((size) => ({ value: size, label: String(size) }))} - size="small" - /> - - {/* 键名排序开关 */} -