refactor(json-tools): 重构状态管理逻辑并实现输入内容持久化
This commit is contained in:
@@ -64,4 +64,15 @@ describe('useJsonTools 窄屏手动比对', () => {
|
||||
act(() => result.current.toggleCollapseA());
|
||||
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}');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import TextInputArea from '@/components/TextInputArea';
|
||||
import JsonResultPanel from './JsonResultPanel';
|
||||
import CollapsiblePanel from './CollapsiblePanel';
|
||||
import { validateJson } from '@/utils/jsonFormatter';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { buildPreview } from '../useJsonTools';
|
||||
import { buildPreview, type UseJsonToolsReturn } from '../useJsonTools';
|
||||
import type { ConvertFunction, ConvertResult } from '../types';
|
||||
|
||||
const CONVERT_LABELS: Record<string, { inputPlaceholder: string; outputLabel: string }> = {
|
||||
@@ -23,29 +23,23 @@ const CONVERT_LABELS: Record<string, { inputPlaceholder: string; outputLabel: st
|
||||
};
|
||||
|
||||
interface JsonConvertSectionProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
tools: UseJsonToolsReturn;
|
||||
mode: string;
|
||||
convertFunction: ConvertFunction;
|
||||
}
|
||||
|
||||
export default function JsonConvertSection({
|
||||
tools,
|
||||
mode,
|
||||
convertFunction,
|
||||
className,
|
||||
...props
|
||||
}: JsonConvertSectionProps) {
|
||||
const [input, setInput] = useState('');
|
||||
const [debouncedInput, setDebouncedInput] = useState('');
|
||||
const { input, setInput, debouncedInput } = tools;
|
||||
const [inputCollapsed, setInputCollapsed] = useState(false);
|
||||
|
||||
const labels = CONVERT_LABELS[mode] ?? CONVERT_LABELS.yaml;
|
||||
|
||||
useEffect(() => {
|
||||
const handle = setTimeout(() => {
|
||||
setDebouncedInput(input);
|
||||
}, 250);
|
||||
return () => clearTimeout(handle);
|
||||
}, [input]);
|
||||
|
||||
const error = useMemo(() => {
|
||||
return validateJson(debouncedInput);
|
||||
}, [debouncedInput]);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import {
|
||||
formatJson,
|
||||
type JsonFormatOptions,
|
||||
@@ -11,22 +11,18 @@ import CollapsiblePanel from './CollapsiblePanel';
|
||||
import JsonResultPanel from './JsonResultPanel';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { buildPreview } from '../useJsonTools';
|
||||
import { buildPreview, type UseJsonToolsReturn } from '../useJsonTools';
|
||||
|
||||
export default function JsonFormatSection() {
|
||||
const [input, setInput] = useState('');
|
||||
const [debouncedInput, setDebouncedInput] = useState('');
|
||||
interface JsonFormatSectionProps {
|
||||
tools: UseJsonToolsReturn;
|
||||
}
|
||||
|
||||
export default function JsonFormatSection({ tools }: JsonFormatSectionProps) {
|
||||
const { input, setInput, debouncedInput } = tools;
|
||||
const [indentSize, setIndentSize] = useState<number>(2);
|
||||
const [sortKeys, setSortKeys] = useState(false);
|
||||
const [inputCollapsed, setInputCollapsed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handle = setTimeout(() => {
|
||||
setDebouncedInput(input);
|
||||
}, 250);
|
||||
return () => clearTimeout(handle);
|
||||
}, [input]);
|
||||
|
||||
const error = useMemo(() => {
|
||||
return validateJson(debouncedInput);
|
||||
}, [debouncedInput]);
|
||||
|
||||
@@ -1,20 +1,31 @@
|
||||
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
|
||||
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(<JsonFormatSection />);
|
||||
render(<Harness />);
|
||||
expect(screen.getByText('JSON 输入')).toBeInTheDocument();
|
||||
expect(screen.getByText('缩进')).toBeInTheDocument();
|
||||
expect(screen.getByText('键名排序')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('输入无效 JSON 时不渲染结果面板与占位区', async () => {
|
||||
render(<JsonFormatSection />);
|
||||
const input = screen.getByPlaceholderText('输入需要格式化的 JSON...');
|
||||
fireEvent.change(input, { target: { value: '{ invalid json ' } });
|
||||
render(<Harness initial="{ invalid json " />);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('格式化结果')).not.toBeInTheDocument();
|
||||
});
|
||||
@@ -25,9 +36,7 @@ describe('JsonFormatSection', () => {
|
||||
});
|
||||
|
||||
it('输入有效 JSON 后渲染格式化结果面板', async () => {
|
||||
render(<JsonFormatSection />);
|
||||
const input = screen.getByPlaceholderText('输入需要格式化的 JSON...');
|
||||
fireEvent.change(input, { target: { value: '{"a":1,"b":2}' } });
|
||||
render(<Harness initial='{"a":1,"b":2}' />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('格式化结果')).toBeInTheDocument();
|
||||
});
|
||||
@@ -37,20 +46,17 @@ describe('JsonFormatSection', () => {
|
||||
|
||||
it('点击标题可折叠输入面板', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<JsonFormatSection />);
|
||||
const input = screen.getByPlaceholderText('输入需要格式化的 JSON...');
|
||||
fireEvent.change(input, { target: { value: '{"a":1}' } });
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,13 +30,13 @@ export default function Index() {
|
||||
{pageMode === 'diff' ? (
|
||||
<DiffWorkspace tools={tools} />
|
||||
) : pageMode === 'format' ? (
|
||||
<JsonFormatSection />
|
||||
<JsonFormatSection tools={tools} />
|
||||
) : pageMode === 'yaml' ? (
|
||||
<JsonConvertSection mode="yaml" convertFunction={yamlConvert} />
|
||||
<JsonConvertSection tools={tools} mode="yaml" convertFunction={yamlConvert} />
|
||||
) : 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>
|
||||
);
|
||||
|
||||
@@ -21,6 +21,11 @@ export interface UseJsonToolsReturn {
|
||||
setRightInput: (val: string) => void;
|
||||
leftError: string | null;
|
||||
rightError: string | null;
|
||||
/** format/yaml/toml/minify 共享的输入内容(已持久化) */
|
||||
input: string;
|
||||
setInput: (val: string) => void;
|
||||
/** input 防抖后的快照,用于格式化/转换计算与持久化 */
|
||||
debouncedInput: string;
|
||||
viewMode: ViewMode;
|
||||
setViewMode: (mode: ViewMode) => void;
|
||||
/** 当前生效的差异结果:宽屏为实时计算,窄屏为手动 Compare 后的结果 */
|
||||
@@ -59,18 +64,34 @@ export function useJsonTools(): UseJsonToolsReturn {
|
||||
|
||||
const [pageMode, setPageMode] = useStorageState('jsonTools/pageMode', 'diff', isValidPageMode);
|
||||
|
||||
const [leftInput, setLeftInputState] = useState('');
|
||||
const [rightInput, setRightInputState] = useState('');
|
||||
const [debouncedLeft, setDebouncedLeft] = useState('');
|
||||
const [debouncedRight, setDebouncedRight] = useState('');
|
||||
const [persistedLeft, setPersistedLeft] = useStorageState('jsonTools/diffLeft', '');
|
||||
const [persistedRight, setPersistedRight] = useStorageState('jsonTools/diffRight', '');
|
||||
const [persistedInput, setPersistedInput] = useStorageState('jsonTools/input', '');
|
||||
|
||||
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(() => {
|
||||
const handle = setTimeout(() => {
|
||||
setDebouncedLeft(leftInput);
|
||||
setDebouncedRight(rightInput);
|
||||
setPersistedLeft(leftInput);
|
||||
setPersistedRight(rightInput);
|
||||
}, 250);
|
||||
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 invalidMsg = '无效的 JSON 格式';
|
||||
@@ -207,6 +228,9 @@ export function useJsonTools(): UseJsonToolsReturn {
|
||||
setRightInput,
|
||||
leftError,
|
||||
rightError,
|
||||
input,
|
||||
setInput,
|
||||
debouncedInput,
|
||||
viewMode: activeViewMode,
|
||||
setViewMode,
|
||||
activeResult,
|
||||
|
||||
Vendored
+6
@@ -89,6 +89,12 @@ export interface StorageSchema {
|
||||
'qrCode/urlExpanded': boolean;
|
||||
/** JSON 工具页面当前子模式 */
|
||||
'jsonTools/pageMode': JsonToolsPageMode;
|
||||
/** JSON 工具:format/yaml/toml/minify 共享的输入内容 */
|
||||
'jsonTools/input': string;
|
||||
/** JSON 工具:diff 模式左侧输入内容 */
|
||||
'jsonTools/diffLeft': string;
|
||||
/** JSON 工具:diff 模式右侧输入内容 */
|
||||
'jsonTools/diffRight': string;
|
||||
/** Base64 转换器页面当前子模式 */
|
||||
'base64Converter/pageMode': Base64ConverterPageMode;
|
||||
/** Base64 转换器「文件」子模式当前方向 */
|
||||
|
||||
Reference in New Issue
Block a user