用 Tailwind CSS 重写 JsonTools 页面
This commit is contained in:
@@ -1,8 +1,5 @@
|
|||||||
import { Box, IconButton, Typography } from '@mui/material';
|
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||||
import NavigateBeforeIcon from '@mui/icons-material/NavigateBefore';
|
|
||||||
import NavigateNextIcon from '@mui/icons-material/NavigateNext';
|
|
||||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
||||||
import { jsonDiffPageStyles } from '@/config/pageTheme';
|
|
||||||
|
|
||||||
interface DiffNavigatorProps {
|
interface DiffNavigatorProps {
|
||||||
total: number;
|
total: number;
|
||||||
@@ -17,29 +14,34 @@ export default function DiffNavigator({ total, currentIndex, onPrev, onNext }: D
|
|||||||
|
|
||||||
if (total === 0) {
|
if (total === 0) {
|
||||||
return (
|
return (
|
||||||
<Box sx={jsonDiffPageStyles.NAVIGATOR}>
|
<div className="flex items-center justify-center gap-3 p-2.5 rounded-lg bg-blue-50 border border-blue-200">
|
||||||
<Typography variant="body2" sx={{ fontWeight: 700, color: 'text.secondary' }}>
|
<span className="text-sm font-bold text-gray-500">{t('jsonDiff:noDiffs')}</span>
|
||||||
{t('jsonDiff:noDiffs')}
|
</div>
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={jsonDiffPageStyles.NAVIGATOR}>
|
<div className="flex items-center justify-center gap-3 p-2.5 rounded-lg bg-blue-50 border border-blue-200">
|
||||||
<IconButton size="small" aria-label={t('jsonDiff:previousDiff')} onClick={onPrev}>
|
<button
|
||||||
<NavigateBeforeIcon />
|
type="button"
|
||||||
</IconButton>
|
aria-label={t('jsonDiff:previousDiff')}
|
||||||
<Typography
|
onClick={onPrev}
|
||||||
variant="body2"
|
className="p-1 rounded-md hover:bg-blue-100 transition-colors"
|
||||||
sx={{ fontWeight: 800, fontFamily: 'monospace', minWidth: 60, textAlign: 'center' }}
|
|
||||||
>
|
>
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
<span className="text-sm font-extrabold font-mono min-w-[60px] text-center">
|
||||||
{currentIndex + 1} / {total}
|
{currentIndex + 1} / {total}
|
||||||
</Typography>
|
</span>
|
||||||
<IconButton size="small" aria-label={t('jsonDiff:nextDiff')} onClick={onNext}>
|
<button
|
||||||
<NavigateNextIcon />
|
type="button"
|
||||||
</IconButton>
|
aria-label={t('jsonDiff:nextDiff')}
|
||||||
</Box>
|
onClick={onNext}
|
||||||
|
className="p-1 rounded-md hover:bg-blue-100 transition-colors"
|
||||||
|
>
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import { Box, Stack, Typography, useTheme } from '@mui/material';
|
|
||||||
import type { Theme } from '@mui/material/styles';
|
|
||||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
||||||
import { jsonDiffPageStyles, surfaceTint } from '@/config/pageTheme';
|
|
||||||
import JsonTree from './JsonTree';
|
import JsonTree from './JsonTree';
|
||||||
import type { DiffNode, DiffResult as DiffResultType, DiffType, ViewMode } from './types';
|
import type { DiffNode, DiffResult as DiffResultType, DiffType, ViewMode } from './types';
|
||||||
|
|
||||||
@@ -16,41 +13,30 @@ export default function DiffResult({ result, viewMode, activePath }: DiffResultP
|
|||||||
|
|
||||||
if (viewMode === 'sideBySide') {
|
if (viewMode === 'sideBySide') {
|
||||||
return (
|
return (
|
||||||
<Stack direction={{ xs: 'column', md: 'row' }} spacing={2} alignItems="stretch">
|
<div className="flex flex-col md:flex-row gap-4 items-stretch">
|
||||||
<Box sx={{ flex: 1, minWidth: 0 }}>
|
<div className="flex-1 min-w-0">
|
||||||
<SectionLabel text={t('jsonDiff:leftLabel')} />
|
<SectionLabel text={t('jsonDiff:leftLabel')} />
|
||||||
<JsonTree node={result.root} side="left" activePath={activePath} />
|
<JsonTree node={result.root} side="left" activePath={activePath} />
|
||||||
</Box>
|
</div>
|
||||||
<Box sx={{ flex: 1, minWidth: 0 }}>
|
<div className="flex-1 min-w-0">
|
||||||
<SectionLabel text={t('jsonDiff:rightLabel')} />
|
<SectionLabel text={t('jsonDiff:rightLabel')} />
|
||||||
<JsonTree node={result.root} side="right" activePath={activePath} />
|
<JsonTree node={result.root} side="right" activePath={activePath} />
|
||||||
</Box>
|
</div>
|
||||||
</Stack>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={jsonDiffPageStyles.TREE_CONTAINER}>
|
<div className="p-3 rounded-lg bg-white border border-gray-200 font-mono text-sm overflow-x-auto min-h-[200px] max-h-[480px] overflow-y-auto">
|
||||||
<UnifiedView node={result.root} depth={0} activePath={activePath} />
|
<UnifiedView node={result.root} depth={0} activePath={activePath} />
|
||||||
</Box>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const SectionLabel = ({ text }: { text: string }) => (
|
const SectionLabel = ({ text }: { text: string }) => (
|
||||||
<Typography
|
<span className="block mb-1.5 text-[11px] font-extrabold tracking-wider text-gray-500 uppercase">
|
||||||
variant="caption"
|
|
||||||
sx={{
|
|
||||||
display: 'block',
|
|
||||||
mb: 0.6,
|
|
||||||
fontWeight: 800,
|
|
||||||
fontSize: '0.7rem',
|
|
||||||
letterSpacing: 0.4,
|
|
||||||
color: 'text.secondary',
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{text}
|
{text}
|
||||||
</Typography>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
const formatPrimitive = (v: unknown): string => {
|
const formatPrimitive = (v: unknown): string => {
|
||||||
@@ -71,17 +57,17 @@ const prefixForType = (type: DiffType): string => {
|
|||||||
return ' ';
|
return ' ';
|
||||||
};
|
};
|
||||||
|
|
||||||
const colorForType = (type: DiffType): string | undefined => {
|
const colorForType = (type: DiffType): string => {
|
||||||
if (type === 'added') return jsonDiffPageStyles.addedText;
|
if (type === 'added') return 'text-green-600';
|
||||||
if (type === 'removed') return jsonDiffPageStyles.removedText;
|
if (type === 'removed') return 'text-red-600';
|
||||||
if (type === 'modified') return jsonDiffPageStyles.modifiedText;
|
if (type === 'modified') return 'text-amber-600';
|
||||||
return undefined;
|
return 'text-gray-900';
|
||||||
};
|
};
|
||||||
|
|
||||||
const bgForType = (type: DiffType, theme: Theme): string | undefined => {
|
const bgForType = (type: DiffType): string | undefined => {
|
||||||
if (type === 'added') return surfaceTint(theme, theme.palette.success.main, 0.15);
|
if (type === 'added') return 'bg-green-50';
|
||||||
if (type === 'removed') return surfaceTint(theme, theme.palette.error.main, 0.15);
|
if (type === 'removed') return 'bg-red-50';
|
||||||
if (type === 'modified') return surfaceTint(theme, theme.palette.warning.main, 0.15);
|
if (type === 'modified') return 'bg-amber-50';
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -177,29 +163,18 @@ interface UnifiedRowProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const UnifiedRow = ({ depth, type, text, active, multiline }: UnifiedRowProps) => {
|
const UnifiedRow = ({ depth, type, text, active, multiline }: UnifiedRowProps) => {
|
||||||
const theme = useTheme();
|
|
||||||
const color = colorForType(type);
|
const color = colorForType(type);
|
||||||
const bg = bgForType(type, theme);
|
const bg = bgForType(type);
|
||||||
return (
|
return (
|
||||||
<Box
|
<div
|
||||||
sx={{
|
className={`${bg ?? ''} ${color} ${active ? 'ring-2 ring-blue-500 rounded' : ''} ${
|
||||||
pl: depth * 1.5,
|
multiline ? 'whitespace-pre' : 'whitespace-nowrap'
|
||||||
pr: 1,
|
} font-mono`}
|
||||||
py: 0.2,
|
style={{ paddingLeft: `${depth * 1.5}rem`, paddingRight: '0.25rem', paddingBlock: '0.2rem' }}
|
||||||
bgcolor: bg,
|
|
||||||
color: color ?? 'text.primary',
|
|
||||||
outline: active ? '2px solid' : 'none',
|
|
||||||
outlineColor: 'primary.main',
|
|
||||||
borderRadius: 0.5,
|
|
||||||
whiteSpace: multiline ? 'pre' : 'nowrap',
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Box component="span" sx={{ fontWeight: 800 }}>
|
<span className="font-extrabold">{prefixForType(type)}</span>
|
||||||
{prefixForType(type)}
|
<span>{text}</span>
|
||||||
</Box>
|
</div>
|
||||||
<Box component="span">{text}</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { Box, Button, Stack, Typography } from '@mui/material';
|
import { Button } from '@/components/ui/button';
|
||||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
||||||
import { formatByteSize } from '@/utils/textStatistics';
|
import { formatByteSize } from '@/utils/textStatistics';
|
||||||
import { useSnackbar } from '@/components/GlobalSnackbar';
|
import { useSnackbar } from '@/components/GlobalSnackbar';
|
||||||
@@ -88,29 +88,24 @@ export default function JsonConvertSection({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing={2.5}>
|
<div className="flex flex-col gap-6">
|
||||||
{/* 工具栏 */}
|
{/* 工具栏 */}
|
||||||
<Stack
|
<div className="flex flex-col sm:flex-row gap-3 justify-between items-stretch sm:items-center">
|
||||||
direction={{ xs: 'column', sm: 'row' }}
|
<div />
|
||||||
spacing={1.5}
|
<div className="flex gap-2">
|
||||||
justifyContent="space-between"
|
<Button variant="outline" onClick={handleClear} className="rounded-lg">
|
||||||
alignItems={{ xs: 'stretch', sm: 'center' }}
|
|
||||||
>
|
|
||||||
<Box />
|
|
||||||
<Stack direction="row" spacing={1}>
|
|
||||||
<Button variant="text" onClick={handleClear} sx={{ borderRadius: 3 }}>
|
|
||||||
{t('jsonFormat:clearButton')}
|
{t('jsonFormat:clearButton')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="default"
|
||||||
disabled={!canConvert}
|
disabled={!canConvert}
|
||||||
onClick={handleConvert}
|
onClick={handleConvert}
|
||||||
sx={{ borderRadius: 3, fontWeight: 700, px: 3 }}
|
className="rounded-lg font-bold px-4"
|
||||||
>
|
>
|
||||||
{t(`jsonFormat:${convertButtonKey}`)}
|
{t(`jsonFormat:${convertButtonKey}`)}
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</div>
|
||||||
</Stack>
|
</div>
|
||||||
|
|
||||||
{/* 输入区 */}
|
{/* 输入区 */}
|
||||||
<TextInputArea
|
<TextInputArea
|
||||||
@@ -125,81 +120,33 @@ export default function JsonConvertSection({
|
|||||||
|
|
||||||
{/* 转换结果 */}
|
{/* 转换结果 */}
|
||||||
{result && result.output ? (
|
{result && result.output ? (
|
||||||
<Box
|
<div className="relative rounded-lg bg-white border border-gray-200 overflow-hidden">
|
||||||
sx={{
|
|
||||||
position: 'relative',
|
|
||||||
borderRadius: 3,
|
|
||||||
bgcolor: 'background.paper',
|
|
||||||
border: '1px solid',
|
|
||||||
borderColor: 'divider',
|
|
||||||
overflow: 'hidden',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* 结果头部 */}
|
{/* 结果头部 */}
|
||||||
<Stack
|
<div className="flex justify-between items-center px-4 py-2 border-b border-gray-200 bg-gray-50">
|
||||||
direction="row"
|
<div className="flex gap-4 items-center">
|
||||||
justifyContent="space-between"
|
<span className="text-[11px] font-extrabold text-gray-500">
|
||||||
alignItems="center"
|
|
||||||
sx={{
|
|
||||||
px: 2,
|
|
||||||
py: 1,
|
|
||||||
borderBottom: '1px solid',
|
|
||||||
borderColor: 'divider',
|
|
||||||
bgcolor: (theme) =>
|
|
||||||
theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.03)' : 'grey.50',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Stack direction="row" spacing={2} alignItems="center">
|
|
||||||
<Typography
|
|
||||||
variant="caption"
|
|
||||||
sx={{ fontWeight: 800, color: 'text.secondary', fontSize: '0.7rem' }}
|
|
||||||
>
|
|
||||||
{t(`jsonFormat:${pk}OutputLabel`)}
|
{t(`jsonFormat:${pk}OutputLabel`)}
|
||||||
</Typography>
|
</span>
|
||||||
<Typography variant="caption" sx={{ color: 'text.disabled', fontSize: '0.65rem' }}>
|
<span className="text-[10px] text-gray-400">
|
||||||
{t('jsonFormat:originalSize')}: {formatByteSize(result.originalBytes)}
|
{t('jsonFormat:originalSize')}: {formatByteSize(result.originalBytes)}
|
||||||
</Typography>
|
</span>
|
||||||
<Typography variant="caption" sx={{ color: 'text.disabled', fontSize: '0.65rem' }}>
|
<span className="text-[10px] text-gray-400">
|
||||||
{t('jsonFormat:formattedSize')}: {formatByteSize(result.outputBytes)}
|
{t('jsonFormat:formattedSize')}: {formatByteSize(result.outputBytes)}
|
||||||
</Typography>
|
</span>
|
||||||
</Stack>
|
</div>
|
||||||
<CopyButton text={result.output} showMessage={showMessage} />
|
<CopyButton text={result.output} showMessage={showMessage} />
|
||||||
</Stack>
|
</div>
|
||||||
|
|
||||||
{/* 转换内容 */}
|
{/* 转换内容 */}
|
||||||
<Box
|
<div className="p-4 font-mono text-sm whitespace-pre-wrap break-all max-h-[400px] overflow-y-auto leading-relaxed">
|
||||||
sx={{
|
|
||||||
p: 2,
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
fontSize: '0.8rem',
|
|
||||||
whiteSpace: 'pre-wrap',
|
|
||||||
wordBreak: 'break-all',
|
|
||||||
maxHeight: 400,
|
|
||||||
overflowY: 'auto',
|
|
||||||
lineHeight: 1.6,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{result.output}
|
{result.output}
|
||||||
</Box>
|
</div>
|
||||||
</Box>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Box
|
<div className="p-4 rounded-lg bg-gray-50 border border-dashed border-gray-300 text-center">
|
||||||
sx={{
|
<p className="text-sm font-semibold text-gray-500">{t(`jsonFormat:${pk}EmptyHint`)}</p>
|
||||||
p: 3,
|
</div>
|
||||||
borderRadius: 3,
|
|
||||||
bgcolor: (theme) =>
|
|
||||||
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',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ fontWeight: 600 }}>
|
|
||||||
{t(`jsonFormat:${pk}EmptyHint`)}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { Box, Typography } from '@mui/material';
|
|
||||||
import TextInputArea from '@/components/TextInputArea';
|
import TextInputArea from '@/components/TextInputArea';
|
||||||
|
|
||||||
interface JsonDiffInputProps {
|
interface JsonDiffInputProps {
|
||||||
@@ -17,21 +16,10 @@ export default function JsonDiffInput({
|
|||||||
error,
|
error,
|
||||||
}: JsonDiffInputProps) {
|
}: JsonDiffInputProps) {
|
||||||
return (
|
return (
|
||||||
<Box sx={{ flex: 1, minWidth: 0 }}>
|
<div className="flex-1 min-w-0">
|
||||||
<Typography
|
<span className="block mb-1.5 text-[11px] font-extrabold tracking-wider text-gray-500 uppercase">
|
||||||
variant="caption"
|
|
||||||
sx={{
|
|
||||||
display: 'block',
|
|
||||||
mb: 0.6,
|
|
||||||
fontWeight: 800,
|
|
||||||
fontSize: '0.7rem',
|
|
||||||
letterSpacing: 0.4,
|
|
||||||
color: 'text.secondary',
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{label}
|
{label}
|
||||||
</Typography>
|
</span>
|
||||||
<TextInputArea
|
<TextInputArea
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
@@ -41,7 +29,7 @@ export default function JsonDiffInput({
|
|||||||
externalError={error ?? undefined}
|
externalError={error ?? undefined}
|
||||||
showClear={true}
|
showClear={true}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,5 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import {
|
import { Button } from '@/components/ui/button';
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
FormControlLabel,
|
|
||||||
FormHelperText,
|
|
||||||
Stack,
|
|
||||||
Switch,
|
|
||||||
TextField,
|
|
||||||
Typography,
|
|
||||||
} from '@mui/material';
|
|
||||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
||||||
import {
|
import {
|
||||||
formatJson,
|
formatJson,
|
||||||
@@ -18,7 +9,6 @@ import {
|
|||||||
} from '@/utils/jsonFormatter';
|
} from '@/utils/jsonFormatter';
|
||||||
import { formatByteSize } from '@/utils/textStatistics';
|
import { formatByteSize } from '@/utils/textStatistics';
|
||||||
import { useSnackbar } from '@/components/GlobalSnackbar';
|
import { useSnackbar } from '@/components/GlobalSnackbar';
|
||||||
import { jsonDiffPageStyles } from '@/config/pageTheme';
|
|
||||||
import CopyButton from '@/components/CopyButton';
|
import CopyButton from '@/components/CopyButton';
|
||||||
import SwitchButtonGroup from '@/components/SwitchButtonGroup';
|
import SwitchButtonGroup from '@/components/SwitchButtonGroup';
|
||||||
|
|
||||||
@@ -78,22 +68,14 @@ export default function JsonFormatSection() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing={2.5}>
|
<div className="flex flex-col gap-6">
|
||||||
{/* 工具栏 */}
|
{/* 工具栏 */}
|
||||||
<Stack
|
<div className="flex flex-col sm:flex-row gap-3 justify-between items-stretch sm:items-center">
|
||||||
direction={{ xs: 'column', sm: 'row' }}
|
<div className="flex gap-3 items-center">
|
||||||
spacing={1.5}
|
|
||||||
justifyContent="space-between"
|
|
||||||
alignItems={{ xs: 'stretch', sm: 'center' }}
|
|
||||||
>
|
|
||||||
<Stack direction="row" spacing={1.5} alignItems="center">
|
|
||||||
{/* 缩进选择 */}
|
{/* 缩进选择 */}
|
||||||
<Typography
|
<span className="text-[11px] font-extrabold text-gray-500">
|
||||||
variant="caption"
|
|
||||||
sx={{ fontWeight: 800, color: 'text.secondary', fontSize: '0.7rem' }}
|
|
||||||
>
|
|
||||||
{t('jsonFormat:indentSize')}
|
{t('jsonFormat:indentSize')}
|
||||||
</Typography>
|
</span>
|
||||||
<SwitchButtonGroup
|
<SwitchButtonGroup
|
||||||
value={indentSize}
|
value={indentSize}
|
||||||
onChange={(v) => setIndentSize(v)}
|
onChange={(v) => setIndentSize(v)}
|
||||||
@@ -103,134 +85,79 @@ export default function JsonFormatSection() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 键名排序开关 */}
|
{/* 键名排序开关 */}
|
||||||
<FormControlLabel
|
<label className="flex items-center gap-2 ml-2">
|
||||||
control={
|
<input
|
||||||
<Switch
|
type="checkbox"
|
||||||
size="small"
|
checked={sortKeys}
|
||||||
checked={sortKeys}
|
onChange={(e) => setSortKeys(e.target.checked)}
|
||||||
onChange={(e) => setSortKeys(e.target.checked)}
|
className="h-4 w-4 rounded border-gray-300 text-blue-500 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
}
|
<span className="text-xs font-bold">{t('jsonFormat:sortKeys')}</span>
|
||||||
label={
|
</label>
|
||||||
<Typography variant="caption" sx={{ fontWeight: 700, fontSize: '0.7rem' }}>
|
</div>
|
||||||
{t('jsonFormat:sortKeys')}
|
|
||||||
</Typography>
|
|
||||||
}
|
|
||||||
sx={{ ml: 1 }}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<Stack direction="row" spacing={1}>
|
<div className="flex gap-2">
|
||||||
<Button variant="text" onClick={handleClear} sx={{ borderRadius: 3 }}>
|
<Button variant="outline" onClick={handleClear} className="rounded-lg">
|
||||||
{t('jsonFormat:clearButton')}
|
{t('jsonFormat:clearButton')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="default"
|
||||||
disabled={!canFormat}
|
disabled={!canFormat}
|
||||||
onClick={handleFormat}
|
onClick={handleFormat}
|
||||||
sx={{ borderRadius: 3, fontWeight: 700, px: 3 }}
|
className="rounded-lg font-bold px-4"
|
||||||
>
|
>
|
||||||
{t('jsonFormat:formatButton')}
|
{t('jsonFormat:formatButton')}
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</div>
|
||||||
</Stack>
|
</div>
|
||||||
|
|
||||||
{/* 输入区 */}
|
{/* 输入区 */}
|
||||||
<Box>
|
<div>
|
||||||
<TextField
|
<textarea
|
||||||
multiline
|
|
||||||
rows={6}
|
|
||||||
fullWidth
|
|
||||||
placeholder={t('jsonFormat:inputPlaceholder')}
|
placeholder={t('jsonFormat:inputPlaceholder')}
|
||||||
value={input}
|
value={input}
|
||||||
onChange={(e) => setInput(e.target.value)}
|
onChange={(e) => setInput(e.target.value)}
|
||||||
error={Boolean(error)}
|
rows={6}
|
||||||
sx={jsonDiffPageStyles.INPUT_STYLE}
|
className={`w-full rounded-lg border ${
|
||||||
|
error ? 'border-red-300' : 'border-gray-200'
|
||||||
|
} p-3 font-mono text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 resize-y`}
|
||||||
/>
|
/>
|
||||||
{error && (
|
{error && (
|
||||||
<FormHelperText error sx={{ mx: 1.5, mt: 0.5, fontWeight: 600 }}>
|
<p className="mx-3 mt-1 text-xs font-semibold text-red-500">
|
||||||
{t('jsonFormat:invalidJson')}
|
{t('jsonFormat:invalidJson')}
|
||||||
</FormHelperText>
|
</p>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</div>
|
||||||
|
|
||||||
{/* 格式化结果 */}
|
{/* 格式化结果 */}
|
||||||
{result && result.formatted ? (
|
{result && result.formatted ? (
|
||||||
<Box
|
<div className="relative rounded-lg bg-white border border-gray-200 overflow-hidden">
|
||||||
sx={{
|
|
||||||
position: 'relative',
|
|
||||||
borderRadius: 3,
|
|
||||||
bgcolor: 'background.paper',
|
|
||||||
border: '1px solid',
|
|
||||||
borderColor: 'divider',
|
|
||||||
overflow: 'hidden',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* 结果头部 */}
|
{/* 结果头部 */}
|
||||||
<Stack
|
<div className="flex justify-between items-center px-4 py-2 border-b border-gray-200 bg-gray-50">
|
||||||
direction="row"
|
<div className="flex gap-4 items-center">
|
||||||
justifyContent="space-between"
|
<span className="text-[11px] font-extrabold text-gray-500">
|
||||||
alignItems="center"
|
|
||||||
sx={{
|
|
||||||
px: 2,
|
|
||||||
py: 1,
|
|
||||||
borderBottom: '1px solid',
|
|
||||||
borderColor: 'divider',
|
|
||||||
bgcolor: (theme) =>
|
|
||||||
theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.03)' : 'grey.50',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Stack direction="row" spacing={2} alignItems="center">
|
|
||||||
<Typography
|
|
||||||
variant="caption"
|
|
||||||
sx={{ fontWeight: 800, color: 'text.secondary', fontSize: '0.7rem' }}
|
|
||||||
>
|
|
||||||
{t('jsonFormat:outputLabel')}
|
{t('jsonFormat:outputLabel')}
|
||||||
</Typography>
|
</span>
|
||||||
<Typography variant="caption" sx={{ color: 'text.disabled', fontSize: '0.65rem' }}>
|
<span className="text-[10px] text-gray-400">
|
||||||
{t('jsonFormat:originalSize')}: {formatByteSize(result.originalBytes)}
|
{t('jsonFormat:originalSize')}: {formatByteSize(result.originalBytes)}
|
||||||
</Typography>
|
</span>
|
||||||
<Typography variant="caption" sx={{ color: 'text.disabled', fontSize: '0.65rem' }}>
|
<span className="text-[10px] text-gray-400">
|
||||||
{t('jsonFormat:formattedSize')}: {formatByteSize(result.formattedBytes)}
|
{t('jsonFormat:formattedSize')}: {formatByteSize(result.formattedBytes)}
|
||||||
</Typography>
|
</span>
|
||||||
</Stack>
|
</div>
|
||||||
<CopyButton text={result.formatted} showMessage={showMessage} />
|
<CopyButton text={result.formatted} showMessage={showMessage} />
|
||||||
</Stack>
|
</div>
|
||||||
|
|
||||||
{/* 格式化内容 */}
|
{/* 格式化内容 */}
|
||||||
<Box
|
<div className="p-4 font-mono text-sm whitespace-pre-wrap break-all max-h-[400px] overflow-y-auto leading-relaxed">
|
||||||
sx={{
|
|
||||||
p: 2,
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
fontSize: '0.8rem',
|
|
||||||
whiteSpace: 'pre-wrap',
|
|
||||||
wordBreak: 'break-all',
|
|
||||||
maxHeight: 400,
|
|
||||||
overflowY: 'auto',
|
|
||||||
lineHeight: 1.6,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{result.formatted}
|
{result.formatted}
|
||||||
</Box>
|
</div>
|
||||||
</Box>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Box
|
<div className="p-4 rounded-lg bg-gray-50 border border-dashed border-gray-300 text-center">
|
||||||
sx={{
|
<p className="text-sm font-semibold text-gray-500">{t('jsonFormat:emptyHint')}</p>
|
||||||
p: 3,
|
</div>
|
||||||
borderRadius: 3,
|
|
||||||
bgcolor: (theme) =>
|
|
||||||
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',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ fontWeight: 600 }}>
|
|
||||||
{t('jsonFormat:emptyHint')}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import { Box, Collapse, useTheme } from '@mui/material';
|
|
||||||
import type { Theme } from '@mui/material/styles';
|
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { surfaceTint } from '@/config/pageTheme';
|
|
||||||
import type { DiffNode, DiffType } from './types';
|
import type { DiffNode, DiffType } from './types';
|
||||||
|
|
||||||
export type TreeSide = 'left' | 'right';
|
export type TreeSide = 'left' | 'right';
|
||||||
@@ -43,19 +40,19 @@ const getValueForSide = (node: DiffNode, side: TreeSide): unknown => {
|
|||||||
return side === 'left' ? node.oldValue : node.newValue;
|
return side === 'left' ? node.oldValue : node.newValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRowBg = (type: DiffType, side: TreeSide, theme: Theme): string | undefined => {
|
const getRowBg = (type: DiffType, side: TreeSide): string | undefined => {
|
||||||
if (!shouldRenderOnSide(type, side)) return undefined;
|
if (!shouldRenderOnSide(type, side)) return undefined;
|
||||||
if (type === 'added') return surfaceTint(theme, theme.palette.success.main, 0.15);
|
if (type === 'added') return 'bg-green-50';
|
||||||
if (type === 'removed') return surfaceTint(theme, theme.palette.error.main, 0.15);
|
if (type === 'removed') return 'bg-red-50';
|
||||||
if (type === 'modified') return surfaceTint(theme, theme.palette.warning.main, 0.15);
|
if (type === 'modified') return 'bg-amber-50';
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getValueColor = (type: DiffType, side: TreeSide): string | undefined => {
|
const getValueColor = (type: DiffType, side: TreeSide): string | undefined => {
|
||||||
if (!shouldRenderOnSide(type, side)) return undefined;
|
if (!shouldRenderOnSide(type, side)) return undefined;
|
||||||
if (type === 'added') return 'success.main';
|
if (type === 'added') return 'text-green-600';
|
||||||
if (type === 'removed') return 'error.main';
|
if (type === 'removed') return 'text-red-600';
|
||||||
if (type === 'modified') return 'warning.main';
|
if (type === 'modified') return 'text-amber-600';
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -74,7 +71,6 @@ const NodeRow = ({
|
|||||||
// 'auto' = follow defaults + activePath; otherwise user explicitly toggled
|
// 'auto' = follow defaults + activePath; otherwise user explicitly toggled
|
||||||
const [override, setOverride] = useState<'auto' | 'open' | 'closed'>('auto');
|
const [override, setOverride] = useState<'auto' | 'open' | 'closed'>('auto');
|
||||||
const rowRef = useRef<HTMLDivElement | null>(null);
|
const rowRef = useRef<HTMLDivElement | null>(null);
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
const onActivePath = Boolean(
|
const onActivePath = Boolean(
|
||||||
activePath &&
|
activePath &&
|
||||||
@@ -99,13 +95,17 @@ const NodeRow = ({
|
|||||||
|
|
||||||
if (!shouldRenderOnSide(node.type, side)) {
|
if (!shouldRenderOnSide(node.type, side)) {
|
||||||
// 渲染占位空行以保持左右两侧高度一致
|
// 渲染占位空行以保持左右两侧高度一致
|
||||||
return <Box sx={{ pl: depth * 1.5, color: 'transparent', userSelect: 'none' }}>·</Box>;
|
return (
|
||||||
|
<div className="text-transparent select-none" style={{ paddingLeft: `${depth * 1.5}rem` }}>
|
||||||
|
·
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = getValueForSide(node, side);
|
const value = getValueForSide(node, side);
|
||||||
const isContainer = isContainerValue(value) && Array.isArray(node.children);
|
const isContainer = isContainerValue(value) && Array.isArray(node.children);
|
||||||
const isArray = Array.isArray(value);
|
const isArray = Array.isArray(value);
|
||||||
const bg = getRowBg(node.type, side, theme);
|
const bg = getRowBg(node.type, side);
|
||||||
const valueColor = getValueColor(node.type, side);
|
const valueColor = getValueColor(node.type, side);
|
||||||
const isActive = activePath === node.path;
|
const isActive = activePath === node.path;
|
||||||
|
|
||||||
@@ -116,50 +116,29 @@ const NodeRow = ({
|
|||||||
const open = isArray ? '[' : '{';
|
const open = isArray ? '[' : '{';
|
||||||
const close = isArray ? ']' : '}';
|
const close = isArray ? ']' : '}';
|
||||||
return (
|
return (
|
||||||
<Box ref={rowRef}>
|
<div ref={rowRef}>
|
||||||
<Box
|
<div
|
||||||
onClick={() => setOverride(expanded ? 'closed' : 'open')}
|
onClick={() => setOverride(expanded ? 'closed' : 'open')}
|
||||||
sx={{
|
className={`cursor-pointer pr-1 py-0.5 ${bg ?? ''} ${
|
||||||
cursor: 'pointer',
|
isActive ? 'ring-2 ring-blue-500 rounded' : ''
|
||||||
pl: depth * 1.5,
|
} flex items-center gap-1 whitespace-nowrap hover:${bg ? 'bg-opacity-80' : 'bg-gray-50'}`}
|
||||||
pr: 1,
|
style={{ paddingLeft: `${depth * 1.5}rem` }}
|
||||||
py: 0.2,
|
|
||||||
bgcolor: bg,
|
|
||||||
outline: isActive ? '2px solid' : 'none',
|
|
||||||
outlineColor: 'primary.main',
|
|
||||||
borderRadius: 0.5,
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 0.5,
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
'&:hover': { bgcolor: bg ?? 'action.hover' },
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Box component="span" sx={{ width: 12, color: 'text.secondary', fontSize: '0.7rem' }}>
|
<span className="w-3 text-gray-500 text-[11px]">{expanded ? '▾' : '▸'}</span>
|
||||||
{expanded ? '▾' : '▸'}
|
|
||||||
</Box>
|
|
||||||
{!isRoot && (
|
{!isRoot && (
|
||||||
<Box component="span" sx={{ color: 'text.primary', fontWeight: 700 }}>
|
<span className="text-gray-900 font-bold">{isArrayKeyDisplay(node.key)}:</span>
|
||||||
{isArrayKeyDisplay(node.key)}:
|
|
||||||
</Box>
|
|
||||||
)}
|
)}
|
||||||
<Box component="span" sx={{ color: 'text.secondary' }}>
|
<span className="text-gray-500">{open}</span>
|
||||||
{open}
|
{!expanded && <span className="text-gray-400 italic">{summarize(value)}</span>}
|
||||||
</Box>
|
|
||||||
{!expanded && (
|
{!expanded && (
|
||||||
<Box component="span" sx={{ color: 'text.disabled', fontStyle: 'italic' }}>
|
<span className="text-gray-500">
|
||||||
{summarize(value)}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
{!expanded && (
|
|
||||||
<Box component="span" sx={{ color: 'text.secondary' }}>
|
|
||||||
{close}
|
{close}
|
||||||
{isLastChild ? '' : ','}
|
{isLastChild ? '' : ','}
|
||||||
</Box>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</div>
|
||||||
<Collapse in={expanded} unmountOnExit>
|
{expanded && (
|
||||||
<Box>
|
<div>
|
||||||
{node.children.map((child, idx) => (
|
{node.children.map((child, idx) => (
|
||||||
<NodeRow
|
<NodeRow
|
||||||
key={child.path}
|
key={child.path}
|
||||||
@@ -171,52 +150,37 @@ const NodeRow = ({
|
|||||||
isLastChild={idx === node.children!.length - 1}
|
isLastChild={idx === node.children!.length - 1}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</div>
|
||||||
<Box
|
)}
|
||||||
sx={{
|
{expanded && (
|
||||||
pl: depth * 1.5,
|
<div
|
||||||
color: 'text.secondary',
|
className="text-gray-500 whitespace-nowrap"
|
||||||
whiteSpace: 'nowrap',
|
style={{ paddingLeft: `${depth * 1.5 + 1.0625}rem` }}
|
||||||
ml: '17px',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{close}
|
{close}
|
||||||
{isLastChild ? '' : ','}
|
{isLastChild ? '' : ','}
|
||||||
</Box>
|
</div>
|
||||||
</Collapse>
|
)}
|
||||||
</Box>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 叶子节点
|
// 叶子节点
|
||||||
return (
|
return (
|
||||||
<Box
|
<div
|
||||||
ref={rowRef}
|
ref={rowRef}
|
||||||
sx={{
|
className={`pr-1 py-0.5 ${bg ?? ''} ${
|
||||||
pl: depth * 1.5,
|
isActive ? 'ring-2 ring-blue-500 rounded' : ''
|
||||||
pr: 1,
|
} flex items-center gap-1 whitespace-nowrap`}
|
||||||
py: 0.2,
|
style={{ paddingLeft: `${depth * 1.5}rem` }}
|
||||||
bgcolor: bg,
|
|
||||||
outline: isActive ? '2px solid' : 'none',
|
|
||||||
outlineColor: 'primary.main',
|
|
||||||
borderRadius: 0.5,
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 0.5,
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Box component="span" sx={{ width: 12 }} />
|
<span className="w-3" />
|
||||||
{!isRoot && (
|
{!isRoot && <span className="text-gray-900 font-bold">{isArrayKeyDisplay(node.key)}:</span>}
|
||||||
<Box component="span" sx={{ color: 'text.primary', fontWeight: 700 }}>
|
<span className={valueColor ?? 'text-gray-900'}>
|
||||||
{isArrayKeyDisplay(node.key)}:
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
<Box component="span" sx={{ color: valueColor ?? 'text.primary' }}>
|
|
||||||
{formatPrimitive(value)}
|
{formatPrimitive(value)}
|
||||||
{isLastChild ? '' : ','}
|
{isLastChild ? '' : ','}
|
||||||
</Box>
|
</span>
|
||||||
</Box>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -242,21 +206,7 @@ export default function JsonTree({
|
|||||||
}: JsonTreeProps) {
|
}: JsonTreeProps) {
|
||||||
const sideKey = useMemo(() => side, [side]);
|
const sideKey = useMemo(() => side, [side]);
|
||||||
return (
|
return (
|
||||||
<Box
|
<div className="p-3 rounded-lg bg-white border border-gray-200 font-mono text-sm overflow-x-auto min-h-[200px] max-h-[480px] overflow-y-auto">
|
||||||
sx={{
|
|
||||||
p: 1.5,
|
|
||||||
borderRadius: 3,
|
|
||||||
bgcolor: 'background.paper',
|
|
||||||
border: '1px solid',
|
|
||||||
borderColor: 'divider',
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
fontSize: '0.8rem',
|
|
||||||
overflowX: 'auto',
|
|
||||||
minHeight: 200,
|
|
||||||
maxHeight: 480,
|
|
||||||
overflowY: 'auto',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<NodeRow
|
<NodeRow
|
||||||
node={node}
|
node={node}
|
||||||
side={sideKey}
|
side={sideKey}
|
||||||
@@ -265,7 +215,7 @@ export default function JsonTree({
|
|||||||
activePath={activePath}
|
activePath={activePath}
|
||||||
isLastChild
|
isLastChild
|
||||||
/>
|
/>
|
||||||
</Box>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+29
-51
@@ -1,12 +1,8 @@
|
|||||||
import { useEffect, useMemo, useState, useCallback } from 'react';
|
import { useEffect, useMemo, useState, useCallback } from 'react';
|
||||||
import { Box, Button, Container, Stack, Typography } from '@mui/material';
|
import { Button } from '@/components/ui/button';
|
||||||
import CompareArrowsIcon from '@mui/icons-material/CompareArrows';
|
import { GitCompareArrows, Braces, ArrowRightLeft, Minimize2 } from 'lucide-react';
|
||||||
import DataObjectIcon from '@mui/icons-material/DataObject';
|
|
||||||
import TransformIcon from '@mui/icons-material/Transform';
|
|
||||||
import CompressIcon from '@mui/icons-material/Compress';
|
|
||||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
||||||
import PageHeader from '@/components/PageHeader';
|
import PageHeader from '@/components/PageHeader';
|
||||||
import { jsonDiffPageStyles } from '@/config/pageTheme';
|
|
||||||
import JsonDiffInput from './JsonDiffInput';
|
import JsonDiffInput from './JsonDiffInput';
|
||||||
import DiffResult from './DiffResult';
|
import DiffResult from './DiffResult';
|
||||||
import DiffNavigator from './DiffNavigator';
|
import DiffNavigator from './DiffNavigator';
|
||||||
@@ -20,7 +16,7 @@ import { jsonToToml } from '@/utils/jsonToToml';
|
|||||||
import { minifyJson } from '@/utils/jsonFormatter';
|
import { minifyJson } from '@/utils/jsonFormatter';
|
||||||
import { useStorageState } from '@/utils/useStorageState';
|
import { useStorageState } from '@/utils/useStorageState';
|
||||||
import type { JsonToolsPageMode } from '@/types/storage';
|
import type { JsonToolsPageMode } from '@/types/storage';
|
||||||
import SwtichButtonGroup from '@/components/SwitchButtonGroup';
|
import SwitchButtonGroup from '@/components/SwitchButtonGroup';
|
||||||
|
|
||||||
interface ParseState {
|
interface ParseState {
|
||||||
value: unknown;
|
value: unknown;
|
||||||
@@ -118,11 +114,11 @@ export default function Index() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const modeIcon: Record<PageMode, React.ReactNode> = {
|
const modeIcon: Record<PageMode, React.ReactNode> = {
|
||||||
diff: <CompareArrowsIcon />,
|
diff: <GitCompareArrows className="h-5 w-5" />,
|
||||||
format: <DataObjectIcon />,
|
format: <Braces className="h-5 w-5" />,
|
||||||
yaml: <TransformIcon />,
|
yaml: <ArrowRightLeft className="h-5 w-5" />,
|
||||||
toml: <TransformIcon />,
|
toml: <ArrowRightLeft className="h-5 w-5" />,
|
||||||
minify: <CompressIcon />,
|
minify: <Minimize2 className="h-5 w-5" />,
|
||||||
};
|
};
|
||||||
|
|
||||||
const yamlConvert: ConvertFunction = useCallback((text: string) => {
|
const yamlConvert: ConvertFunction = useCallback((text: string) => {
|
||||||
@@ -141,18 +137,18 @@ export default function Index() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<div>
|
||||||
<Container sx={{ p: 2 }}>
|
<div className="p-2">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={t(modeTitles[pageMode].title)}
|
title={t(modeTitles[pageMode].title)}
|
||||||
subtitle={t(modeTitles[pageMode].subtitle)}
|
subtitle={t(modeTitles[pageMode].subtitle)}
|
||||||
icon={modeIcon[pageMode]}
|
icon={modeIcon[pageMode]}
|
||||||
iconColor={jsonDiffPageStyles.primaryColor}
|
iconColor="#3b82f6"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Stack spacing={2.5}>
|
<div className="flex flex-col gap-6">
|
||||||
{/* 页面模式切换器 */}
|
{/* 页面模式切换器 */}
|
||||||
<SwtichButtonGroup
|
<SwitchButtonGroup
|
||||||
value={pageMode}
|
value={pageMode}
|
||||||
onChange={(v: PageMode) => setPageMode(v)}
|
onChange={(v: PageMode) => setPageMode(v)}
|
||||||
options={[
|
options={[
|
||||||
@@ -168,13 +164,8 @@ export default function Index() {
|
|||||||
{pageMode === 'diff' ? (
|
{pageMode === 'diff' ? (
|
||||||
<>
|
<>
|
||||||
{/* 工具栏 */}
|
{/* 工具栏 */}
|
||||||
<Stack
|
<div className="flex flex-col sm:flex-row gap-3 justify-between items-stretch sm:items-center">
|
||||||
direction={{ xs: 'column', sm: 'row' }}
|
<SwitchButtonGroup
|
||||||
spacing={1.5}
|
|
||||||
justifyContent="space-between"
|
|
||||||
alignItems={{ xs: 'stretch', sm: 'center' }}
|
|
||||||
>
|
|
||||||
<SwtichButtonGroup
|
|
||||||
value={viewMode}
|
value={viewMode}
|
||||||
onChange={(v: ViewMode) => setViewMode(v)}
|
onChange={(v: ViewMode) => setViewMode(v)}
|
||||||
options={[
|
options={[
|
||||||
@@ -183,23 +174,23 @@ export default function Index() {
|
|||||||
]}
|
]}
|
||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
<Stack direction="row" spacing={1}>
|
<div className="flex gap-2">
|
||||||
<Button variant="text" onClick={handleClear} sx={{ borderRadius: 3 }}>
|
<Button variant="outline" onClick={handleClear} className="rounded-lg">
|
||||||
{t('jsonDiff:clearButton')}
|
{t('jsonDiff:clearButton')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="default"
|
||||||
disabled={!canCompare}
|
disabled={!canCompare}
|
||||||
onClick={handleCompare}
|
onClick={handleCompare}
|
||||||
sx={{ borderRadius: 3, fontWeight: 700, px: 3, whiteSpace: 'nowrap' }}
|
className="rounded-lg font-bold px-4 whitespace-nowrap"
|
||||||
>
|
>
|
||||||
{t('jsonDiff:compareButton')}
|
{t('jsonDiff:compareButton')}
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</div>
|
||||||
</Stack>
|
</div>
|
||||||
|
|
||||||
{/* 输入区 */}
|
{/* 输入区 */}
|
||||||
<Stack direction={{ xs: 'column', md: 'row' }} spacing={2}>
|
<div className="flex flex-col md:flex-row gap-4">
|
||||||
<JsonDiffInput
|
<JsonDiffInput
|
||||||
label={t('jsonDiff:leftLabel')}
|
label={t('jsonDiff:leftLabel')}
|
||||||
placeholder={t('jsonDiff:leftPlaceholder')}
|
placeholder={t('jsonDiff:leftPlaceholder')}
|
||||||
@@ -214,7 +205,7 @@ export default function Index() {
|
|||||||
onChange={setRightInput}
|
onChange={setRightInput}
|
||||||
error={rightError}
|
error={rightError}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</div>
|
||||||
|
|
||||||
{/* 差异展示 */}
|
{/* 差异展示 */}
|
||||||
{diffResult ? (
|
{diffResult ? (
|
||||||
@@ -228,22 +219,9 @@ export default function Index() {
|
|||||||
<DiffResult result={diffResult} viewMode={viewMode} activePath={activePath} />
|
<DiffResult result={diffResult} viewMode={viewMode} activePath={activePath} />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Box
|
<div className="p-4 rounded-lg bg-gray-50 border border-dashed border-gray-300 text-center">
|
||||||
sx={{
|
<p className="text-sm font-semibold text-gray-500">{t('jsonDiff:emptyHint')}</p>
|
||||||
p: 3,
|
</div>
|
||||||
borderRadius: 3,
|
|
||||||
bgcolor: (theme) =>
|
|
||||||
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',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ fontWeight: 600 }}>
|
|
||||||
{t('jsonDiff:emptyHint')}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : pageMode === 'format' ? (
|
) : pageMode === 'format' ? (
|
||||||
@@ -259,8 +237,8 @@ export default function Index() {
|
|||||||
convertButtonKey="minifyButton"
|
convertButtonKey="minifyButton"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</div>
|
||||||
</Container>
|
</div>
|
||||||
</Box>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user