From 4f7e402dba18b4c750dd2ce3710c6996d6ffb279 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=A8=E9=9C=96=E9=93=83?= Date: Thu, 28 May 2026 23:18:58 +0800 Subject: [PATCH] refactor(pages): apply UI+Hook separation and unify component naming MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Dashboard, RightClickRestorer: rename page component to Index - Jwt: extract useJwt.ts hook + JwtSection.tsx sub-component (127→76 lines) - TextStatistics: extract useTextStatistics.ts hook (63→48 lines) - JsonTools: extract useJsonTools.ts hook + constants.ts (199→109 lines) - All pages now follow CODING_STANDARDS.md §11 pattern --- src/pages/Dashboard/index.tsx | 2 +- src/pages/JsonTools/constants.ts | 27 ++++ src/pages/JsonTools/index.tsx | 130 ++++------------- src/pages/JsonTools/useJsonTools.ts | 137 ++++++++++++++++++ src/pages/Jwt/JwtSection.tsx | 38 +++++ src/pages/Jwt/index.tsx | 81 ++--------- src/pages/Jwt/useJwt.ts | 43 ++++++ src/pages/RightClickRestorer/index.tsx | 2 +- src/pages/TextStatistics/index.tsx | 17 +-- src/pages/TextStatistics/useTextStatistics.ts | 23 +++ 10 files changed, 307 insertions(+), 193 deletions(-) create mode 100644 src/pages/JsonTools/constants.ts create mode 100644 src/pages/JsonTools/useJsonTools.ts create mode 100644 src/pages/Jwt/JwtSection.tsx create mode 100644 src/pages/Jwt/useJwt.ts create mode 100644 src/pages/TextStatistics/useTextStatistics.ts diff --git a/src/pages/Dashboard/index.tsx b/src/pages/Dashboard/index.tsx index 1d28102..7d51ebd 100644 --- a/src/pages/Dashboard/index.tsx +++ b/src/pages/Dashboard/index.tsx @@ -5,7 +5,7 @@ import { useMemo } from 'react'; import { useI18n } from '@/utils/chromeI18n'; import { cn } from '@/lib/utils'; -export default function DashboardPage() { +export default function Index() { const { navigateTo, visiblePages, pageOrder, recentlyUsedTools } = useRouter(); const { t } = useI18n(['features']); diff --git a/src/pages/JsonTools/constants.ts b/src/pages/JsonTools/constants.ts new file mode 100644 index 0000000..2fb1915 --- /dev/null +++ b/src/pages/JsonTools/constants.ts @@ -0,0 +1,27 @@ +import type { JsonToolsPageMode } from '@/types/storage'; + +export const VALID_PAGE_MODES: readonly JsonToolsPageMode[] = [ + 'diff', + 'format', + 'yaml', + 'toml', + 'minify', +]; + +export const isValidPageMode = (val: unknown): val is JsonToolsPageMode => + typeof val === 'string' && (VALID_PAGE_MODES as readonly string[]).includes(val); + +export interface ParseState { + value: unknown; + error: string | null; +} + +export 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 }; + } +}; diff --git a/src/pages/JsonTools/index.tsx b/src/pages/JsonTools/index.tsx index 3b7fb2b..359b851 100644 --- a/src/pages/JsonTools/index.tsx +++ b/src/pages/JsonTools/index.tsx @@ -1,113 +1,39 @@ -import { useCallback, useEffect, useMemo, useState } from 'react'; import { useI18n } from '@/utils/chromeI18n'; import JsonDiffInput from './JsonDiffInput'; import DiffResult from './DiffResult'; import DiffNavigator from './DiffNavigator'; import JsonFormatSection from './JsonFormatSection'; -import type { ConvertFunction } from './JsonConvertSection'; import JsonConvertSection from './JsonConvertSection'; -import { diffJson } from './diffEngine'; -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'; import SwitchButtonGroup from '@/components/SwitchButtonGroup'; +import { useJsonTools } from './useJsonTools'; +import type { JsonToolsPageMode } from '@/types/storage'; import type { ViewMode } from './types'; -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 } = useI18n(['jsonDiff', 'jsonFormat']); - const [pageMode, setPageMode] = useStorageState('jsonTools/pageMode', 'diff', isValidPageMode); - - // Debounce input - const [leftInput, setLeftInput] = useState(''); - const [rightInput, setRightInput] = useState(''); - - // Debounced values - const [debouncedLeft, setDebouncedLeft] = useState(''); - const [debouncedRight, setDebouncedRight] = useState(''); - - useEffect(() => { - const handle = setTimeout(() => { - setDebouncedLeft(leftInput); - setDebouncedRight(rightInput); - }, 250); - return () => clearTimeout(handle); - }, [leftInput, rightInput]); - - // Parse debounced inputs - const parseState = useMemo(() => { - const invalidMsg = t('jsonDiff:invalidJson'); - return { - left: tryParse(debouncedLeft, invalidMsg), - right: tryParse(debouncedRight, invalidMsg), - }; - }, [debouncedLeft, debouncedRight, t]); - - const leftError = parseState.left.error; - const rightError = parseState.right.error; - - const [viewMode, setViewMode] = useState('sideBySide'); - const [currentDiffIndex, setCurrentDiffIndex] = useState(0); - - // Real-time diff computation - const diffResult = useMemo(() => { - const { left, right } = parseState; - if (left.error || right.error || debouncedLeft.trim() === '' || debouncedRight.trim() === '') { - return null; - } - return diffJson(left.value, right.value); - }, [parseState, debouncedLeft, debouncedRight]); - - const total = diffResult?.diffPaths.length ?? 0; - - const handlePrev = useCallback(() => { - if (total === 0) return; - setCurrentDiffIndex((idx) => (idx - 1 + total) % total); - }, [total]); - - const handleNext = useCallback(() => { - if (total === 0) return; - setCurrentDiffIndex((idx) => (idx + 1) % total); - }, [total]); - - const activePath = diffResult && total > 0 ? diffResult.diffPaths[currentDiffIndex] : undefined; - - 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 }; - }, []); + const { + pageMode, + setPageMode, + leftInput, + rightInput, + setLeftInput, + setRightInput, + leftError, + rightError, + viewMode, + setViewMode, + diffResult, + total, + currentDiffIndex, + handlePrev, + handleNext, + activePath, + yamlConvert, + tomlConvert, + minifyConvert, + } = useJsonTools(); return (
@@ -144,10 +70,7 @@ export default function Index() { label={t('jsonDiff:leftLabel')} placeholder={t('jsonDiff:leftPlaceholder')} value={leftInput} - onChange={(val) => { - setLeftInput(val); - setCurrentDiffIndex(0); - }} + onChange={setLeftInput} error={leftError} minRows={9} /> @@ -155,10 +78,7 @@ export default function Index() { label={t('jsonDiff:rightLabel')} placeholder={t('jsonDiff:rightPlaceholder')} value={rightInput} - onChange={(val) => { - setRightInput(val); - setCurrentDiffIndex(0); - }} + onChange={setRightInput} error={rightError} minRows={9} /> diff --git a/src/pages/JsonTools/useJsonTools.ts b/src/pages/JsonTools/useJsonTools.ts new file mode 100644 index 0000000..57d91e2 --- /dev/null +++ b/src/pages/JsonTools/useJsonTools.ts @@ -0,0 +1,137 @@ +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useI18n } from '@/utils/chromeI18n'; +import { useStorageState } from '@/utils/useStorageState'; +import { diffJson } from './diffEngine'; +import { jsonToYaml } from '@/utils/jsonToYaml'; +import { jsonToToml } from '@/utils/jsonToToml'; +import { minifyJson } from '@/utils/jsonFormatter'; +import { isValidPageMode, tryParse } from './constants'; +import type { JsonToolsPageMode } from '@/types/storage'; +import type { ConvertFunction } from './JsonConvertSection'; +import type { ViewMode } from './types'; + +export interface UseJsonToolsReturn { + pageMode: JsonToolsPageMode; + setPageMode: (mode: JsonToolsPageMode) => void; + // Diff mode state + leftInput: string; + rightInput: string; + setLeftInput: (val: string) => void; + setRightInput: (val: string) => void; + leftError: string | null; + rightError: string | null; + viewMode: ViewMode; + setViewMode: (mode: ViewMode) => void; + diffResult: ReturnType | null; + total: number; + currentDiffIndex: number; + handlePrev: () => void; + handleNext: () => void; + activePath: string | undefined; + // Convert functions + yamlConvert: ConvertFunction; + tomlConvert: ConvertFunction; + minifyConvert: ConvertFunction; +} + +export function useJsonTools(): UseJsonToolsReturn { + const { t } = useI18n(['jsonDiff', 'jsonFormat']); + const [pageMode, setPageMode] = useStorageState('jsonTools/pageMode', 'diff', isValidPageMode); + + // Diff inputs + const [leftInput, setLeftInput] = useState(''); + const [rightInput, setRightInput] = useState(''); + const [debouncedLeft, setDebouncedLeft] = useState(''); + const [debouncedRight, setDebouncedRight] = useState(''); + + // Debounce + useEffect(() => { + const handle = setTimeout(() => { + setDebouncedLeft(leftInput); + setDebouncedRight(rightInput); + }, 250); + return () => clearTimeout(handle); + }, [leftInput, rightInput]); + + // Parse debounced inputs + const parseState = useMemo(() => { + const invalidMsg = t('jsonDiff:invalidJson'); + return { + left: tryParse(debouncedLeft, invalidMsg), + right: tryParse(debouncedRight, invalidMsg), + }; + }, [debouncedLeft, debouncedRight, t]); + + const leftError = parseState.left.error; + const rightError = parseState.right.error; + + const [viewMode, setViewMode] = useState('sideBySide'); + const [currentDiffIndex, setCurrentDiffIndex] = useState(0); + + // Real-time diff computation + const diffResult = useMemo(() => { + const { left, right } = parseState; + if (left.error || right.error || debouncedLeft.trim() === '' || debouncedRight.trim() === '') { + return null; + } + return diffJson(left.value, right.value); + }, [parseState, debouncedLeft, debouncedRight]); + + const total = diffResult?.diffPaths.length ?? 0; + + const handlePrev = useCallback(() => { + if (total === 0) return; + setCurrentDiffIndex((idx) => (idx - 1 + total) % total); + }, [total]); + + const handleNext = useCallback(() => { + if (total === 0) return; + setCurrentDiffIndex((idx) => (idx + 1) % total); + }, [total]); + + const activePath = diffResult && total > 0 ? diffResult.diffPaths[currentDiffIndex] : undefined; + + // Convert functions + 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 { + pageMode, + setPageMode, + leftInput, + rightInput, + setLeftInput: (val: string) => { + setLeftInput(val); + setCurrentDiffIndex(0); + }, + setRightInput: (val: string) => { + setRightInput(val); + setCurrentDiffIndex(0); + }, + leftError, + rightError, + viewMode, + setViewMode, + diffResult, + total, + currentDiffIndex, + handlePrev, + handleNext, + activePath, + yamlConvert, + tomlConvert, + minifyConvert, + }; +} diff --git a/src/pages/Jwt/JwtSection.tsx b/src/pages/Jwt/JwtSection.tsx new file mode 100644 index 0000000..4a11e0f --- /dev/null +++ b/src/pages/Jwt/JwtSection.tsx @@ -0,0 +1,38 @@ +import CopyButton from '@/components/CopyButton'; +import { useI18n } from '@/utils/chromeI18n'; +import { stringifyJson } from '@/utils/jwt'; +import { cn } from '@/lib/utils'; + +interface JwtSectionProps { + title: string; + content: unknown; + colorClass: string; + bgClass: string; + borderClass: string; +} + +export default function JwtSection({ + title, + content, + colorClass, + bgClass, + borderClass, +}: JwtSectionProps) { + const { t } = useI18n('jwt'); + return ( +
+
+ + {title} + + +
+
+        {content ? stringifyJson(content) : t('jwt:invalidFormat')}
+      
+
+ ); +} diff --git a/src/pages/Jwt/index.tsx b/src/pages/Jwt/index.tsx index 6e37771..07aa1cb 100644 --- a/src/pages/Jwt/index.tsx +++ b/src/pages/Jwt/index.tsx @@ -1,91 +1,31 @@ -import { useCallback, useEffect, useMemo, useState } from 'react'; -import { parseJwt, stringifyJson } from '@/utils/jwt'; -import CopyButton from '@/components/CopyButton'; import TextInputArea from '@/components/TextInputArea'; +import CopyButton from '@/components/CopyButton'; +import JwtSection from './JwtSection'; +import { useJwt } from './useJwt'; import { useI18n } from '@/utils/chromeI18n'; -import { useContextMenuData } from '@/utils/useContextMenuData'; -import { cn } from '@/lib/utils'; - -interface SectionProps { - title: string; - content: unknown; - colorClass: string; - bgClass: string; - borderClass: string; -} - -const Section = ({ title, content, colorClass, bgClass, borderClass }: SectionProps) => { - const { t } = useI18n('jwt'); - return ( -
-
- - {title} - - -
-
-        {content ? stringifyJson(content) : t('jwt:invalidFormat')}
-      
-
- ); -}; export default function Index() { const { t } = useI18n(['jwt', 'jsonFormat']); - const [jwtInput, setJwtInput] = useState(''); - - // 2. 防抖中转管道:切断高频键盘敲击时的红色语法闪烁 - const [debouncedInput, setDebouncedInput] = useState(''); - - useEffect(() => { - const handle = setTimeout(() => { - setDebouncedInput(jwtInput); - }, 200); - return () => clearTimeout(handle); - }, [jwtInput]); - - const handleContextMenuData = useCallback((payload: string) => { - const cleaned = payload.replace(/^Bearer\s*/i, '').trim(); - setJwtInput(cleaned); - }, []); - - useContextMenuData({ featureKey: 'jwt', onData: handleContextMenuData }); - - const result = useMemo(() => { - if (!debouncedInput.trim()) { - return null; - } - return parseJwt(debouncedInput); - }, [debouncedInput]); + const { jwtInput, result, handleChange, handleClear } = useJwt(); return (
- {/* 输入终端 */} { - const cleaned = val.replace(/^Bearer\s*/i, '').trim(); - setJwtInput(cleaned); - }} + onChange={handleChange} allowCopy={true} showClear={true} externalError={result?.error || undefined} - onClear={() => setJwtInput('')} + onClear={handleClear} /> - {/* 解码看板结果展现 */} {result && !result.error && (
- {/* Header 分区:完美致敬 JWT.io 的鲜艳色彩,同时实现黑夜暗化自适应 */} -
- {/* Payload 分区 */} -
- {/* Signature 签名区:完全对齐标准的 shadcn 骨架阶度 */}
@@ -120,7 +58,6 @@ export default function Index() {
)} - {/* 当解析错误时的干净中性引导拦截 */} {result?.error && (

diff --git a/src/pages/Jwt/useJwt.ts b/src/pages/Jwt/useJwt.ts new file mode 100644 index 0000000..5f65e3f --- /dev/null +++ b/src/pages/Jwt/useJwt.ts @@ -0,0 +1,43 @@ +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useContextMenuData } from '@/utils/useContextMenuData'; +import { parseJwt } from '@/utils/jwt'; +import type { JwtResult } from '@/utils/jwt'; + +export interface UseJwtReturn { + jwtInput: string; + result: JwtResult | null; + handleChange: (val: string) => void; + handleClear: () => void; +} + +export function useJwt(): UseJwtReturn { + const [jwtInput, setJwtInput] = useState(''); + const [debouncedInput, setDebouncedInput] = useState(''); + + // 防抖管道 + useEffect(() => { + const handle = setTimeout(() => setDebouncedInput(jwtInput), 200); + return () => clearTimeout(handle); + }, [jwtInput]); + + // 右键菜单数据 + const handleContextMenuData = useCallback((payload: string) => { + setJwtInput(payload.replace(/^Bearer\s*/i, '').trim()); + }, []); + + useContextMenuData({ featureKey: 'jwt', onData: handleContextMenuData }); + + // 响应式解析 + const result = useMemo(() => { + if (!debouncedInput.trim()) return null; + return parseJwt(debouncedInput); + }, [debouncedInput]); + + const handleChange = useCallback((val: string) => { + setJwtInput(val.replace(/^Bearer\s*/i, '').trim()); + }, []); + + const handleClear = useCallback(() => setJwtInput(''), []); + + return { jwtInput, result, handleChange, handleClear }; +} diff --git a/src/pages/RightClickRestorer/index.tsx b/src/pages/RightClickRestorer/index.tsx index 3b14f78..35dec5f 100644 --- a/src/pages/RightClickRestorer/index.tsx +++ b/src/pages/RightClickRestorer/index.tsx @@ -5,7 +5,7 @@ import { Shield, ShieldCheck, MousePointerClick, AlertTriangle } from 'lucide-re import { useRightClickRestorer } from './useRightClickRestorer'; import { useI18n } from '@/utils/chromeI18n'; -export default function RightClickRestorerPage() { +export default function Index() { const { t } = useI18n('rightClickRestorer'); const { domain, isLoading, isUnlocked, isUnsupported, unlock } = useRightClickRestorer(); diff --git a/src/pages/TextStatistics/index.tsx b/src/pages/TextStatistics/index.tsx index ef96961..438251e 100644 --- a/src/pages/TextStatistics/index.tsx +++ b/src/pages/TextStatistics/index.tsx @@ -1,21 +1,12 @@ -import { useCallback, useMemo, useState } from 'react'; import TextInputArea from '@/components/TextInputArea'; -import { formatByteSize, getTextStats } from '@/utils/textStatistics'; +import { formatByteSize } from '@/utils/textStatistics'; import { useI18n } from '@/utils/chromeI18n'; -import { useContextMenuData } from '@/utils/useContextMenuData'; +import { useTextStatistics } from './useTextStatistics'; import { cn } from '@/lib/utils'; export default function Index() { const { t } = useI18n('textStatistics'); - const [text, setText] = useState(''); - - const handleContextMenuData = useCallback((payload: string) => { - setText(payload); - }, []); - - useContextMenuData({ featureKey: 'textStatistics', onData: handleContextMenuData }); - - const stats = useMemo(() => getTextStats(text), [text]); + const { text, stats, setText } = useTextStatistics(); const statItems = [ { label: t('textStatistics:characters'), value: stats.characters }, @@ -26,7 +17,6 @@ export default function Index() { return (

- {/* 文本输入区域 */} - {/* 统计结果展示区域 */}
{statItems.map((item) => (
void; +} + +export function useTextStatistics(): UseTextStatisticsReturn { + const [text, setText] = useState(''); + + const handleContextMenuData = useCallback((payload: string) => { + setText(payload); + }, []); + + useContextMenuData({ featureKey: 'textStatistics', onData: handleContextMenuData }); + + const stats = useMemo(() => getTextStats(text), [text]); + + return { text, stats, setText }; +}