Files
testing-tool/pages/JsonTools/DiffResult.tsx
T
雨霖铃 de7da9f1db fix: 修复暗黑模式样式兼容性
- 在 tailwind.config.js 中启用 darkMode: 'class' 并扩展语义化颜色变量
- 将全项目硬编码颜色(bg-white、text-gray-*、border-gray-* 等)替换为语义化类名
- 修复 popup/index.html 硬编码浅色背景色
- 同步更新相关单元测试中的类名断言
2026-05-22 16:04:50 +08:00

196 lines
5.7 KiB
TypeScript

import { useLazyTranslation } from '@/utils/useLazyTranslation';
import JsonTree from './JsonTree';
import type { DiffNode, DiffResult as DiffResultType, DiffType, ViewMode } from './types';
interface DiffResultProps {
result: DiffResultType;
viewMode: ViewMode;
activePath?: string;
}
export default function DiffResult({ result, viewMode, activePath }: DiffResultProps) {
const { t } = useLazyTranslation('jsonDiff');
if (viewMode === 'sideBySide') {
return (
<div className="flex flex-col md:flex-row gap-4 items-stretch">
<div className="flex-1 min-w-0">
<SectionLabel text={t('jsonDiff:leftLabel')} />
<JsonTree node={result.root} side="left" activePath={activePath} />
</div>
<div className="flex-1 min-w-0">
<SectionLabel text={t('jsonDiff:rightLabel')} />
<JsonTree node={result.root} side="right" activePath={activePath} />
</div>
</div>
);
}
return (
<div className="p-3 rounded-lg bg-background border border-border font-mono text-sm overflow-x-auto min-h-[200px] max-h-[480px] overflow-y-auto">
<UnifiedView node={result.root} depth={0} activePath={activePath} />
</div>
);
}
const SectionLabel = ({ text }: { text: string }) => (
<span className="block mb-1.5 text-[11px] font-extrabold tracking-wider text-muted-foreground uppercase">
{text}
</span>
);
const formatPrimitive = (v: unknown): string => {
if (v === undefined) return 'undefined';
if (v === null) return 'null';
if (typeof v === 'string') return JSON.stringify(v);
if (typeof v === 'number' || typeof v === 'boolean') return String(v);
return JSON.stringify(v);
};
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 ' ';
};
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;
};
interface UnifiedViewProps {
node: DiffNode;
depth: number;
activePath?: string;
}
const UnifiedView = ({ node, depth, activePath }: UnifiedViewProps) => {
const isRoot = depth === 0;
const hasChildren = Array.isArray(node.children) && node.children.length > 0;
const isContainer =
hasChildren || isContainerType(node.oldValue) || isContainerType(node.newValue);
const keyLabel = isRoot ? '' : `${node.key}: `;
if (!isContainer) {
// 叶子节点
if (node.type === 'modified') {
return (
<>
<UnifiedRow
depth={depth}
type="removed"
text={`${keyLabel}${formatPrimitive(node.oldValue)}`}
active={activePath === node.path}
/>
<UnifiedRow
depth={depth}
type="added"
text={`${keyLabel}${formatPrimitive(node.newValue)}`}
active={activePath === node.path}
/>
</>
);
}
const value = node.type === 'added' ? node.newValue : node.oldValue;
return (
<UnifiedRow
depth={depth}
type={node.type}
text={`${keyLabel}${formatPrimitive(value)}`}
active={activePath === node.path}
/>
);
}
// 容器节点:added/removed 整块呈现
if (node.type === 'added') {
return (
<UnifiedRow
depth={depth}
type="added"
text={`${keyLabel}${stringifyMultiline(node.newValue, depth)}`}
active={activePath === node.path}
multiline
/>
);
}
if (node.type === 'removed') {
return (
<UnifiedRow
depth={depth}
type="removed"
text={`${keyLabel}${stringifyMultiline(node.oldValue, depth)}`}
active={activePath === node.path}
multiline
/>
);
}
const isArr = Array.isArray(node.oldValue) || Array.isArray(node.newValue);
const open = isArr ? '[' : '{';
const close = isArr ? ']' : '}';
return (
<>
<UnifiedRow depth={depth} type="unchanged" text={`${keyLabel}${open}`} />
{node.children?.map((child) => (
<UnifiedView key={child.path} node={child} depth={depth + 1} activePath={activePath} />
))}
<UnifiedRow depth={depth} type="unchanged" text={close} />
</>
);
};
interface UnifiedRowProps {
depth: number;
type: DiffType;
text: string;
active?: boolean;
multiline?: boolean;
}
const UnifiedRow = ({ depth, type, text, active, multiline }: UnifiedRowProps) => {
const color = colorForType(type);
const bg = bgForType(type);
return (
<div
className={`${bg ?? ''} ${color} ${active ? 'ring-2 ring-blue-500 rounded' : ''} ${
multiline ? 'whitespace-pre' : 'whitespace-nowrap'
} font-mono`}
style={{ paddingLeft: `${depth * 1.5}rem`, paddingRight: '0.25rem', paddingBlock: '0.2rem' }}
>
<span className="font-extrabold">{prefixForType(type)}</span>
<span>{text}</span>
</div>
);
};
const stringifyMultiline = (v: unknown, depth: number): string => {
try {
const json = JSON.stringify(v, null, 2);
if (!json) return formatPrimitive(v);
const indent = ' '.repeat(depth);
return json
.split('\n')
.map((line, idx) => (idx === 0 ? line : indent + line))
.join('\n');
} catch {
return formatPrimitive(v);
}
};
export type { DiffResultProps };