Merge branch 'develop'

# Conflicts:
#	src/pages/JsonTools/__tests__/useJsonTools.test.ts
#	src/pages/JsonTools/components/JsonDiffPanel.tsx
#	src/pages/JsonTools/useJsonTools.ts
This commit is contained in:
2026-07-15 17:25:26 +08:00
10 changed files with 305 additions and 142 deletions
@@ -64,4 +64,15 @@ describe('useJsonTools 窄屏手动比对', () => {
act(() => result.current.toggleCollapseA()); act(() => result.current.toggleCollapseA());
expect(result.current.collapsedA).toBe(false); expect(result.current.collapsedA).toBe(false);
}); });
it('单输入经防抖后持久化,重挂载可恢复', async () => {
localStorage.clear();
const { result, unmount } = renderHook(() => useJsonTools());
act(() => result.current.setInput('{"x":1}'));
await new Promise((r) => setTimeout(r, 300));
unmount();
const { result: result2 } = renderHook(() => useJsonTools());
expect(result2.current.input).toBe('{"x":1}');
});
}); });
@@ -0,0 +1,72 @@
import React from 'react';
import { ChevronRight } from 'lucide-react';
import { cn } from '@/lib/utils';
export interface CollapsiblePanelProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'> {
/** 面板标题 */
title: string;
/** 是否处于折叠态 */
collapsed: boolean;
onToggleCollapse: () => void;
/** 折叠态展示的单行缩略预览文本 */
preview?: string;
/** 展开态内容 */
children?: React.ReactNode;
}
export default function CollapsiblePanel({
title,
collapsed,
onToggleCollapse,
preview,
children,
className,
...props
}: CollapsiblePanelProps) {
return (
<div
className={cn(
'flex flex-col overflow-hidden rounded-md border bg-card transition-all duration-300 ease-in-out',
className,
)}
style={{
flex: collapsed ? '0 0 36px' : '1 1 120px',
}}
{...props}
>
<button
type="button"
onClick={onToggleCollapse}
className={cn(
'flex h-[36px] w-full cursor-pointer select-none items-center justify-between bg-muted/20 hover:bg-muted/30 transition-colors px-3 shrink-0 text-left focus:outline-none focus:bg-muted/40',
!collapsed && 'border-b border-border/60',
)}
>
<span className="text-[11px] font-semibold text-foreground/90 shrink-0">{title}</span>
<span
className={cn(
'flex-1 truncate font-mono text-xs text-muted-foreground/70 ml-3 transition-all duration-300',
collapsed ? 'opacity-100 max-w-full' : 'opacity-0 max-w-0 pointer-events-none',
)}
>
{preview}
</span>
<ChevronRight
className={cn(
'h-4 w-4 shrink-0 text-muted-foreground hover:text-foreground transition-transform duration-300',
!collapsed && 'rotate-90',
)}
/>
</button>
<div
className={cn(
'flex-1 min-h-0 flex flex-col transition-opacity duration-300',
collapsed ? 'opacity-0 pointer-events-none' : 'opacity-100',
)}
>
{children}
</div>
</div>
);
}
@@ -1,55 +1,45 @@
import React, { useEffect, useMemo, useState } from 'react'; import React, { useMemo } from 'react';
import EmptyPlaceholder from '@/components/EmptyPlaceholder';
import TextInputArea from '@/components/TextInputArea'; import TextInputArea from '@/components/TextInputArea';
import JsonResultPanel from './JsonResultPanel'; import JsonResultPanel from './JsonResultPanel';
import CollapsiblePanel from './CollapsiblePanel';
import { validateJson } from '@/utils/jsonFormatter'; import { validateJson } from '@/utils/jsonFormatter';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { buildPreview, type UseJsonToolsReturn } from '../useJsonTools';
import type { ConvertFunction, ConvertResult } from '../types'; import type { ConvertFunction, ConvertResult } from '../types';
const CONVERT_LABELS: Record< const CONVERT_LABELS: Record<string, { inputPlaceholder: string; outputLabel: string }> = {
string,
{ inputPlaceholder: string; outputLabel: string; emptyHint: string }
> = {
yaml: { yaml: {
inputPlaceholder: '输入需要转换的 JSON...', inputPlaceholder: '输入需要转换的 JSON...',
outputLabel: 'YAML 结果', outputLabel: 'YAML 结果',
emptyHint: '输入 JSON 后点击转换',
}, },
toml: { toml: {
inputPlaceholder: '输入需要转换的 JSON...', inputPlaceholder: '输入需要转换的 JSON...',
outputLabel: 'TOML 结果', outputLabel: 'TOML 结果',
emptyHint: '输入 JSON 后点击转换',
}, },
minify: { minify: {
inputPlaceholder: '输入需要压缩的 JSON...', inputPlaceholder: '输入需要压缩的 JSON...',
outputLabel: '压缩结果', outputLabel: '压缩结果',
emptyHint: '输入 JSON 后点击压缩',
}, },
}; };
interface JsonConvertSectionProps extends React.HTMLAttributes<HTMLDivElement> { interface JsonConvertSectionProps extends React.HTMLAttributes<HTMLDivElement> {
tools: UseJsonToolsReturn;
mode: string; mode: string;
convertFunction: ConvertFunction; convertFunction: ConvertFunction;
} }
export default function JsonConvertSection({ export default function JsonConvertSection({
tools,
mode, mode,
convertFunction, convertFunction,
className, className,
...props ...props
}: JsonConvertSectionProps) { }: JsonConvertSectionProps) {
const [input, setInput] = useState(''); const { input, setInput, debouncedInput } = tools;
const [debouncedInput, setDebouncedInput] = useState(''); const [inputCollapsed, setInputCollapsed] = useState(false);
const labels = CONVERT_LABELS[mode] ?? CONVERT_LABELS.yaml; const labels = CONVERT_LABELS[mode] ?? CONVERT_LABELS.yaml;
useEffect(() => {
const handle = setTimeout(() => {
setDebouncedInput(input);
}, 250);
return () => clearTimeout(handle);
}, [input]);
const error = useMemo(() => { const error = useMemo(() => {
return validateJson(debouncedInput); return validateJson(debouncedInput);
}, [debouncedInput]); }, [debouncedInput]);
@@ -71,32 +61,40 @@ export default function JsonConvertSection({
} }
}, [debouncedInput, error, convertFunction]); }, [debouncedInput, error, convertFunction]);
return ( const preview = useMemo(() => buildPreview(input), [input]);
<div className={cn('w-full flex flex-col gap-4', className)} {...props}>
<TextInputArea
placeholder={labels.inputPlaceholder}
value={input}
onChange={setInput}
externalError={error || runtimeError || undefined}
showClear={true}
allowCopy={true}
minRows={7}
maxRows={14}
onClear={() => setInput('')}
/>
{result?.output ? ( return (
<JsonResultPanel <div className={cn('w-full flex flex-1 min-h-0 flex-col gap-3', className)} {...props}>
title={labels.outputLabel} <CollapsiblePanel
content={result.output} title="JSON 输入"
originalBytes={result.originalBytes} collapsed={inputCollapsed}
outputBytes={result.outputBytes} onToggleCollapse={() => setInputCollapsed((v) => !v)}
maxHeight="380px" preview={preview}
>
<TextInputArea
fill
borderless
placeholder={labels.inputPlaceholder}
value={input}
onChange={setInput}
externalError={error || runtimeError || undefined}
showClear={true}
allowCopy={true}
className="min-h-0 flex-1"
onClear={() => setInput('')}
/> />
) : ( </CollapsiblePanel>
<EmptyPlaceholder>
{error ? '请修正上方 JSON 的语法错误以开启实时流式格式化' : labels.emptyHint} {result?.output && (
</EmptyPlaceholder> <div className="flex min-h-0 flex-1">
<JsonResultPanel
fill
title={labels.outputLabel}
content={result.output}
originalBytes={result.originalBytes}
outputBytes={result.outputBytes}
/>
</div>
)} )}
</div> </div>
); );
@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { ChevronRight } from 'lucide-react';
import TextInputArea from '@/components/TextInputArea'; import TextInputArea from '@/components/TextInputArea';
import CollapsiblePanel from './CollapsiblePanel';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
export interface JsonDiffPanelProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> { export interface JsonDiffPanelProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {
@@ -30,62 +30,30 @@ export default function JsonDiffPanel({
...props ...props
}: JsonDiffPanelProps) { }: JsonDiffPanelProps) {
return ( return (
<div <CollapsiblePanel
title={title}
collapsed={collapsed}
onToggleCollapse={onToggleCollapse}
preview={preview}
className={cn( className={cn(
'flex flex-col overflow-hidden rounded-md border bg-card transition-all duration-300 ease-in-out',
error error
? 'border-destructive focus-within:ring-1 focus-within:ring-destructive focus-within:border-destructive' ? 'border-destructive focus-within:ring-1 focus-within:ring-destructive focus-within:border-destructive'
: 'border-border focus-within:ring-1 focus-within:ring-ring focus-within:border-border/80', : 'border-border focus-within:ring-1 focus-within:ring-ring focus-within:border-border/80',
className, className,
)} )}
style={{
flex: collapsed ? '0 0 36px' : '1 1 120px',
}}
{...props} {...props}
> >
<button <TextInputArea
type="button" fill
onClick={onToggleCollapse} value={value}
className={cn( onChange={onChange}
'flex h-[36px] w-full cursor-pointer select-none items-center justify-between bg-muted/20 hover:bg-muted/30 transition-colors px-3 shrink-0 text-left focus:outline-none focus:bg-muted/40', placeholder={placeholder}
!collapsed && 'border-b border-border/60', externalError={error ?? undefined}
)} showClear={true}
> allowCopy={true}
<span className="text-[11px] font-semibold text-foreground/90 shrink-0">{title}</span> borderless
<span className="min-h-0 flex-1"
className={cn( />
'flex-1 truncate font-mono text-xs text-muted-foreground/70 ml-3 transition-all duration-300', </CollapsiblePanel>
collapsed ? 'opacity-100 max-w-full' : 'opacity-0 max-w-0 pointer-events-none',
)}
>
{preview}
</span>
<ChevronRight
className={cn(
'h-4 w-4 shrink-0 text-muted-foreground hover:text-foreground transition-transform duration-300',
!collapsed && 'rotate-90',
)}
/>
</button>
<div
className={cn(
'flex-1 min-h-0 flex flex-col transition-opacity duration-300',
collapsed ? 'opacity-0 pointer-events-none' : 'opacity-100',
)}
>
<TextInputArea
fill
value={value}
onChange={onChange}
placeholder={placeholder}
externalError={error ?? undefined}
showClear={true}
allowCopy={true}
borderless
className="min-h-0 flex-1"
/>
</div>
</div>
); );
} }
@@ -1,29 +1,27 @@
import { useEffect, useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { import {
formatJson, formatJson,
type JsonFormatOptions, type JsonFormatOptions,
type JsonFormatResult, type JsonFormatResult,
validateJson, validateJson,
} from '@/utils/jsonFormatter'; } from '@/utils/jsonFormatter';
import EmptyPlaceholder from '@/components/EmptyPlaceholder';
import SwitchButtonGroup from '@/components/SwitchButtonGroup'; import SwitchButtonGroup from '@/components/SwitchButtonGroup';
import TextInputArea from '@/components/TextInputArea'; import TextInputArea from '@/components/TextInputArea';
import CollapsiblePanel from './CollapsiblePanel';
import JsonResultPanel from './JsonResultPanel'; import JsonResultPanel from './JsonResultPanel';
import { Checkbox } from '@/components/ui/checkbox'; import { Checkbox } from '@/components/ui/checkbox';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { buildPreview, type UseJsonToolsReturn } from '../useJsonTools';
export default function JsonFormatSection() { interface JsonFormatSectionProps {
const [input, setInput] = useState(''); tools: UseJsonToolsReturn;
const [debouncedInput, setDebouncedInput] = useState(''); }
export default function JsonFormatSection({ tools }: JsonFormatSectionProps) {
const { input, setInput, debouncedInput } = tools;
const [indentSize, setIndentSize] = useState<number>(2); const [indentSize, setIndentSize] = useState<number>(2);
const [sortKeys, setSortKeys] = useState(false); const [sortKeys, setSortKeys] = useState(false);
const [inputCollapsed, setInputCollapsed] = useState(false);
useEffect(() => {
const handle = setTimeout(() => {
setDebouncedInput(input);
}, 250);
return () => clearTimeout(handle);
}, [input]);
const error = useMemo(() => { const error = useMemo(() => {
return validateJson(debouncedInput); return validateJson(debouncedInput);
@@ -47,9 +45,11 @@ export default function JsonFormatSection() {
} }
}, [debouncedInput, error, indentSize, sortKeys]); }, [debouncedInput, error, indentSize, sortKeys]);
const preview = useMemo(() => buildPreview(input), [input]);
return ( return (
<div className="w-full flex flex-col gap-4"> <div className="w-full flex flex-1 min-h-0 flex-col gap-3">
<div className="flex h-10 items-center justify-between px-1.5 bg-secondary/40 rounded-xl border border-border/60"> <div className="flex h-10 items-center justify-between px-1.5 bg-secondary/40 rounded-xl border border-border/60 shrink-0">
<div className="flex gap-4 items-center w-full"> <div className="flex gap-4 items-center w-full">
<div className="flex gap-2 items-center shrink-0 select-none"> <div className="flex gap-2 items-center shrink-0 select-none">
<span className="text-[10px] font-bold text-muted-foreground/90 uppercase tracking-wider"> <span className="text-[10px] font-bold text-muted-foreground/90 uppercase tracking-wider">
@@ -57,7 +57,7 @@ export default function JsonFormatSection() {
</span> </span>
<SwitchButtonGroup <SwitchButtonGroup
value={indentSize} value={indentSize}
onChange={(v) => setIndentSize(Number(v))} onChange={(v: number) => setIndentSize(Number(v))}
options={[2, 4, 6, 8].map((size) => ({ value: size, label: String(size) }))} options={[2, 4, 6, 8].map((size) => ({ value: size, label: String(size) }))}
size="small" size="small"
/> />
@@ -86,29 +86,36 @@ export default function JsonFormatSection() {
</div> </div>
</div> </div>
<TextInputArea <CollapsiblePanel
placeholder="输入需要格式化的 JSON..." title="JSON 输入"
value={input} collapsed={inputCollapsed}
onChange={setInput} onToggleCollapse={() => setInputCollapsed((v) => !v)}
externalError={error || runtimeError || undefined} preview={preview}
showClear={true} >
allowCopy={true} <TextInputArea
minRows={8} fill
maxRows={15} borderless
onClear={() => setInput('')} placeholder="输入需要格式化的 JSON..."
/> value={input}
onChange={setInput}
{result?.formatted ? ( externalError={error || runtimeError || undefined}
<JsonResultPanel showClear={true}
title="格式化结果" allowCopy={true}
content={result.formatted} className="min-h-0 flex-1"
originalBytes={result.originalBytes} onClear={() => setInput('')}
outputBytes={result.formattedBytes}
/> />
) : ( </CollapsiblePanel>
<EmptyPlaceholder>
{error ? '请修正上方 JSON 的语法错误以开启实时流式格式化' : '输入 JSON 后点击格式化'} {result?.formatted && (
</EmptyPlaceholder> <div className="flex min-h-0 flex-1">
<JsonResultPanel
fill
title="格式化结果"
content={result.formatted}
originalBytes={result.originalBytes}
outputBytes={result.formattedBytes}
/>
</div>
)} )}
</div> </div>
); );
@@ -1,5 +1,6 @@
import { formatBytes } from '@/utils/format'; import { formatBytes } from '@/utils/format';
import { CopyButton } from '@/components/CopyButton'; import { CopyButton } from '@/components/CopyButton';
import { cn } from '@/lib/utils';
export interface JsonResultPanelProps { export interface JsonResultPanelProps {
title: string; title: string;
@@ -8,6 +9,11 @@ export interface JsonResultPanelProps {
outputBytes: number; outputBytes: number;
outputSizeLabel?: string; outputSizeLabel?: string;
maxHeight?: string; maxHeight?: string;
/**
* 撑满父容器剩余高度并启用内部滚动(而非按 maxHeight 截断)。
* 用于 popup / sidepanel 等固定高度场景下与折叠输入面板共享垂直空间。
*/
fill?: boolean;
} }
export default function JsonResultPanel({ export default function JsonResultPanel({
@@ -17,9 +23,15 @@ export default function JsonResultPanel({
outputBytes, outputBytes,
outputSizeLabel = '格式化后大小', outputSizeLabel = '格式化后大小',
maxHeight = '420px', maxHeight = '420px',
fill = false,
}: JsonResultPanelProps) { }: JsonResultPanelProps) {
return ( return (
<div className="relative rounded-xl border border-border bg-card text-card-foreground shadow-sm overflow-hidden"> <div
className={cn(
'relative rounded-xl border border-border bg-card text-card-foreground shadow-sm overflow-hidden',
fill ? 'flex flex-col flex-1 min-h-0' : 'flex flex-col',
)}
>
<div className="flex h-9 items-center justify-between px-4 border-b border-border bg-muted/50 select-none"> <div className="flex h-9 items-center justify-between px-4 border-b border-border bg-muted/50 select-none">
<div className="flex gap-4 items-center"> <div className="flex gap-4 items-center">
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground/90"> <span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground/90">
@@ -43,8 +55,11 @@ export default function JsonResultPanel({
</div> </div>
<div <div
className="p-4 font-mono text-xs text-foreground/90 whitespace-pre-wrap break-all overflow-y-auto leading-relaxed select-text" className={cn(
style={{ maxHeight }} 'p-4 font-mono text-xs text-foreground/90 whitespace-pre-wrap break-all overflow-y-auto leading-relaxed select-text',
fill && 'flex-1 min-h-0',
)}
style={fill ? undefined : { maxHeight }}
> >
{content} {content}
</div> </div>
@@ -0,0 +1,62 @@
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { useEffect, useState } from 'react';
import { describe, it, expect } from 'vitest';
import JsonFormatSection from '../JsonFormatSection';
import type { UseJsonToolsReturn } from '../../useJsonTools';
function Harness({ initial = '' }: { initial?: string }) {
const [input, setInput] = useState(initial);
const [debouncedInput, setDebouncedInput] = useState(initial);
useEffect(() => {
const t = setTimeout(() => setDebouncedInput(input), 250);
return () => clearTimeout(t);
}, [input]);
const tools = { input, setInput, debouncedInput } as unknown as UseJsonToolsReturn;
return <JsonFormatSection tools={tools} />;
}
describe('JsonFormatSection', () => {
it('渲染输入面板标题与缩进/键名排序工具栏', () => {
render(<Harness />);
expect(screen.getByText('JSON 输入')).toBeInTheDocument();
expect(screen.getByText('缩进')).toBeInTheDocument();
expect(screen.getByText('键名排序')).toBeInTheDocument();
});
it('输入无效 JSON 时不渲染结果面板与占位区', async () => {
render(<Harness initial="{ invalid json " />);
await waitFor(() => {
expect(screen.queryByText('格式化结果')).not.toBeInTheDocument();
});
expect(
screen.queryByText('请修正上方 JSON 的语法错误以开启实时流式格式化'),
).not.toBeInTheDocument();
expect(screen.queryByText('输入 JSON 后点击格式化')).not.toBeInTheDocument();
});
it('输入有效 JSON 后渲染格式化结果面板', async () => {
render(<Harness initial='{"a":1,"b":2}' />);
await waitFor(() => {
expect(screen.getByText('格式化结果')).toBeInTheDocument();
});
expect(screen.getByText(/"a": 1/)).toBeInTheDocument();
expect(screen.getByText(/"b": 2/)).toBeInTheDocument();
});
it('点击标题可折叠输入面板', async () => {
const user = userEvent.setup();
render(<Harness initial='{"a":1}' />);
await waitFor(() => {
expect(screen.getByText('格式化结果')).toBeInTheDocument();
});
await user.click(screen.getByText('JSON 输入'));
const input = screen.getByPlaceholderText('输入需要格式化的 JSON...');
await waitFor(() => {
expect(input.closest('.opacity-0')).toBeInTheDocument();
});
expect(screen.getByText('{"a":1}(点击展开)')).toBeInTheDocument();
});
});
+4 -4
View File
@@ -30,13 +30,13 @@ export default function Index() {
{pageMode === 'diff' ? ( {pageMode === 'diff' ? (
<DiffWorkspace tools={tools} /> <DiffWorkspace tools={tools} />
) : pageMode === 'format' ? ( ) : pageMode === 'format' ? (
<JsonFormatSection /> <JsonFormatSection tools={tools} />
) : pageMode === 'yaml' ? ( ) : pageMode === 'yaml' ? (
<JsonConvertSection mode="yaml" convertFunction={yamlConvert} /> <JsonConvertSection tools={tools} mode="yaml" convertFunction={yamlConvert} />
) : pageMode === 'toml' ? ( ) : pageMode === 'toml' ? (
<JsonConvertSection mode="toml" convertFunction={tomlConvert} /> <JsonConvertSection tools={tools} mode="toml" convertFunction={tomlConvert} />
) : ( ) : (
<JsonConvertSection mode="minify" convertFunction={minifyConvert} /> <JsonConvertSection tools={tools} mode="minify" convertFunction={minifyConvert} />
)} )}
</div> </div>
); );
+30 -6
View File
@@ -21,6 +21,11 @@ export interface UseJsonToolsReturn {
setRightInput: (val: string) => void; setRightInput: (val: string) => void;
leftError: string | null; leftError: string | null;
rightError: string | null; rightError: string | null;
/** format/yaml/toml/minify 共享的输入内容(已持久化) */
input: string;
setInput: (val: string) => void;
/** input 防抖后的快照,用于格式化/转换计算与持久化 */
debouncedInput: string;
viewMode: ViewMode; viewMode: ViewMode;
setViewMode: (mode: ViewMode) => void; setViewMode: (mode: ViewMode) => void;
/** 当前生效的差异结果:宽屏为实时计算,窄屏为手动 Compare 后的结果 */ /** 当前生效的差异结果:宽屏为实时计算,窄屏为手动 Compare 后的结果 */
@@ -48,7 +53,7 @@ export interface UseJsonToolsReturn {
minifyConvert: ConvertFunction; minifyConvert: ConvertFunction;
} }
const buildPreview = (raw: string): string => { export const buildPreview = (raw: string): string => {
const single = raw.replace(/\s+/g, ' ').trim(); const single = raw.replace(/\s+/g, ' ').trim();
const truncated = single.length > 80 ? `${single.slice(0, 80)}` : single; const truncated = single.length > 80 ? `${single.slice(0, 80)}` : single;
return truncated ? `${truncated}(点击展开)` : '(点击展开)'; return truncated ? `${truncated}(点击展开)` : '(点击展开)';
@@ -59,18 +64,34 @@ export function useJsonTools(): UseJsonToolsReturn {
const [pageMode, setPageMode] = useStorageState('jsonTools/pageMode', 'diff', isValidPageMode); const [pageMode, setPageMode] = useStorageState('jsonTools/pageMode', 'diff', isValidPageMode);
const [leftInput, setLeftInputState] = useState(''); const [persistedLeft, setPersistedLeft] = useStorageState('jsonTools/diffLeft', '');
const [rightInput, setRightInputState] = useState(''); const [persistedRight, setPersistedRight] = useStorageState('jsonTools/diffRight', '');
const [debouncedLeft, setDebouncedLeft] = useState(''); const [persistedInput, setPersistedInput] = useStorageState('jsonTools/input', '');
const [debouncedRight, setDebouncedRight] = useState('');
const [leftInput, setLeftInputState] = useState<string>(persistedLeft);
const [rightInput, setRightInputState] = useState<string>(persistedRight);
const [input, setInput] = useState<string>(persistedInput);
const [debouncedLeft, setDebouncedLeft] = useState<string>(persistedLeft);
const [debouncedRight, setDebouncedRight] = useState<string>(persistedRight);
const [debouncedInput, setDebouncedInput] = useState<string>(persistedInput);
useEffect(() => { useEffect(() => {
const handle = setTimeout(() => { const handle = setTimeout(() => {
setDebouncedLeft(leftInput); setDebouncedLeft(leftInput);
setDebouncedRight(rightInput); setDebouncedRight(rightInput);
setPersistedLeft(leftInput);
setPersistedRight(rightInput);
}, 250); }, 250);
return () => clearTimeout(handle); return () => clearTimeout(handle);
}, [leftInput, rightInput]); }, [leftInput, rightInput, setPersistedLeft, setPersistedRight]);
useEffect(() => {
const handle = setTimeout(() => {
setDebouncedInput(input);
setPersistedInput(input);
}, 250);
return () => clearTimeout(handle);
}, [input, setPersistedInput]);
const parseState = useMemo(() => { const parseState = useMemo(() => {
const invalidMsg = '无效的 JSON 格式'; const invalidMsg = '无效的 JSON 格式';
@@ -207,6 +228,9 @@ export function useJsonTools(): UseJsonToolsReturn {
setRightInput, setRightInput,
leftError, leftError,
rightError, rightError,
input,
setInput,
debouncedInput,
viewMode: activeViewMode, viewMode: activeViewMode,
setViewMode, setViewMode,
activeResult, activeResult,
+6
View File
@@ -89,6 +89,12 @@ export interface StorageSchema {
'qrCode/urlExpanded': boolean; 'qrCode/urlExpanded': boolean;
/** JSON 工具页面当前子模式 */ /** JSON 工具页面当前子模式 */
'jsonTools/pageMode': JsonToolsPageMode; 'jsonTools/pageMode': JsonToolsPageMode;
/** JSON 工具:format/yaml/toml/minify 共享的输入内容 */
'jsonTools/input': string;
/** JSON 工具:diff 模式左侧输入内容 */
'jsonTools/diffLeft': string;
/** JSON 工具:diff 模式右侧输入内容 */
'jsonTools/diffRight': string;
/** Base64 转换器页面当前子模式 */ /** Base64 转换器页面当前子模式 */
'base64Converter/pageMode': Base64ConverterPageMode; 'base64Converter/pageMode': Base64ConverterPageMode;
/** Base64 转换器「文件」子模式当前方向 */ /** Base64 转换器「文件」子模式当前方向 */