diff --git a/src/pages/JsonTools/__tests__/useJsonTools.test.ts b/src/pages/JsonTools/__tests__/useJsonTools.test.ts index 7a96ebc..d84563e 100644 --- a/src/pages/JsonTools/__tests__/useJsonTools.test.ts +++ b/src/pages/JsonTools/__tests__/useJsonTools.test.ts @@ -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}'); + }); }); diff --git a/src/pages/JsonTools/components/JsonConvertSection.tsx b/src/pages/JsonTools/components/JsonConvertSection.tsx index 86298b1..0d03eeb 100644 --- a/src/pages/JsonTools/components/JsonConvertSection.tsx +++ b/src/pages/JsonTools/components/JsonConvertSection.tsx @@ -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 = { @@ -23,29 +23,23 @@ const CONVERT_LABELS: Record { + 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]); diff --git a/src/pages/JsonTools/components/JsonFormatSection.tsx b/src/pages/JsonTools/components/JsonFormatSection.tsx index cac1ec0..094d8d3 100644 --- a/src/pages/JsonTools/components/JsonFormatSection.tsx +++ b/src/pages/JsonTools/components/JsonFormatSection.tsx @@ -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(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]); diff --git a/src/pages/JsonTools/components/__tests__/JsonFormatSection.test.tsx b/src/pages/JsonTools/components/__tests__/JsonFormatSection.test.tsx index 75637eb..cc07842 100644 --- a/src/pages/JsonTools/components/__tests__/JsonFormatSection.test.tsx +++ b/src/pages/JsonTools/components/__tests__/JsonFormatSection.test.tsx @@ -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 ; +} describe('JsonFormatSection', () => { it('渲染输入面板标题与缩进/键名排序工具栏', () => { - render(); + render(); expect(screen.getByText('JSON 输入')).toBeInTheDocument(); expect(screen.getByText('缩进')).toBeInTheDocument(); expect(screen.getByText('键名排序')).toBeInTheDocument(); }); it('输入无效 JSON 时不渲染结果面板与占位区', async () => { - render(); - const input = screen.getByPlaceholderText('输入需要格式化的 JSON...'); - fireEvent.change(input, { target: { value: '{ invalid json ' } }); + render(); await waitFor(() => { expect(screen.queryByText('格式化结果')).not.toBeInTheDocument(); }); @@ -25,9 +36,7 @@ describe('JsonFormatSection', () => { }); it('输入有效 JSON 后渲染格式化结果面板', async () => { - render(); - const input = screen.getByPlaceholderText('输入需要格式化的 JSON...'); - fireEvent.change(input, { target: { value: '{"a":1,"b":2}' } }); + render(); await waitFor(() => { expect(screen.getByText('格式化结果')).toBeInTheDocument(); }); @@ -37,20 +46,17 @@ describe('JsonFormatSection', () => { it('点击标题可折叠输入面板', async () => { const user = userEvent.setup(); - render(); - const input = screen.getByPlaceholderText('输入需要格式化的 JSON...'); - fireEvent.change(input, { target: { value: '{"a":1}' } }); + render(); 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(); }); }); diff --git a/src/pages/JsonTools/index.tsx b/src/pages/JsonTools/index.tsx index f019610..9fb13a8 100644 --- a/src/pages/JsonTools/index.tsx +++ b/src/pages/JsonTools/index.tsx @@ -30,13 +30,13 @@ export default function Index() { {pageMode === 'diff' ? ( ) : pageMode === 'format' ? ( - + ) : pageMode === 'yaml' ? ( - + ) : pageMode === 'toml' ? ( - + ) : ( - + )} ); diff --git a/src/pages/JsonTools/useJsonTools.ts b/src/pages/JsonTools/useJsonTools.ts index c3e7af5..604601b 100644 --- a/src/pages/JsonTools/useJsonTools.ts +++ b/src/pages/JsonTools/useJsonTools.ts @@ -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(persistedLeft); + const [rightInput, setRightInputState] = useState(persistedRight); + const [input, setInput] = useState(persistedInput); + const [debouncedLeft, setDebouncedLeft] = useState(persistedLeft); + const [debouncedRight, setDebouncedRight] = useState(persistedRight); + const [debouncedInput, setDebouncedInput] = useState(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, diff --git a/src/types/storage.d.ts b/src/types/storage.d.ts index eb906de..7fb43e1 100644 --- a/src/types/storage.d.ts +++ b/src/types/storage.d.ts @@ -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 转换器「文件」子模式当前方向 */