refactor(JsonTools): 规范化页面组件目录结构

- 将子组件移至 components/ 子目录(JsonDiffInput、DiffResult、JsonFormatSection、JsonConvertSection、DiffNavigator、JsonTree)
- 将 diffEngine.ts 工具函数移至 src/utils/
- 提取 ConvertFunction/ConvertResult 类型到 types.ts,解决循环依赖
- 创建 __tests__/ 目录结构
- 更新所有相关导入路径

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
雨霖铃
2026-06-10 20:39:40 +08:00
parent 30d3561726
commit 016173a9c5
12 changed files with 33 additions and 20 deletions
@@ -0,0 +1,11 @@
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import Index from '../index';
describe('JsonTools 页面', () => {
it('应该渲染页面标题', () => {
render(<Index />);
// 基本渲染测试
expect(screen.getByRole('button')).toBeInTheDocument();
});
});
@@ -2,7 +2,7 @@ import React from 'react';
import { useI18n } from '@/utils/chromeI18n';
import { cn } from '@/lib/utils';
import JsonTree from './JsonTree';
import type { DiffNode, DiffResult as DiffResultType, DiffType, ViewMode } from './types';
import type { DiffNode, DiffResult as DiffResultType, DiffType, ViewMode } from '../types';
export interface DiffResultProps extends React.HTMLAttributes<HTMLDivElement> {
result: DiffResultType;
@@ -5,14 +5,7 @@ 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;
import type { ConvertFunction, ConvertResult } from '../types';
interface JsonConvertSectionProps extends React.HTMLAttributes<HTMLDivElement> {
translationPrefix: string;
@@ -1,6 +1,6 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { ChevronDown, ChevronRight } from 'lucide-react'; // 用正统的矢量箭头平替原生的字符 '▾' '▸'
import type { DiffNode, DiffType } from './types';
import type { DiffNode, DiffType } from '../types';
import { cn } from '@/lib/utils';
export type TreeSide = 'left' | 'right';
-189
View File
@@ -1,189 +0,0 @@
import type { DiffNode, DiffResult, DiffType } from './types';
const ROOT_PATH = '$';
const SENTINEL = Symbol('missing');
type MaybeMissing = unknown | typeof SENTINEL;
const isObject = (v: unknown): v is Record<string, unknown> =>
typeof v === 'object' && v !== null && !Array.isArray(v);
const isArray = (v: unknown): v is unknown[] => Array.isArray(v);
/**
* 健壮的 JSONPath 生成器:支持针对包含点号、空格或特殊字符的键名进行括号转义拦截
*/
const buildPath = (parent: string, key: string, isArrayChild: boolean): string => {
if (isArrayChild) {
return `${parent}[${key}]`;
}
const needsEscaping = key.includes('.') || key.includes('[') || key.includes(' ');
const formattedKey = needsEscaping ? `["${key}"]` : `.${key}`;
return parent === ROOT_PATH ? `${ROOT_PATH}${formattedKey}` : `${parent}${formattedKey}`;
};
const primitiveEqual = (a: unknown, b: unknown): boolean => {
if (typeof a === 'number' && typeof b === 'number') {
return Object.is(a, b);
}
return a === b;
};
const diffNode = (
left: MaybeMissing,
right: MaybeMissing,
key: string,
path: string,
diffPaths: string[],
): DiffNode => {
// 分支 1:节点增加行为拦截 (叶子节点状态)
if (left === SENTINEL && right !== SENTINEL) {
diffPaths.push(path);
return {
key,
type: 'added',
oldValue: undefined,
newValue: right,
path,
isLeaf: !isObject(right) && !isArray(right),
hasDiffInChildren: false, // 自身即是新增,子树无需向下检索
};
}
// 分支 2:节点删除行为拦截 (叶子节点状态)
if (right === SENTINEL && left !== SENTINEL) {
diffPaths.push(path);
return {
key,
type: 'removed',
oldValue: left,
newValue: undefined,
path,
isLeaf: !isObject(left) && !isArray(left),
hasDiffInChildren: false, // 自身即是删除,子树无需向下检索
};
}
const leftObj = isObject(left);
const rightObj = isObject(right);
const leftArr = isArray(left);
const rightArr = isArray(right);
// 分支 3:双对象深层递归 (容器状态)
if (leftObj && rightObj) {
const keySet = new Set<string>();
const leftKeys = Object.keys(left);
const rightKeys = Object.keys(right);
for (let i = 0; i < leftKeys.length; i++) keySet.add(leftKeys[i]);
for (let i = 0; i < rightKeys.length; i++) keySet.add(rightKeys[i]);
const children: DiffNode[] = [];
keySet.forEach((k) => {
const childPath = buildPath(path, k, false);
const l: MaybeMissing = k in left ? left[k] : SENTINEL;
const r: MaybeMissing = k in right ? right[k] : SENTINEL;
children.push(diffNode(l, r, k, childPath, diffPaths));
});
const hasDiffInChildren = children.some((c) => c.type !== 'unchanged' || c.hasDiffInChildren);
return {
key,
type: hasDiffInChildren ? 'modified' : 'unchanged',
oldValue: left,
newValue: right,
children,
path,
isLeaf: false,
hasDiffInChildren, // 完美注入预计算衍生状态
};
}
// 分支 4:双数组深层按序递归 (容器状态)
if (leftArr && rightArr) {
const len = Math.max(left.length, right.length);
const children: DiffNode[] = new Array(len);
for (let i = 0; i < len; i++) {
const k = String(i);
const childPath = buildPath(path, k, true);
const l: MaybeMissing = i < left.length ? left[i] : SENTINEL;
const r: MaybeMissing = i < right.length ? right[i] : SENTINEL;
children[i] = diffNode(l, r, k, childPath, diffPaths);
}
const hasDiffInChildren = children.some((c) => c.type !== 'unchanged' || c.hasDiffInChildren);
return {
key,
type: hasDiffInChildren ? 'modified' : 'unchanged',
oldValue: left,
newValue: right,
children,
path,
isLeaf: false,
hasDiffInChildren, // 完美注入预计算衍生状态
};
}
// 分支 5:绝对类型安全防护大闸 (双基本基元比对)
const leftIsContainer = leftObj || leftArr;
const rightIsContainer = rightObj || rightArr;
if (!leftIsContainer && !rightIsContainer) {
if (left === null || right === null) {
if (left === null && right === null) {
return {
key,
type: 'unchanged',
oldValue: left,
newValue: right,
path,
isLeaf: true,
hasDiffInChildren: false,
};
}
} else if (typeof left === typeof right) {
if (primitiveEqual(left, right)) {
return {
key,
type: 'unchanged',
oldValue: left,
newValue: right,
path,
isLeaf: true,
hasDiffInChildren: false,
};
}
}
}
// 类型完全发生突变错配,或者基本数值不相等
diffPaths.push(path);
return {
key,
type: 'modified',
oldValue: left,
newValue: right,
path,
isLeaf: !leftIsContainer && !rightIsContainer,
hasDiffInChildren: false, // 变动在自身,后代无子树变动
};
};
/**
* 比较两个 JSON 值的差异,返回安全的差异树及高精度差异路径列表。
*/
export const diffJson = (left: unknown, right: unknown): DiffResult => {
const diffPaths: string[] = [];
const root = diffNode(left, right, '', ROOT_PATH, diffPaths);
return {
root,
diffPaths,
diffCount: diffPaths.length,
};
};
export type { DiffNode, DiffResult, DiffType };
+5 -5
View File
@@ -1,9 +1,9 @@
import { useI18n } from '@/utils/chromeI18n';
import JsonDiffInput from './JsonDiffInput';
import DiffResult from './DiffResult';
import DiffNavigator from './DiffNavigator';
import JsonFormatSection from './JsonFormatSection';
import JsonConvertSection from './JsonConvertSection';
import JsonDiffInput from './components/JsonDiffInput';
import DiffResult from './components/DiffResult';
import DiffNavigator from './components/DiffNavigator';
import JsonFormatSection from './components/JsonFormatSection';
import JsonConvertSection from './components/JsonConvertSection';
import SwitchButtonGroup from '@/components/SwitchButtonGroup';
import { useJsonTools } from './useJsonTools';
import type { JsonToolsPageMode } from '@/types/storage';
+10
View File
@@ -53,3 +53,13 @@ export interface DiffResult {
/** 差异核心总计数(等价于 diffPaths.length),注入 tabular-nums 配合渲染 */
diffCount: number;
}
/** JSON 转换结果 */
export interface ConvertResult {
output: string;
originalBytes: number;
outputBytes: number;
}
/** JSON 转换函数类型 */
export type ConvertFunction = (text: string) => ConvertResult;
+2 -3
View File
@@ -1,14 +1,13 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useI18n } from '@/utils/chromeI18n';
import { useStorageState } from '@/utils/useStorageState';
import { diffJson } from './diffEngine';
import { diffJson } from '@/utils/diffEngine';
import { jsonToYaml } from '@/utils/jsonToYaml';
import { jsonToToml } from '@/utils/jsonToToml';
import { minifyJson } from '@/utils/jsonFormatter';
import { isValidPageMode, tryParse } from './constants';
import type { JsonToolsPageMode } from '@/types/storage';
import type { ConvertFunction } from './JsonConvertSection';
import type { ViewMode } from './types';
import type { ConvertFunction, ViewMode } from './types';
export interface UseJsonToolsReturn {
pageMode: JsonToolsPageMode;