import { useEffect, useMemo, useState, useCallback } from 'react'; import { Box, Button, Container, Stack, ToggleButton, ToggleButtonGroup, Typography, } from '@mui/material'; import CompareArrowsIcon from '@mui/icons-material/CompareArrows'; import DataObjectIcon from '@mui/icons-material/DataObject'; import TransformIcon from '@mui/icons-material/Transform'; import CompressIcon from '@mui/icons-material/Compress'; import { useTranslation } from 'react-i18next'; import PageHeader from '@/components/PageHeader'; import { jsonDiffPageStyles } from '@/config/pageTheme'; import JsonDiffInput from './JsonDiffInput'; import DiffResult from './DiffResult'; import DiffNavigator from './DiffNavigator'; import JsonFormatSection from './JsonFormatSection'; import JsonConvertSection from './JsonConvertSection'; import type { ConvertFunction } from './JsonConvertSection'; import { diffJson } from './diffEngine'; import type { DiffResult as DiffResultType, ViewMode } from './types'; import { jsonToYaml } from '@/utils/jsonToYaml'; import { jsonToToml } from '@/utils/jsonToToml'; import { minifyJson } from '@/utils/jsonFormatter'; import { useStorageState } from '@/utils/useStorageState'; import type { JsonToolsPageMode } from '@/types/storage'; interface ParseState { value: unknown; error: string | null; } const tryParse = (raw: string, invalidMsg: string): ParseState => { const trimmed = raw.trim(); if (!trimmed) return { value: undefined, error: null }; try { return { value: JSON.parse(trimmed), error: null }; } catch { return { value: undefined, error: invalidMsg }; } }; /** 页面模式 */ const VALID_PAGE_MODES: readonly JsonToolsPageMode[] = ['diff', 'format', 'yaml', 'toml', 'minify']; const isValidPageMode = (val: unknown): val is JsonToolsPageMode => typeof val === 'string' && (VALID_PAGE_MODES as readonly string[]).includes(val); type PageMode = JsonToolsPageMode; export default function Index() { const { t } = useTranslation(['jsonDiff', 'jsonFormat']); const [pageMode, setPageMode] = useStorageState('jsonTools/pageMode', 'diff', isValidPageMode); const [leftInput, setLeftInput] = useState(''); const [rightInput, setRightInput] = useState(''); const [leftError, setLeftError] = useState(null); const [rightError, setRightError] = useState(null); const [diffResult, setDiffResult] = useState(null); const [viewMode, setViewMode] = useState('sideBySide'); const [currentDiffIndex, setCurrentDiffIndex] = useState(0); // 防抖校验输入 useEffect(() => { const handle = setTimeout(() => { const invalid = t('jsonDiff:invalidJson'); setLeftError(tryParse(leftInput, invalid).error); setRightError(tryParse(rightInput, invalid).error); }, 300); return () => clearTimeout(handle); }, [leftInput, rightInput, t]); const canCompare = useMemo(() => { return leftInput.trim() !== '' && rightInput.trim() !== '' && !leftError && !rightError; }, [leftInput, rightInput, leftError, rightError]); const handleCompare = () => { const invalid = t('jsonDiff:invalidJson'); const left = tryParse(leftInput, invalid); const right = tryParse(rightInput, invalid); setLeftError(left.error); setRightError(right.error); if (left.error || right.error) { setDiffResult(null); return; } const result = diffJson(left.value, right.value); setDiffResult(result); setCurrentDiffIndex(0); }; const handleClear = () => { setLeftInput(''); setRightInput(''); setLeftError(null); setRightError(null); setDiffResult(null); setCurrentDiffIndex(0); }; const total = diffResult?.diffPaths.length ?? 0; const handlePrev = () => { if (total === 0) return; setCurrentDiffIndex((idx) => (idx - 1 + total) % total); }; const handleNext = () => { if (total === 0) return; setCurrentDiffIndex((idx) => (idx + 1) % total); }; const activePath = diffResult && total > 0 ? diffResult.diffPaths[currentDiffIndex] : undefined; /** 页面模式对应的标题和副标题翻译键 */ const modeTitles: Record = { diff: { title: 'jsonDiff:pageTitle', subtitle: 'jsonDiff:pageSubtitle' }, format: { title: 'jsonFormat:formatTitle', subtitle: 'jsonFormat:formatSubtitle' }, yaml: { title: 'jsonFormat:yamlTitle', subtitle: 'jsonFormat:yamlSubtitle' }, toml: { title: 'jsonFormat:tomlTitle', subtitle: 'jsonFormat:tomlSubtitle' }, minify: { title: 'jsonFormat:minifyTitle', subtitle: 'jsonFormat:minifySubtitle' }, }; const modeIcon: Record = { diff: , format: , yaml: , toml: , minify: , }; const yamlConvert: ConvertFunction = useCallback((text: string) => { const r = jsonToYaml(text); return { output: r.output, originalBytes: r.originalBytes, outputBytes: r.outputBytes }; }, []); const tomlConvert: ConvertFunction = useCallback((text: string) => { const r = jsonToToml(text); return { output: r.output, originalBytes: r.originalBytes, outputBytes: r.outputBytes }; }, []); const minifyConvert: ConvertFunction = useCallback((text: string) => { const r = minifyJson(text); return { output: r.minified, originalBytes: r.originalBytes, outputBytes: r.minifiedBytes }; }, []); return ( {/* 页面模式切换器 */} v && setPageMode(v)} sx={{ borderRadius: 3, flexWrap: 'wrap', gap: 0.5 }} > {t('jsonFormat:diffMode')} {t('jsonFormat:formatMode')} {t('jsonFormat:yamlMode')} {t('jsonFormat:tomlMode')} {t('jsonFormat:minifyMode')} {pageMode === 'diff' ? ( <> {/* 工具栏 */} v && setViewMode(v)} sx={{ borderRadius: 3 }} > {t('jsonDiff:sideBySideMode')} {t('jsonDiff:unifiedMode')} {/* 输入区 */} {/* 差异展示 */} {diffResult ? ( <> ) : ( theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.03)' : 'grey.50', border: '1px dashed', borderColor: (theme) => theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.15)' : 'grey.300', textAlign: 'center', }} > {t('jsonDiff:emptyHint')} )} ) : pageMode === 'format' ? ( ) : pageMode === 'yaml' ? ( ) : pageMode === 'toml' ? ( ) : ( )} ); }