diff --git a/src/components/RouterContainer.tsx b/src/components/RouterContainer.tsx index 36a46ec..1dbb08a 100644 --- a/src/components/RouterContainer.tsx +++ b/src/components/RouterContainer.tsx @@ -41,7 +41,7 @@ export default function RouterContainer() {
-

{'页面未找到'}

+

页面未找到

{`该功能在当前运行环境(${entryPointType})下不可用或已被移除。`}

diff --git a/src/components/TextInputArea.tsx b/src/components/TextInputArea.tsx index 58e79a9..f1fcaff 100644 --- a/src/components/TextInputArea.tsx +++ b/src/components/TextInputArea.tsx @@ -1,7 +1,7 @@ import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react'; import { X } from 'lucide-react'; import { cn } from '@/lib/utils'; -import { toast } from 'sonner'; // 推荐使用 shadcn 的默认 Toast +import { toast } from 'sonner'; import { CopyButton } from '@/components/CopyButton'; import { Button } from './ui/button'; @@ -44,7 +44,6 @@ export interface TextInputAreaProps extends Omit< onClear?: () => void; } -// 提炼基础的 ActionButton,全面向 shadcn 核心 Button 样式对齐 function ActionButton({ action, value, @@ -126,7 +125,6 @@ const TextInputArea = forwardRef((props const textArea = internalRef.current; if (!textArea) return; - // 重置高度计算 textArea.style.height = 'auto'; const computedMin = minRows * 24; @@ -255,7 +253,6 @@ const TextInputArea = forwardRef((props {hasBottomBar && (
- {/* 左侧自定义动作 */}
{bottomActions.map((action) => ( ((props ))}
- {/* 右侧系统按钮组 */}
{allowCopy && value && ( - + )} {showClear && value && !disabled && !readOnly && (
- {/* 错误提示 */} {displayError && (

{displayError} diff --git a/src/components/__tests__/CopyButton.test.tsx b/src/components/__tests__/CopyButton.test.tsx index 845fa0d..6d1c8ac 100644 --- a/src/components/__tests__/CopyButton.test.tsx +++ b/src/components/__tests__/CopyButton.test.tsx @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { act, render, screen, waitFor } from '@testing-library/react'; +import { act, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; // unmock the globally-mocked component so we test the real implementation @@ -68,15 +68,10 @@ describe('CopyButton', () => { expect(mockedToast.error).toHaveBeenCalled(); }); - // ==================== 新增测试 ==================== - - it('初始渲染时显示 Copy 图标且无 emerald 样式', () => { + it('初始渲染时带有默认 aria-label', () => { render(); - const button = screen.getByRole('button'); - expect(button.className).not.toContain('text-emerald'); - // 通过 aria-label 确认按钮存在,图标由 lucide 渲染为 svg - expect(button).toHaveAttribute('aria-label'); + expect(screen.getByRole('button')).toHaveAttribute('aria-label', '复制'); }); it('自定义 tooltip 会覆盖默认 title 和 aria-label', () => { @@ -119,15 +114,12 @@ describe('CopyButton', () => { await user.click(screen.getByRole('button')); - // 在 1.5 秒超时到期前卸载组件 act(() => { vi.advanceTimersByTime(500); }); - // 卸载不应抛出 "Can't perform a React state update on an unmounted component" 警告 expect(() => unmount()).not.toThrow(); - // 前进剩余时间,确认没有异常 act(() => { vi.advanceTimersByTime(2000); }); @@ -141,23 +133,15 @@ describe('CopyButton', () => { const button = screen.getByRole('button'); - // 快速点击 3 次 await user.click(button); await user.click(button); await user.click(button); - // copyTextToClipboard 应该被调用 3 次(每次点击都执行) expect(mockedCopy).toHaveBeenCalledTimes(3); - // 但 setTimeout 相关的 clearTimeout + setTimeout 组合应正常工作 - // advance 1.5 秒后,copied 状态应恢复为 false act(() => { vi.advanceTimersByTime(1500); }); - - await waitFor(() => { - expect(button.className).not.toContain('text-emerald'); - }); }); it('其他 button props 通过 ...props 透传', () => { diff --git a/src/components/__tests__/EmptyPlaceholder.test.tsx b/src/components/__tests__/EmptyPlaceholder.test.tsx index e72fbb8..d5e6ce7 100644 --- a/src/components/__tests__/EmptyPlaceholder.test.tsx +++ b/src/components/__tests__/EmptyPlaceholder.test.tsx @@ -4,13 +4,13 @@ import EmptyPlaceholder from '@/components/EmptyPlaceholder'; describe('EmptyPlaceholder 组件', () => { it('应渲染字符串提示文本', () => { - render({'请输入内容'}); + render(请输入内容); expect(screen.getByText('请输入内容')).toBeInTheDocument(); }); it('应应用规范空状态容器样式', () => { - const { container } = render({'提示'}); + const { container } = render(提示); const placeholder = container.firstChild; expect(placeholder).toHaveClass( @@ -24,14 +24,14 @@ describe('EmptyPlaceholder 组件', () => { it('应支持 className 自定义容器样式', () => { const { container } = render( - {'提示'}, + 提示, ); expect(container.firstChild).toHaveClass('flex-1', 'min-h-[320px]'); }); it('应支持 messageClassName 自定义文本样式', () => { - render({'提示'}); + render(提示); expect(screen.getByText('提示')).toHaveClass('text-sm', 'max-w-none'); }); diff --git a/src/entrypoints/background.ts b/src/entrypoints/background.ts index 90f0771..7c56c95 100644 --- a/src/entrypoints/background.ts +++ b/src/entrypoints/background.ts @@ -41,14 +41,11 @@ export default defineBackground(() => { try { await browser.action.openPopup(); } catch (err) { - console.warn( - '[Context Menu] 自动打开 popup 失败,请手动点击扩展图标,暂存数据已安全保留:', - err, - ); + console.warn('[Context Menu] 自动打开 popup 失败,请手动点击扩展图标,数据已暂存:', err); } }); - // 监听扩展图标点击事件,安全激活侧边栏 + // 扩展图标点击时打开侧边栏 browser.action.onClicked.addListener(async (tab) => { if (tab.id) { try { diff --git a/src/entrypoints/content/contextMenuHandler.ts b/src/entrypoints/content/contextMenuHandler.ts index 4594960..c5671e2 100644 --- a/src/entrypoints/content/contextMenuHandler.ts +++ b/src/entrypoints/content/contextMenuHandler.ts @@ -4,18 +4,17 @@ import { getTextStats } from '@/utils/textStatistics'; import { hidePopover, showTextStatsResult, showTimestampResult } from './uiPopover'; function convertTimestamp(input: string): string { - const invalidText = '无效时间戳'; const num = Number(input.trim()); if (isNaN(num)) { - return invalidText; + return '无效时间戳'; } - // 1e12 判定毫秒级/秒级时间戳兼容 + // 1e12 区分毫秒/秒级时间戳 const d = num > 1e12 ? new Date(num) : new Date(num * 1000); if (isNaN(d.getTime())) { - return invalidText; + return '无效时间戳'; } const year = d.getFullYear(); diff --git a/src/entrypoints/popup/main.tsx b/src/entrypoints/popup/main.tsx index e13a462..6f602b5 100644 --- a/src/entrypoints/popup/main.tsx +++ b/src/entrypoints/popup/main.tsx @@ -1,11 +1,8 @@ import ReactDOM from 'react-dom/client'; -import { applyThemeFromSnapshot } from '@/utils/themeSnapshot'; import AppRoot from '@/providers/AppRoot'; import '@/index.css'; import App from './App.tsx'; -applyThemeFromSnapshot(); - ReactDOM.createRoot(document.getElementById('root')!).render( diff --git a/src/entrypoints/sidepanel/main.tsx b/src/entrypoints/sidepanel/main.tsx index e13a462..6f602b5 100644 --- a/src/entrypoints/sidepanel/main.tsx +++ b/src/entrypoints/sidepanel/main.tsx @@ -1,11 +1,8 @@ import ReactDOM from 'react-dom/client'; -import { applyThemeFromSnapshot } from '@/utils/themeSnapshot'; import AppRoot from '@/providers/AppRoot'; import '@/index.css'; import App from './App.tsx'; -applyThemeFromSnapshot(); - ReactDOM.createRoot(document.getElementById('root')!).render( diff --git a/src/layout/TopBar/SearchResultItem.tsx b/src/layout/TopBar/SearchResultItem.tsx index db020a9..62ca2b4 100644 --- a/src/layout/TopBar/SearchResultItem.tsx +++ b/src/layout/TopBar/SearchResultItem.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { type FeatureConfig } from '@/config/features'; import { cn } from '@/lib/utils'; @@ -8,7 +7,7 @@ interface SearchResultItemProps { onSelect: (feature: FeatureConfig) => void; } -const SearchResultItem = React.memo(({ feature, selected, onSelect }: SearchResultItemProps) => { +export default function SearchResultItem({ feature, selected, onSelect }: SearchResultItemProps) { return (

  • ); -}); -SearchResultItem.displayName = 'SearchResultItem'; - -export default SearchResultItem; +} diff --git a/src/layout/TopBar/constants.ts b/src/layout/TopBar/constants.ts index 41cbb2b..7e1924f 100644 --- a/src/layout/TopBar/constants.ts +++ b/src/layout/TopBar/constants.ts @@ -4,8 +4,6 @@ export const SEARCH_HISTORY_DISPLAY = 5; export const isSearchHistory = (val: unknown): val is string[] => Array.isArray(val) && val.every((item) => typeof item === 'string'); -/** 根据平台返回搜索框快捷键提示文案 */ export function getSearchShortcutLabel(): string { - if (typeof navigator === 'undefined') return 'Ctrl+K'; return /Mac|iPhone|iPad|iPod/i.test(navigator.userAgent) ? '⌘K' : 'Ctrl+K'; } diff --git a/src/layout/TopBar/index.tsx b/src/layout/TopBar/index.tsx index 3a66eb4..6513e12 100644 --- a/src/layout/TopBar/index.tsx +++ b/src/layout/TopBar/index.tsx @@ -8,18 +8,17 @@ import { useTopBar } from './useTopBar'; export default function TopBar() { const { searchQuery, - showResults, searchResults, recentFeatures, selectedIndex, isDashboard, ThemeIcon, themeTitle, + showDropdown, containerRef, inputRef, - setSearchQuery, - setShowResults, - setSelectedIndex, + handleSearchQueryChange, + handleSearchFocus, handleSelectFeature, handleKeyDown, cycleThemeMode, @@ -40,14 +39,6 @@ export default function TopBar() { }, ]; - const handleSearchQueryChange = (value: string) => { - setSearchQuery(value); - setShowResults(true); - setSelectedIndex(-1); - }; - - const showDropdown = showResults && (searchQuery.trim() || recentFeatures.length > 0); - return (
    @@ -70,7 +61,7 @@ export default function TopBar() { inputRef={inputRef} searchQuery={searchQuery} onSearchQueryChange={handleSearchQueryChange} - onFocus={() => setShowResults(true)} + onFocus={handleSearchFocus} onKeyDown={handleKeyDown} onClear={clearSearch} /> diff --git a/src/layout/TopBar/useTopBar.ts b/src/layout/TopBar/useTopBar.ts index 632b103..1e4b87c 100644 --- a/src/layout/TopBar/useTopBar.ts +++ b/src/layout/TopBar/useTopBar.ts @@ -9,18 +9,17 @@ import { isSearchHistory, SEARCH_HISTORY_DISPLAY, SEARCH_HISTORY_LIMIT } from '. export interface UseTopBarReturn { searchQuery: string; - showResults: boolean; searchResults: FeatureConfig[]; recentFeatures: FeatureConfig[]; selectedIndex: number; isDashboard: boolean; ThemeIcon: typeof Sun; themeTitle: string; + showDropdown: boolean; containerRef: RefObject; inputRef: RefObject; - setSearchQuery: (value: string) => void; - setShowResults: (value: boolean) => void; - setSelectedIndex: (value: number | ((prev: number) => number)) => void; + handleSearchQueryChange: (value: string) => void; + handleSearchFocus: () => void; handleSelectFeature: (feature: FeatureConfig) => void; handleKeyDown: (e: React.KeyboardEvent) => void; cycleThemeMode: () => void; @@ -90,7 +89,6 @@ export function useTopBar(): UseTopBarReturn { }, [searchHistory, searchQuery]); const saveToHistory = (featureKey: string) => { - if (!featureKey.trim()) return; setSearchHistory((prev) => [featureKey, ...prev.filter((h) => h !== featureKey)].slice(0, SEARCH_HISTORY_LIMIT), ); @@ -124,18 +122,14 @@ export function useTopBar(): UseTopBarReturn { setSelectedIndex((prev) => (prev > 0 ? prev - 1 : prev)); } else if (e.key === 'Enter') { e.preventDefault(); - if (selectedIndex >= 0 && selectedIndex < totalItems) { - if (searchQuery.trim()) { - handleSelectFeature(searchResults[selectedIndex]); - } else { - const selected = displayedHistory[selectedIndex]; - if (selected) { - handleSelectFeature(selected); - } - } - } else if (searchQuery.trim() && searchResults.length > 0) { - handleSelectFeature(searchResults[0]); - } + const items = searchQuery.trim() ? searchResults : displayedHistory; + const feature = + selectedIndex >= 0 && selectedIndex < totalItems + ? items[selectedIndex] + : searchQuery.trim() && searchResults.length > 0 + ? searchResults[0] + : undefined; + if (feature) handleSelectFeature(feature); } else if (e.key === 'Escape') { setShowResults(false); inputRef.current?.blur(); @@ -147,20 +141,30 @@ export function useTopBar(): UseTopBarReturn { setSelectedIndex(-1); }; + const handleSearchQueryChange = (value: string) => { + setSearchQuery(value); + setShowResults(true); + setSelectedIndex(-1); + }; + + const handleSearchFocus = () => setShowResults(true); + + const showDropdown = + showResults && (searchQuery.trim().length > 0 || displayedHistory.length > 0); + return { searchQuery, - showResults, searchResults, recentFeatures: displayedHistory, selectedIndex, + showDropdown, isDashboard: currentPage === 'dashboard', ThemeIcon, themeTitle, containerRef, inputRef, - setSearchQuery, - setShowResults, - setSelectedIndex, + handleSearchQueryChange, + handleSearchFocus, handleSelectFeature, handleKeyDown, cycleThemeMode, diff --git a/src/lib/README.md b/src/lib/README.md index 9274705..4ea8d0f 100644 --- a/src/lib/README.md +++ b/src/lib/README.md @@ -37,7 +37,6 @@ import { cn } from '@/lib/utils'; | `business.ts` | 业务数据生成器(订单号、价格、日期、状态等) | | `technical.ts` | 技术数据生成器(UUID、IPv4、URL) | | `basic.ts` | 基础类型生成器(整数、浮点数、布尔值、字符串) | -| `types.ts` | 生成器库内部类型与通用选项 | 使用示例: diff --git a/src/lib/generators/basic.ts b/src/lib/generators/basic.ts index c7118ca..f0decf2 100644 --- a/src/lib/generators/basic.ts +++ b/src/lib/generators/basic.ts @@ -1,14 +1,6 @@ -/** - * 基础类型生成器 - * 包含:随机整数、随机浮点数、随机字符串、从列表选择 - */ - import type { GeneratorDefinition } from '@/types/testDataGenerator'; import { randomInt, randomPick } from './random'; -/** - * 随机整数生成器 - */ export const randomIntGenerator: GeneratorDefinition = { id: 'randomInt', name: '随机整数', @@ -43,9 +35,6 @@ export const randomIntGenerator: GeneratorDefinition = { }, }; -/** - * 随机浮点数生成器 - */ export const randomFloat: GeneratorDefinition = { id: 'randomFloat', name: '随机浮点数', @@ -85,9 +74,6 @@ export const randomFloat: GeneratorDefinition = { }, }; -/** - * 随机字符串生成器 - */ export const randomString: GeneratorDefinition = { id: 'randomString', name: '随机字符串', @@ -166,9 +152,6 @@ export const randomString: GeneratorDefinition = { }, }; -/** - * 从列表选择生成器 - */ export const fromList: GeneratorDefinition = { id: 'fromList', name: '从列表选择', diff --git a/src/lib/generators/business.ts b/src/lib/generators/business.ts index cc366ae..32791a9 100644 --- a/src/lib/generators/business.ts +++ b/src/lib/generators/business.ts @@ -1,14 +1,6 @@ -/** - * 业务数据生成器 - * 包含:订单号、价格、日期、状态、数量、评分、折扣、库存 - */ - import type { GeneratorDefinition } from '@/types/testDataGenerator'; import { randomInt, randomPick } from './random'; -/** - * 格式化日期 - */ function formatDate(date: Date, format: string): string { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); @@ -26,9 +18,6 @@ function formatDate(date: Date, format: string): string { .replace('ss', seconds); } -/** - * 订单号生成器 - */ export const orderId: GeneratorDefinition = { id: 'orderId', name: '订单号', @@ -68,9 +57,6 @@ export const orderId: GeneratorDefinition = { }, }; -/** - * 价格生成器 - */ export const price: GeneratorDefinition = { id: 'price', name: '价格', @@ -142,9 +128,6 @@ export const price: GeneratorDefinition = { }, }; -/** - * 生成心理定价(如 9.99, 19.9, 99) - */ function generatePsychologicalPrice(min: number, max: number): number { const patterns = [ () => { @@ -167,9 +150,6 @@ function generatePsychologicalPrice(min: number, max: number): number { return price; } -/** - * 生成真实分布价格(对数正态分布) - */ function generateRealisticPrice(min: number, max: number): number { const mean = (Math.log(min) + Math.log(max)) / 2; const stdDev = (Math.log(max) - Math.log(min)) / 4; @@ -182,9 +162,6 @@ function generateRealisticPrice(min: number, max: number): number { return Math.max(min, Math.min(max, value)); } -/** - * 日期生成器 - */ export const date: GeneratorDefinition = { id: 'date', name: '日期', @@ -235,9 +212,6 @@ export const date: GeneratorDefinition = { }, }; -/** - * 状态生成器 - */ export const status: GeneratorDefinition = { id: 'status', name: '状态', @@ -290,9 +264,6 @@ export const status: GeneratorDefinition = { }, }; -/** - * 数量生成器 - */ export const quantity: GeneratorDefinition = { id: 'quantity', name: '数量', @@ -339,9 +310,6 @@ export const quantity: GeneratorDefinition = { }, }; -/** - * 泊松分布随机数 - */ function poissonRandom(lambda: number): number { const L = Math.exp(-lambda); let k = 0; @@ -353,9 +321,6 @@ function poissonRandom(lambda: number): number { return k - 1; } -/** - * 评分生成器 - */ export const rating: GeneratorDefinition = { id: 'rating', name: '评分', @@ -410,9 +375,6 @@ export const rating: GeneratorDefinition = { }, }; -/** - * 偏向高分的随机数(模拟真实评分分布) - */ function skewedRandom(min: number, max: number): number { // 使用 Beta 分布模拟评分偏向 const alpha = 3; @@ -425,9 +387,6 @@ function skewedRandom(min: number, max: number): number { return min + x * (max - min); } -/** - * 折扣生成器 - */ export const discount: GeneratorDefinition = { id: 'discount', name: '折扣', @@ -479,9 +438,6 @@ export const discount: GeneratorDefinition = { }, }; -/** - * 生成心理折扣(如 5%, 10%, 20%, 30%) - */ function generatePsychologicalDiscount(min: number, max: number): number { const psychologicalDiscounts = [5, 10, 15, 20, 25, 30, 40, 50, 60, 70, 80]; const validDiscounts = psychologicalDiscounts.filter((d) => d >= min && d <= max); @@ -491,9 +447,6 @@ function generatePsychologicalDiscount(min: number, max: number): number { return randomInt(min, max); } -/** - * 库存生成器 - */ export const stock: GeneratorDefinition = { id: 'stock', name: '库存', diff --git a/src/lib/generators/index.ts b/src/lib/generators/index.ts index 6ce4f5a..9b63cb1 100644 --- a/src/lib/generators/index.ts +++ b/src/lib/generators/index.ts @@ -1,17 +1,9 @@ -/** - * 生成器库索引 - * 导出所有生成器和分类配置 - */ - import type { GeneratorDefinition, GeneratorCategory } from '@/types/testDataGenerator'; import { personalGenerators } from './personal'; import { businessGenerators } from './business'; import { technicalGenerators } from './technical'; import { basicGenerators } from './basic'; -/** - * 生成器分类配置 - */ export const generatorCategories: GeneratorCategory[] = [ { id: 'personal', name: '个人信息', icon: 'User' }, { id: 'business', name: '业务数据', icon: 'Briefcase' }, @@ -19,9 +11,6 @@ export const generatorCategories: GeneratorCategory[] = [ { id: 'basic', name: '基础类型', icon: 'Hash' }, ]; -/** - * 所有生成器列表 - */ export const allGenerators: GeneratorDefinition[] = [ ...personalGenerators, ...businessGenerators, @@ -29,23 +18,14 @@ export const allGenerators: GeneratorDefinition[] = [ ...basicGenerators, ]; -/** - * 根据 ID 获取生成器 - */ export function getGeneratorById(id: string): GeneratorDefinition | undefined { return allGenerators.find((g) => g.id === id); } -/** - * 根据分类 ID 获取生成器列表 - */ export function getGeneratorsByCategory(categoryId: string): GeneratorDefinition[] { return allGenerators.filter((g) => g.categoryId === categoryId); } -/** - * 搜索生成器 - */ export function searchGenerators(query: string): GeneratorDefinition[] { const lowerQuery = query.toLowerCase(); return allGenerators.filter( diff --git a/src/lib/generators/personal.ts b/src/lib/generators/personal.ts index d7d2235..5fc89ab 100644 --- a/src/lib/generators/personal.ts +++ b/src/lib/generators/personal.ts @@ -1,12 +1,6 @@ -/** - * 个人信息生成器 - * 包含:中文姓名、邮箱、手机号、身份证号、中文地址、年龄 - */ - import type { GeneratorDefinition } from '@/types/testDataGenerator'; import { randomInt, randomPick } from './random'; -// 中文姓氏 const SURNAMES = [ '王', '李', @@ -60,7 +54,6 @@ const SURNAMES = [ '阎', ]; -// 中文名字常用字 const NAME_CHARS = [ '伟', '芳', @@ -114,10 +107,8 @@ const NAME_CHARS = [ '慧', ]; -// 邮箱域名 const EMAIL_DOMAINS = ['qq.com', '163.com', '126.com', 'gmail.com', 'outlook.com', 'hotmail.com']; -// 省份 const PROVINCES = [ '北京市', '天津市', @@ -153,7 +144,6 @@ const PROVINCES = [ '新疆维吾尔自治区', ]; -// 城市 const CITIES: Record = { 北京市: ['东城区', '西城区', '朝阳区', '海淀区'], 上海市: ['黄浦区', '徐汇区', '长宁区', '静安区'], @@ -163,9 +153,6 @@ const CITIES: Record = { 四川省: ['成都市', '绵阳市', '德阳市', '宜宾市'], }; -/** - * 中文姓名生成器 - */ export const chineseName: GeneratorDefinition = { id: 'chineseName', name: '中文姓名', @@ -179,9 +166,6 @@ export const chineseName: GeneratorDefinition = { }, }; -/** - * 邮箱生成器 - */ export const email: GeneratorDefinition = { id: 'email', name: '邮箱', @@ -208,9 +192,6 @@ export const email: GeneratorDefinition = { }, }; -/** - * 手机号生成器 - */ export const chinesePhone: GeneratorDefinition = { id: 'chinesePhone', name: '手机号', @@ -241,9 +222,6 @@ export const chinesePhone: GeneratorDefinition = { }, }; -/** - * 身份证号生成器 - */ export const idCard: GeneratorDefinition = { id: 'idCard', name: '身份证号', @@ -268,20 +246,12 @@ export const idCard: GeneratorDefinition = { generate: (params) => { const regions = ['110101', '310101', '440103', '440305', '510104', '330102']; const region = params.region === 'random' ? randomPick(regions) : (params.region as string); - - // 出生日期 1960-2005年 const year = randomInt(1960, 2005); const month = String(randomInt(1, 12)).padStart(2, '0'); const day = String(randomInt(1, 28)).padStart(2, '0'); const birthday = `${year}${month}${day}`; - - // 顺序码 const sequence = String(randomInt(1, 999)).padStart(3, '0'); - - // 前17位 const prefix17 = region + birthday + sequence; - - // 校验码 const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]; const checkCodes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']; let sum = 0; @@ -317,9 +287,6 @@ export const idCard: GeneratorDefinition = { }, }; -/** - * 中文地址生成器 - */ export const chineseAddress: GeneratorDefinition = { id: 'chineseAddress', name: '中文地址', @@ -348,9 +315,6 @@ export const chineseAddress: GeneratorDefinition = { }, }; -/** - * 年龄生成器 - */ export const age: GeneratorDefinition = { id: 'age', name: '年龄', @@ -402,9 +366,6 @@ export const age: GeneratorDefinition = { }, }; -/** - * 正态分布随机数 - */ function normalRandom(min: number, max: number): number { const mean = (min + max) / 2; const stdDev = (max - min) / 6; @@ -417,9 +378,6 @@ function normalRandom(min: number, max: number): number { return Math.max(min, Math.min(max, value)); } -/** - * 人口统计分布随机数(模拟真实年龄分布) - */ function demographicRandom(): number { const ageGroups = [ { min: 0, max: 14, weight: 0.18 }, diff --git a/src/lib/generators/technical.ts b/src/lib/generators/technical.ts index e7da0ac..bbbd38e 100644 --- a/src/lib/generators/technical.ts +++ b/src/lib/generators/technical.ts @@ -1,14 +1,6 @@ -/** - * 技术数据生成器 - * 包含:UUID、IPv4、URL - */ - import type { GeneratorDefinition } from '@/types/testDataGenerator'; import { randomInt, randomPick } from './random'; -/** - * UUID 生成器 - */ export const uuid: GeneratorDefinition = { id: 'uuid', name: 'UUID', @@ -65,9 +57,6 @@ export const uuid: GeneratorDefinition = { }, }; -/** - * 生成 UUID v4 - */ function generateUUIDv4(): string { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { const r = (Math.random() * 16) | 0; @@ -76,9 +65,6 @@ function generateUUIDv4(): string { }); } -/** - * 生成 UUID v1(简化版本,模拟时间戳) - */ function generateUUIDv1(): string { const now = Date.now(); const timeLow = (now & 0xffffffff).toString(16).padStart(8, '0'); @@ -92,9 +78,6 @@ function generateUUIDv1(): string { return `${timeLow}-${timeMid}-${timeHi.toString(16)}-${clockSeq.toString(16)}-${node}`; } -/** - * IPv4 生成器 - */ export const ipv4: GeneratorDefinition = { id: 'ipv4', name: 'IPv4', @@ -129,16 +112,10 @@ export const ipv4: GeneratorDefinition = { }, }; -/** - * 生成完全随机 IPv4 - */ function generateRandomIPv4(): string { return Array.from({ length: 4 }, () => randomInt(0, 255)).join('.'); } -/** - * 生成内网 IPv4 - */ function generatePrivateIPv4(): string { const ranges = [ { prefix: '10', second: () => randomInt(0, 255) }, @@ -153,9 +130,6 @@ function generatePrivateIPv4(): string { return `${range.prefix}.${range.second()}.${randomInt(1, 254)}.${randomInt(1, 254)}`; } -/** - * 生成公网 IPv4 - */ function generatePublicIPv4(): string { let first: number; do { @@ -165,9 +139,6 @@ function generatePublicIPv4(): string { return `${first}.${randomInt(0, 255)}.${randomInt(0, 255)}.${randomInt(1, 254)}`; } -/** - * URL 生成器 - */ export const url: GeneratorDefinition = { id: 'url', name: 'URL', @@ -237,8 +208,6 @@ export const url: GeneratorDefinition = { path = `/${randomPick(['about', 'contact', 'products', 'services', 'blog'])}`; } } - - // 添加查询参数 let query = ''; if (Math.random() > 0.5) { const params = new URLSearchParams(); diff --git a/src/lib/generators/types.ts b/src/lib/generators/types.ts deleted file mode 100644 index f21c44e..0000000 --- a/src/lib/generators/types.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * 生成器内部类型定义 - * 与 testDataGenerator.ts 分离,用于生成器库内部 - */ - -import type { GeneratorDefinition, GeneratorCategory } from '@/types/testDataGenerator'; - -export type { GeneratorDefinition, GeneratorCategory }; - -/** - * 生成器注册表 - */ -export interface GeneratorRegistry { - /** 分类列表 */ - categories: GeneratorCategory[]; - /** 生成器列表 */ - generators: GeneratorDefinition[]; -} - -/** - * 随机数生成选项 - */ -export interface RandomOptions { - /** 最小值 */ - min?: number; - /** 最大值 */ - max?: number; - /** 是否包含最小值 */ - includeMin?: boolean; - /** 是否包含最大值 */ - includeMax?: boolean; -} - -/** - * 字符集选项 - */ -export interface CharsetOptions { - /** 是否包含大写字母 */ - uppercase?: boolean; - /** 是否包含小写字母 */ - lowercase?: boolean; - /** 是否包含数字 */ - digits?: boolean; - /** 是否包含特殊字符 */ - special?: boolean; - /** 自定义字符集 */ - custom?: string; -} diff --git a/src/pages/Base64Converter/components/Base64ConverterSection.tsx b/src/pages/Base64Converter/components/Base64ConverterSection.tsx index 75d19e4..a3ec4ee 100644 --- a/src/pages/Base64Converter/components/Base64ConverterSection.tsx +++ b/src/pages/Base64Converter/components/Base64ConverterSection.tsx @@ -40,12 +40,12 @@ export default function Base64ConverterSection({ mode }: Base64ConverterSectionP decodedFileName, setCustomFileName, resetAll, - safeFileSelect, + handleFileSelect, maxFileSizeStr, } = useBase64Converter({ mode }); const handleDirectionChange = (next: Base64ConvertDirection) => { - if (!next || next === direction) return; + if (next === direction) return; resetAll(); setDirection(next); }; @@ -80,7 +80,7 @@ export default function Base64ConverterSection({ mode }: Base64ConverterSectionP e.preventDefault(); setIsDragging(false); const file = e.dataTransfer.files[0]; - if (file) safeFileSelect(file); + if (file) void handleFileSelect(file); }} onClick={() => fileInputRef.current?.click()} className={cn( @@ -99,7 +99,7 @@ export default function Base64ConverterSection({ mode }: Base64ConverterSectionP hidden onChange={(e) => { const file = e.target.files?.[0]; - if (file) safeFileSelect(file); + if (file) void handleFileSelect(file); }} /> {isLoading ? ( @@ -123,7 +123,7 @@ export default function Base64ConverterSection({ mode }: Base64ConverterSectionP {formatBytes(info.size)} · {info.type} - {'点击或拖拽以替换文件'} + 点击或拖拽以替换文件
    ) : ( @@ -158,17 +158,17 @@ export default function Base64ConverterSection({ mode }: Base64ConverterSectionP
    - {'Base64 编码结果'} + Base64 编码结果
    @@ -186,14 +186,14 @@ export default function Base64ConverterSection({ mode }: Base64ConverterSectionP
    - {'原始大小'}:{' '} + 原始大小:{' '} {formatBytes(result.originalBytes)} | - {'编码大小'}:{' '} + 编码大小:{' '} {formatBytes(result.outputBytes)} @@ -206,7 +206,7 @@ export default function Base64ConverterSection({ mode }: Base64ConverterSectionP className="h-7 rounded-md text-muted-foreground hover:text-destructive text-[11px] gap-1 px-2" > - {'清空'} + 清空
    @@ -215,7 +215,7 @@ export default function Base64ConverterSection({ mode }: Base64ConverterSectionP ) : (
    void; - /** 下载按钮点击回调 */ onDownload: () => void; - /** 可选的预览内容,ImageMode 用于渲染图片预览 */ children?: React.ReactNode; } @@ -41,31 +25,20 @@ export default function DecodeResultPaper({ }: DecodeResultPaperProps) { return (
    - {/* 标题 */} {title} - {/* 可选预览内容(ImageMode 的图片) */} {children} - {/* 文件信息 */}
    - - {'推断的 MIME 类型'}: {mimeType} - - - {'解码大小'}: {formatBytes(blobSize)} - + 推断的 MIME 类型: {mimeType} + 解码大小: {formatBytes(blobSize)}
    - {/* 文件名输入 */}
    - + onFileNameChange(e.target.value)} />
    - {/* 下载按钮 */}
    ); diff --git a/src/pages/Base64Converter/components/TextMode.tsx b/src/pages/Base64Converter/components/TextMode.tsx index 560ca66..19125cb 100644 --- a/src/pages/Base64Converter/components/TextMode.tsx +++ b/src/pages/Base64Converter/components/TextMode.tsx @@ -8,7 +8,7 @@ interface TextModeProps { onSwitchToImageMode?: () => void; } -export default function TextMode({ onSwitchToImageMode }: TextModeProps = {}) { +export default function TextMode({ onSwitchToImageMode }: TextModeProps) { const { input, setInput, diff --git a/src/pages/Base64Converter/useBase64Converter.ts b/src/pages/Base64Converter/useBase64Converter.ts index b9f8485..89a4baa 100644 --- a/src/pages/Base64Converter/useBase64Converter.ts +++ b/src/pages/Base64Converter/useBase64Converter.ts @@ -93,16 +93,7 @@ export function useBase64Converter({ mode }: UseBase64ConverterProps) { [mode], ); - const safeFileSelect = useCallback( - (file: File) => { - handleFileSelect(file).catch((err) => { - console.error(`Base64 [${mode}] pipeline crash:`, err); - }); - }, - [handleFileSelect, mode], - ); - - const decodePipeline = useMemo(() => { + const decodeResult = useMemo(() => { const cleanedInput = debouncedDecodeInput.replace(/^data:image\/[a-z+]+;base64,/i, '').trim(); if (!cleanedInput) return { decoded: null, error: null }; @@ -118,8 +109,8 @@ export function useBase64Converter({ mode }: UseBase64ConverterProps) { } }, [debouncedDecodeInput]); - const decoded = decodePipeline.decoded; - const decodeError = decodePipeline.error; + const decoded = decodeResult.decoded; + const decodeError = decodeResult.error; const decodedFileName = useMemo(() => { if (customFileName) return customFileName; @@ -144,7 +135,7 @@ export function useBase64Converter({ mode }: UseBase64ConverterProps) { decodedFileName, setCustomFileName, resetAll, - safeFileSelect, + handleFileSelect, maxFileSizeStr, }; } diff --git a/src/pages/Base64Converter/useTextMode.ts b/src/pages/Base64Converter/useTextMode.ts index a5ac180..73cddcc 100644 --- a/src/pages/Base64Converter/useTextMode.ts +++ b/src/pages/Base64Converter/useTextMode.ts @@ -4,7 +4,7 @@ import { useContextMenuData } from '@/utils/useContextMenuData'; const IMAGE_DATA_URI_PATTERN = /^\s*data:image\//i; -const ERROR_MESSAGE_TO_I18N: Record = { +const ERROR_MESSAGES: Record = { 'Invalid Base64 string': '无效的 Base64 字符串', 'Input appears to be binary data (e.g. an image). Please use the Image tab instead.': '检测到二进制数据(如图像),请使用图像选项卡', @@ -30,7 +30,7 @@ export function useTextMode() { useContextMenuData({ featureKey: 'base64Converter', onData: handleContextMenuData }); - const conversionPipeline = useMemo(() => { + const conversion = useMemo(() => { const trimmed = debouncedInput.trim(); if (!trimmed) return { output: '', error: null }; @@ -38,31 +38,26 @@ export function useTextMode() { if (direction === 'encode') { const result = textToBase64(debouncedInput); return { output: result.output, error: null }; - } else { - const decoded = base64ToText(trimmed); - return { output: decoded, error: null }; } + const decoded = base64ToText(trimmed); + return { output: decoded, error: null }; } catch (e) { const message = e instanceof Error ? e.message : ''; - const i18nKey = ERROR_MESSAGE_TO_I18N[message]; return { output: '', - error: i18nKey || message || '转换失败', + error: ERROR_MESSAGES[message] || message || '转换失败', }; } }, [debouncedInput, direction]); - const output = conversionPipeline.output; - const error = conversionPipeline.error; + const output = conversion.output; + const error = conversion.error; const placeholder = direction === 'encode' ? '输入需要编码为 Base64 的文本...' : '输入需要解码的 Base64 字符串...'; const outputLabel = direction === 'encode' ? 'Base64 编码结果' : '解码文本结果'; - const showImageHint = useMemo( - () => direction === 'decode' && IMAGE_DATA_URI_PATTERN.test(input), - [direction, input], - ); + const showImageHint = direction === 'decode' && IMAGE_DATA_URI_PATTERN.test(input); const handleDirectionChange = (value: 'encode' | 'decode') => { if (value === direction) return; diff --git a/src/pages/Dashboard/__tests__/index.test.tsx b/src/pages/Dashboard/__tests__/index.test.tsx index 5810dd4..60d73b2 100644 --- a/src/pages/Dashboard/__tests__/index.test.tsx +++ b/src/pages/Dashboard/__tests__/index.test.tsx @@ -1,8 +1,7 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { getFeatureByKey } from '@/config/features'; -import type { DashboardFeatureItem } from '../useDashboard'; +import { resolveDashboardFeatures, type DashboardFeatureItem } from '../dashboardFeatures'; import Index from '../index'; const mockNavigateTo = vi.fn(); @@ -13,11 +12,7 @@ vi.mock('../useDashboard', () => ({ })); function buildFeatureItem(key: 'timestamp' | 'jwt'): DashboardFeatureItem { - const feature = getFeatureByKey(key)!; - return { - key, - feature: feature as DashboardFeatureItem['feature'], - }; + return resolveDashboardFeatures([key], [key])[0]!; } describe('Dashboard 页面', () => { diff --git a/src/pages/Dashboard/dashboardFeatures.ts b/src/pages/Dashboard/dashboardFeatures.ts index ee38d29..fee39ad 100644 --- a/src/pages/Dashboard/dashboardFeatures.ts +++ b/src/pages/Dashboard/dashboardFeatures.ts @@ -14,10 +14,6 @@ function isDashboardFeature(feature: FeatureConfig): feature is DashboardFeature return feature.icon != null; } -/** - * 将页面 key 列表解析为 Dashboard 可展示的工具项。 - * 过滤条件:在 visiblePages 中且 feature 配置了 icon。 - */ export function resolveDashboardFeatures( keys: PageType[], visiblePages: PageType[], diff --git a/src/pages/Dashboard/index.tsx b/src/pages/Dashboard/index.tsx index 72c532e..69acdde 100644 --- a/src/pages/Dashboard/index.tsx +++ b/src/pages/Dashboard/index.tsx @@ -9,7 +9,7 @@ export default function Index() { {showRecent && (

    - {'最近使用'} + 最近使用

    {recentFeatures.map(({ key, feature }) => { @@ -37,10 +37,10 @@ export default function Index() {

    - {'全部工具'} + 全部工具

    {visibleFeatures.length === 0 ? ( -

    {'没有可用的工具'}

    +

    没有可用的工具

    ) : (
    { it('应该渲染模式切换按钮', () => { render(); - // 基本渲染测试:页面含多个模式切换按钮,应至少渲染一个 expect(screen.getAllByRole('button').length).toBeGreaterThan(0); }); }); diff --git a/src/pages/JsonTools/components/DiffNavigator.tsx b/src/pages/JsonTools/components/DiffNavigator.tsx index cc5cb25..7392bcc 100644 --- a/src/pages/JsonTools/components/DiffNavigator.tsx +++ b/src/pages/JsonTools/components/DiffNavigator.tsx @@ -30,7 +30,7 @@ export default function DiffNavigator({ )} {...props} > - {'无差异'} + 无差异
    ); } @@ -43,11 +43,10 @@ export default function DiffNavigator({ )} {...props} > - {/* 上一处差异按钮 */}
    @@ -133,7 +133,6 @@ const UnifiedView = ({ node, depth, activePath }: UnifiedViewProps) => { ); } - // 容器节点整块渲染处理 if (node.type === 'added') { return ( { - translationPrefix: string; + mode: string; convertFunction: ConvertFunction; } export default function JsonConvertSection({ - translationPrefix, + mode, convertFunction, className, ...props @@ -41,10 +41,8 @@ export default function JsonConvertSection({ const [input, setInput] = useState(''); const [debouncedInput, setDebouncedInput] = useState(''); - const pk = translationPrefix; - const labels = CONVERT_LABELS[pk] || CONVERT_LABELS.yaml; + const labels = CONVERT_LABELS[mode] ?? CONVERT_LABELS.yaml; - // Debounce input useEffect(() => { const handle = setTimeout(() => { setDebouncedInput(input); @@ -56,33 +54,25 @@ export default function JsonConvertSection({ return validateJson(debouncedInput); }, [debouncedInput]); - const conversionPipeline = useMemo(() => { + const { result, runtimeError } = useMemo((): { + result: ConvertResult | null; + runtimeError: string | null; + } => { const trimmed = debouncedInput.trim(); - if (!trimmed || error) return null; + if (!trimmed || error) return { result: null, runtimeError: null }; try { - return convertFunction(debouncedInput); + return { result: convertFunction(debouncedInput), runtimeError: null }; } catch (e) { - // 捕获可能从外部转换器(如 YAML.stringify)中抛出的底层异常 return { - isRuntimeError: true, - errorMessage: e instanceof Error ? e.message : String(e), + result: null, + runtimeError: e instanceof Error ? e.message : String(e), }; } }, [debouncedInput, error, convertFunction]); - const runtimeError = - conversionPipeline && 'isRuntimeError' in conversionPipeline - ? conversionPipeline.errorMessage - : null; - const result = - conversionPipeline && !('isRuntimeError' in conversionPipeline) - ? (conversionPipeline as ConvertResult) - : null; - return (
    - {/* 输入区 */} setInput('')} /> - {/* Result display */} - {result && result.output ? ( + {result?.output ? ( (2); const [sortKeys, setSortKeys] = useState(false); - // Debounce input useEffect(() => { const handle = setTimeout(() => { setDebouncedInput(input); @@ -30,40 +29,31 @@ export default function JsonFormatSection() { return validateJson(debouncedInput); }, [debouncedInput]); - // Real-time formatting pipeline - const formattedPipeline = useMemo(() => { + const { result, runtimeError } = useMemo((): { + result: JsonFormatResult | null; + runtimeError: string | null; + } => { const trimmed = debouncedInput.trim(); - if (!trimmed || error) return null; + if (!trimmed || error) return { result: null, runtimeError: null }; try { const options: JsonFormatOptions = { indentSize, sortKeys }; - return formatJson(debouncedInput, options); + return { result: formatJson(debouncedInput, options), runtimeError: null }; } catch (e) { return { - isRuntimeError: true, - errorMessage: e instanceof SyntaxError ? e.message : String(e), + result: null, + runtimeError: e instanceof SyntaxError ? e.message : String(e), }; } }, [debouncedInput, error, indentSize, sortKeys]); - const runtimeError = - formattedPipeline && 'isRuntimeError' in formattedPipeline - ? formattedPipeline.errorMessage - : null; - const result = - formattedPipeline && !('isRuntimeError' in formattedPipeline) - ? (formattedPipeline as JsonFormatResult) - : null; - return (
    - {/* 工具控制栏 */}
    - {/* 缩进配置区 */}
    - {'缩进'} + 缩进 - {/* 键名排序区 */}
    setSortKeys(!sortKeys)} className="flex items-center gap-2 cursor-pointer select-none group py-1" @@ -91,15 +80,14 @@ export default function JsonFormatSection() { htmlFor="sort-keys-checkbox" className="text-xs font-bold text-foreground/80 cursor-pointer tracking-tight group-hover:text-foreground" > - {'键名排序'} + 键名排序
    - {/* 满血版输入终端 */} setInput('')} /> - {/* 格式化结果流面板展示 */} - {result && result.formatted ? ( + {result?.formatted ? ( - {'原始大小'}:{' '} + 原始大小:{' '} {formatBytes(originalBytes)} | diff --git a/src/pages/JsonTools/components/JsonTree.tsx b/src/pages/JsonTools/components/JsonTree.tsx index 0641979..e3ee6f2 100644 --- a/src/pages/JsonTools/components/JsonTree.tsx +++ b/src/pages/JsonTools/components/JsonTree.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'; -import { ChevronDown, ChevronRight } from 'lucide-react'; // 用正统的矢量箭头平替原生的字符 '▾' '▸' +import { ChevronDown, ChevronRight } from 'lucide-react'; import type { DiffNode, DiffType } from '../types'; import { cn } from '@/lib/utils'; @@ -82,14 +82,12 @@ const NodeRow = React.memo( ? false : onActivePath || depth < defaultExpandDepth; - // 当激活路径精准定位到本行时,平滑滚动至容器中心 useEffect(() => { if (activePath === node.path) { rowRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' }); } }, [activePath, node.path]); - // 占位空行分支:必须加 h-[22px] 锁定绝对等高,防止两侧文本高度塌陷发生高低错位 if (!shouldRenderOnSide(node.type, side)) { return (
    - {/* 大容器开端行 */}
    setOverride(expanded ? 'closed' : 'open')} className={cn( @@ -135,7 +132,6 @@ const NodeRow = React.memo( )} style={indentStyle} > - {/* 折叠小箭头:升级为精巧的 Lucide SVG 矢量微动效 */} {expanded ? ( @@ -164,7 +160,6 @@ const NodeRow = React.memo( )}
    - {/* 容器子节点递归区 */} {expanded && (
    {node.children.map((child, idx) => ( @@ -181,7 +176,6 @@ const NodeRow = React.memo(
    )} - {/* 大容器收尾行 */} {expanded && (
    - {/* 与上方的折叠键轴线严格对齐 */} + {!isRoot && ( {node.key}: )} @@ -231,7 +224,6 @@ export default function JsonTree({ ...props }: JsonTreeProps) { return ( - /* 最外层承载器:统一收拢至标准的 bg-card 与等宽 tabular-nums 控制轴 */
    ) : pageMode === 'yaml' ? ( - + ) : pageMode === 'toml' ? ( - + ) : ( - + )}
    ); diff --git a/src/pages/JsonTools/useJsonTools.ts b/src/pages/JsonTools/useJsonTools.ts index 2954413..46623a1 100644 --- a/src/pages/JsonTools/useJsonTools.ts +++ b/src/pages/JsonTools/useJsonTools.ts @@ -11,7 +11,6 @@ import type { ConvertFunction, ViewMode } from './types'; export interface UseJsonToolsReturn { pageMode: JsonToolsPageMode; setPageMode: (mode: JsonToolsPageMode) => void; - // Diff mode state leftInput: string; rightInput: string; setLeftInput: (val: string) => void; @@ -26,7 +25,6 @@ export interface UseJsonToolsReturn { handlePrev: () => void; handleNext: () => void; activePath: string | undefined; - // Convert functions yamlConvert: ConvertFunction; tomlConvert: ConvertFunction; minifyConvert: ConvertFunction; @@ -35,13 +33,11 @@ export interface UseJsonToolsReturn { export function useJsonTools(): UseJsonToolsReturn { 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); @@ -50,7 +46,6 @@ export function useJsonTools(): UseJsonToolsReturn { return () => clearTimeout(handle); }, [leftInput, rightInput]); - // Parse debounced inputs const parseState = useMemo(() => { const invalidMsg = '无效的 JSON 格式'; return { @@ -65,7 +60,6 @@ export function useJsonTools(): UseJsonToolsReturn { 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() === '') { @@ -88,7 +82,6 @@ export function useJsonTools(): UseJsonToolsReturn { 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 }; diff --git a/src/pages/QrCode/__tests__/ImageUploader.test.tsx b/src/pages/QrCode/__tests__/ImageUploader.test.tsx index 5d4807a..33b5df0 100644 --- a/src/pages/QrCode/__tests__/ImageUploader.test.tsx +++ b/src/pages/QrCode/__tests__/ImageUploader.test.tsx @@ -2,12 +2,10 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { act, fireEvent, render, screen } from '@testing-library/react'; import ImageUploader from '../components/ImageUploader'; -// 配置多端一致性常驻桩(WXT 规范) const storageOnChangedMock = { addListener: vi.fn(), removeListener: vi.fn() }; (globalThis as any).chrome = { storage: { onChanged: storageOnChangedMock } }; (globalThis as any).browser = { storage: { onChanged: storageOnChangedMock } }; -// 模拟 URL API const mockCreateObjectURL = vi.fn(); const mockRevokeObjectURL = vi.fn(); Object.defineProperty(window.URL, 'createObjectURL', { value: mockCreateObjectURL }); @@ -171,21 +169,4 @@ describe('ImageUploader 组件', () => { expect(pasteListeners).toHaveLength(0); }); }); - - describe('样式测试', () => { - it('拖拽状态时应应用拖拽样式', () => { - const { container } = render(); - const dropzone = container.firstChild as HTMLElement; - expect(dropzone).toBeInTheDocument(); - }); - - it('有文件时应应用有文件样式', () => { - const mockFile = new File(['test'], 'test.png', { type: 'image/png' }); - const { container } = render( - , - ); - const dropzone = container.firstChild as HTMLElement; - expect(dropzone).toBeInTheDocument(); - }); - }); }); diff --git a/src/pages/QrCode/components/GeneratePanel.tsx b/src/pages/QrCode/components/GeneratePanel.tsx index 7db02f6..f8c9bd3 100644 --- a/src/pages/QrCode/components/GeneratePanel.tsx +++ b/src/pages/QrCode/components/GeneratePanel.tsx @@ -6,7 +6,6 @@ import { Button } from '@/components/ui/button'; import { Label } from '@/components/ui/label'; import { cn } from '@/lib/utils'; -/** 文本预览的最大显示字符数 */ const TEXT_PREVIEW_MAX_LENGTH = 80; export default function GeneratePanel() { @@ -22,13 +21,11 @@ export default function GeneratePanel() { const isInputStep = generatorState.step === 'input'; const hasText = generatorState.textToEncode.trim().length > 0; - /** 截断文本用于预览显示 */ const getTruncatedText = (text: string) => { if (text.length <= TEXT_PREVIEW_MAX_LENGTH) return text; return text.slice(0, TEXT_PREVIEW_MAX_LENGTH) + '...'; }; - // 输入态:只显示输入框和生成按钮 if (isInputStep) { return (
    @@ -40,13 +37,13 @@ export default function GeneratePanel() { >
    - {'生成中...'} + 生成中... ) : ( <> - {'生成二维码'} + 生成二维码 )} @@ -79,18 +76,16 @@ export default function GeneratePanel() { ); } - // 预览态:显示文本预览 + 二维码(紧凑布局,适配popup窗口) return (
    - {/* 文本预览区域(固定高度,单行显示) */}
    - {/* 二维码预览区域(自适应剩余空间) */} void; - /** 清除文件回调 */ onClearFile: () => void; - /** 文件预览 URL */ previewUrl: string; - /** 是否正在拖拽 */ dragging: boolean; - /** 拖拽状态变更回调 */ onDraggingChange: (dragging: boolean) => void; } @@ -27,26 +20,18 @@ const ImageUploader = ({ dragging, onDraggingChange, }: ImageUploaderProps) => { - const fileInputRef = useRef(null); - - const handleFileChange = useCallback( - (file: File) => { - onFileChange(file); - }, - [onFileChange], - ); - - const handleClearFile = useCallback(() => { + const handleClearFile = () => { if (previewUrl) { URL.revokeObjectURL(previewUrl); } onClearFile(); toast.success('图片已清除'); - }, [previewUrl, onClearFile]); + }; const handleInputChange = (e: React.ChangeEvent) => { - if (e.target.files && e.target.files.length > 0) { - handleFileChange(e.target.files[0]); + const file = e.target.files?.[0]; + if (file) { + onFileChange(file); } }; @@ -64,7 +49,7 @@ const ImageUploader = ({ onDraggingChange(false); const droppedFile = e.dataTransfer.files?.[0]; if (droppedFile) { - handleFileChange(droppedFile); + onFileChange(droppedFile); } }; @@ -82,7 +67,6 @@ const ImageUploader = ({ onDrop={handleDrop} >
    {selectedFile.name} - {'点击更换图片'} + 点击更换图片
    ) : ( <> @@ -122,10 +106,10 @@ const ImageUploader = ({ className="w-12 h-12 text-muted-foreground mx-auto mb-2" /> - {'点击、拖拽或粘贴上传二维码图片'} + 点击、拖拽或粘贴上传二维码图片 - {'支持 PNG、JPG、WEBP、Base64 格式'} + 支持 PNG、JPG、WEBP、Base64 格式 )} diff --git a/src/pages/QrCode/components/ParsePanel.tsx b/src/pages/QrCode/components/ParsePanel.tsx index 5c3c6b3..d1426f5 100644 --- a/src/pages/QrCode/components/ParsePanel.tsx +++ b/src/pages/QrCode/components/ParsePanel.tsx @@ -13,7 +13,6 @@ export default function ParsePanel() { const hasFile = parserState.selectedFile !== null; - // 全局粘贴事件监听 const handlePaste = useCallback( async (e: ClipboardEvent) => { const items = e.clipboardData?.items; @@ -56,7 +55,6 @@ export default function ParsePanel() { }; }, [handlePaste]); - // 未上传图片:只显示上传区域 if (!hasFile) { return (
    @@ -72,18 +70,16 @@ export default function ParsePanel() { ); } - // 已上传图片:显示图片预览 + 解析结果 return (
    - {/* 图片预览区域(小图预览) */}
    @@ -95,13 +91,12 @@ export default function ParsePanel() {

    {parserState.selectedFile?.name}

    {parserState.parsing && ( -

    {'解析中...'}

    +

    解析中...

    )}
    - {/* 解析结果区域 */}
    { - /** 二维码 Data URL */ qrCodeDataUrl: string; - /** 下载回调 */ onDownload: () => void; - /** 复制回调 */ onCopy: () => void; - /** 自定义占位文本,用于空状态提示 */ placeholderText?: string; } @@ -24,7 +19,6 @@ const QrCodePreview = ({ className, ...props }: QrCodePreviewProps) => { - // 空状态下的虚线骨架屏 if (!qrCodeDataUrl) { return (
    - {/* - 二维码容器适配: - 在暗黑模式下,纯黑白的二维码如果直接暴露在暗色背景下,会导致手机摄像头极难识别。 - 通过裹一层 bg-white 和 p-2,确保黑白对比度绝对安全,同时加入 shadow 增强卡片感。 - */}
    diff --git a/src/pages/QrCode/hooks/useQrCode.ts b/src/pages/QrCode/hooks/useQrCode.ts index 9c07a6d..4c8e199 100644 --- a/src/pages/QrCode/hooks/useQrCode.ts +++ b/src/pages/QrCode/hooks/useQrCode.ts @@ -12,32 +12,26 @@ function isUrl(text: string): boolean { const trimmed = text.trim(); if (!trimmed) return false; - // 检查是否以 http:// 或 https:// 开头 if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) { return true; } - // 检查是否为域名格式(包含.且不以特殊字符开头) const domainPattern = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z]{2,})+([/:].*)?$/; return domainPattern.test(trimmed); } -/** 检测URL是否为图片格式 */ function isImageUrl(url: string): boolean { const trimmedUrl = url.trim().toLowerCase(); - // 检查是否为 data:image 格式 if (trimmedUrl.startsWith('data:image/')) { return true; } - // 检查是否包含图片扩展名 const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg', '.ico']; if (imageExtensions.some((ext) => trimmedUrl.includes(ext))) { return true; } - // 检查URL路径中是否包含图片相关关键词 const imageKeywords = ['/image/', '/img/', '/photo/', '/pic/', '/upload/']; if (imageKeywords.some((keyword) => trimmedUrl.includes(keyword))) { return true; @@ -46,7 +40,6 @@ function isImageUrl(url: string): boolean { return false; } -/** 生成二维码的核心逻辑 */ function generateQrCodeDataUrl(text: string): string { const trimmedText = text.trim(); if (!trimmedText) return ''; @@ -241,7 +234,17 @@ export function useQrCode(): QrCodeContextValue { const qrCodeDataUrl = generateQrCodeDataUrl(payload); - if (!qrCodeDataUrl) { + if (qrCodeDataUrl) { + setGeneratorState((prev) => ({ + ...prev, + step: 'preview', + textToEncode: payload, + savedText: payload.trim(), + qrCodeDataUrl, + generating: false, + inputError: '', + })); + } else { setGeneratorState((prev) => ({ ...prev, step: 'input', @@ -328,7 +331,7 @@ export function useQrCode(): QrCodeContextValue { }); parseQrCode(file).catch((err) => { - console.error('Parser standalone task thread exploded:', err); + console.error('解析二维码失败:', err); }); }, [parseQrCode], diff --git a/src/pages/RightClickRestorer/constants.ts b/src/pages/RightClickRestorer/constants.ts index 5474777..165900b 100644 --- a/src/pages/RightClickRestorer/constants.ts +++ b/src/pages/RightClickRestorer/constants.ts @@ -6,10 +6,6 @@ export type RestorerStatus = 'unsupported' | 'locked' | 'unlocked'; export const CARD_CLASS = 'w-full p-4 rounded-xl border border-border bg-card text-card-foreground shadow-sm overflow-hidden space-y-3'; -export const LOADING_TEXT = '正在加载...'; - -export const DOMAIN_LABEL = '当前域名'; - interface StatusConfig { badgeVariant: 'default' | 'secondary' | 'destructive'; badgeClassName: string; @@ -50,7 +46,7 @@ export const STATUS_CONFIG: Record = { badgeClassName: 'gap-1 bg-green-600 hover:bg-green-700 shrink-0', badgeIcon: ShieldCheck, badgeLabel: '已解锁', - description: '点击下方按钮,为当前网站临时解锁右键菜单。刷新页面后需要重新解锁。', + description: '当前网站右键菜单已临时解锁。刷新页面后需要重新解锁。', buttonIcon: MousePointerClick, buttonLabel: '右键已解锁', buttonDisabled: true, diff --git a/src/pages/RightClickRestorer/index.tsx b/src/pages/RightClickRestorer/index.tsx index 97121fc..42b8143 100644 --- a/src/pages/RightClickRestorer/index.tsx +++ b/src/pages/RightClickRestorer/index.tsx @@ -2,7 +2,7 @@ import { Button } from '@/components/ui/button'; import { Label } from '@/components/ui/label'; import { Badge } from '@/components/ui/badge'; import { Loader2 } from 'lucide-react'; -import { CARD_CLASS, DOMAIN_LABEL, LOADING_TEXT, STATUS_CONFIG } from './constants'; +import { CARD_CLASS, STATUS_CONFIG } from './constants'; import { useRightClickRestorer } from './useRightClickRestorer'; export default function Index() { @@ -13,7 +13,7 @@ export default function Index() {
    - {LOADING_TEXT} + 正在加载...
    ); @@ -24,9 +24,9 @@ export default function Index() { const ButtonIcon = config.buttonIcon; return ( -
    +
    - +
    {domain || '—'} @@ -42,7 +42,7 @@ export default function Index() { className="w-full gap-2" disabled={config.buttonDisabled} variant={config.buttonVariant} - onClick={config.buttonDisabled ? undefined : () => void unlock()} + onClick={() => void unlock()} > {config.buttonLabel} diff --git a/src/pages/RightClickRestorer/useRightClickRestorer.ts b/src/pages/RightClickRestorer/useRightClickRestorer.ts index b209126..0cc99dd 100644 --- a/src/pages/RightClickRestorer/useRightClickRestorer.ts +++ b/src/pages/RightClickRestorer/useRightClickRestorer.ts @@ -3,11 +3,6 @@ import { MessageAction, sendMessageToContent } from '@/utils/messages'; import { isUnsupportedPageUrl } from '@/utils/restrictedUrls'; import type { RestorerStatus } from './constants'; -function deriveStatus(isUnsupported: boolean, isUnlocked: boolean): RestorerStatus { - if (isUnsupported) return 'unsupported'; - return isUnlocked ? 'unlocked' : 'locked'; -} - export interface UseRightClickRestorerReturn { domain: string; isLoading: boolean; @@ -16,10 +11,9 @@ export interface UseRightClickRestorerReturn { } export function useRightClickRestorer(): UseRightClickRestorerReturn { - const [domain, setDomain] = useState(''); - const [isLoading, setIsLoading] = useState(true); - const [isUnlocked, setIsUnlocked] = useState(false); - const [isUnsupported, setIsUnsupported] = useState(false); + const [domain, setDomain] = useState(''); + const [isLoading, setIsLoading] = useState(true); + const [status, setStatus] = useState('locked'); useEffect(() => { const load = async () => { @@ -30,13 +24,13 @@ export function useRightClickRestorer(): UseRightClickRestorerReturn { setDomain(url ? new URL(url).hostname : ''); if (isUnsupportedPageUrl(url)) { - setIsUnsupported(true); + setStatus('unsupported'); return; } const response = await sendMessageToContent(MessageAction.QUERY_RIGHT_CLICK_STATUS); if (response?.success) { - setIsUnlocked(response.restored); + setStatus(response.restored ? 'unlocked' : 'locked'); } } catch (err) { console.error('[RightClickRestorer] Failed to load state:', err); @@ -49,12 +43,12 @@ export function useRightClickRestorer(): UseRightClickRestorerReturn { }, []); async function unlock() { - if (isUnsupported) return; + if (status === 'unsupported') return; try { const response = await sendMessageToContent(MessageAction.RESTORE_RIGHT_CLICK); if (response?.success) { - setIsUnlocked(response.restored); + setStatus(response.restored ? 'unlocked' : 'locked'); } } catch (err) { console.error('[RightClickRestorer] Failed to unlock:', err); @@ -64,7 +58,7 @@ export function useRightClickRestorer(): UseRightClickRestorerReturn { return { domain, isLoading, - status: deriveStatus(isUnsupported, isUnlocked), + status, unlock, }; } diff --git a/src/pages/StorageCleaner/__tests__/index.test.tsx b/src/pages/StorageCleaner/__tests__/index.test.tsx index e73564d..bd348fc 100644 --- a/src/pages/StorageCleaner/__tests__/index.test.tsx +++ b/src/pages/StorageCleaner/__tests__/index.test.tsx @@ -5,7 +5,6 @@ import Index from '../index'; import { clearStorage, getCookieSize, getCurrentTab } from '@/utils/storageCleaner'; import { toast } from 'sonner'; -// Mock the chrome APIs vi.mock('@/utils/chromeStorage', () => ({ storageUtil: { get: vi.fn().mockResolvedValue(null), diff --git a/src/pages/StorageCleaner/components/AutoRefreshToggle.tsx b/src/pages/StorageCleaner/components/AutoRefreshToggle.tsx index 0926cd7..3b57b4f 100644 --- a/src/pages/StorageCleaner/components/AutoRefreshToggle.tsx +++ b/src/pages/StorageCleaner/components/AutoRefreshToggle.tsx @@ -1,18 +1,3 @@ -/** - * 自动刷新开关行 - * - * 用于 StorageCleaner 页面的操作区域内(非独立卡片), - * 控制清理后是否自动刷新页面。左侧为标签文本,右侧为 shadcn/ui Switch 开关。 - * - * @example - * ```tsx - * setReloadAfterClean(checked)} - * /> - * ``` - */ - import React from 'react'; import { cn } from '@/lib/utils'; import { Switch } from '@/components/ui/switch'; @@ -22,20 +7,10 @@ export interface AutoRefreshToggleProps extends Omit< React.HTMLAttributes, 'onChange' > { - /** 开关的当前状态(受控) */ reloadAfterClean: boolean; - /** 状态变化时的回调函数 */ onChange: (checked: boolean) => void; } -/** - * 自动刷新开关 - * - * @param reloadAfterClean - 当前开关状态 - * @param onChange - 状态变化回调,接收新的布尔值 - * @param className - 额外的 CSS 类名,用于覆盖或扩展样式 - * @param props - 透传给外层 div 的其他 HTML 属性 - */ export default function AutoRefreshToggle({ reloadAfterClean, onChange, @@ -54,7 +29,7 @@ export default function AutoRefreshToggle({ htmlFor="auto-refresh-switch" className="text-xs font-bold text-muted-foreground/90 cursor-pointer select-none tracking-wide uppercase" > - {'清理后自动刷新页面'} + 清理后自动刷新页面 diff --git a/src/pages/StorageCleaner/components/ErrorDisplay.tsx b/src/pages/StorageCleaner/components/ErrorDisplay.tsx index 5f3728d..2879711 100644 --- a/src/pages/StorageCleaner/components/ErrorDisplay.tsx +++ b/src/pages/StorageCleaner/components/ErrorDisplay.tsx @@ -24,7 +24,7 @@ export default function ErrorDisplay({ error, className, ...props }: ErrorDispla

    - {'存储清理功能仅适用于标准网页'} + 存储清理功能仅适用于标准网页

    diff --git a/src/pages/StorageCleaner/components/OptionItem.tsx b/src/pages/StorageCleaner/components/OptionItem.tsx index 745c506..7eefb08 100644 --- a/src/pages/StorageCleaner/components/OptionItem.tsx +++ b/src/pages/StorageCleaner/components/OptionItem.tsx @@ -2,11 +2,11 @@ import React from 'react'; import { formatBytes } from '@/utils/format'; import { cn } from '@/lib/utils'; import type { StorageSizeInfo } from '../useStorageCleaner'; -import { OPTION_LABELS } from '../constants'; +import { CLEAN_OPTION_KEYS, OPTION_LABELS } from '../constants'; import { Checkbox } from '@/components/ui/checkbox'; interface OptionItemProps extends React.HTMLAttributes { - labelKey: string; + labelKey: (typeof CLEAN_OPTION_KEYS)[number]; checked: boolean; sizeInfo?: StorageSizeInfo; onChange: () => void; @@ -22,8 +22,7 @@ export default function OptionItem({ }: OptionItemProps) { const sizeValue = sizeInfo?.value; const isCount = sizeInfo?.displayType === 'count'; - const label = - labelKey in OPTION_LABELS ? OPTION_LABELS[labelKey as keyof typeof OPTION_LABELS] : labelKey; + const label = OPTION_LABELS[labelKey]; return (
    ) : ( - {'无数据'} + 无数据 )}
    diff --git a/src/pages/StorageCleaner/components/StorageCleanerConfirm.tsx b/src/pages/StorageCleaner/components/StorageCleanerConfirm.tsx index e619e80..116bcc3 100644 --- a/src/pages/StorageCleaner/components/StorageCleanerConfirm.tsx +++ b/src/pages/StorageCleaner/components/StorageCleanerConfirm.tsx @@ -10,7 +10,7 @@ import { import { Badge } from '@/components/ui/badge'; import { AlertTriangle } from 'lucide-react'; import type { StorageCleanerOptions } from '@/types/storage'; -import { OPTION_LABELS } from '../constants'; +import { OPTION_LABELS, CLEAN_OPTION_KEYS } from '../constants'; import { cn } from '@/lib/utils'; export interface StorageCleanerConfirmProps { @@ -26,11 +26,9 @@ export function StorageCleanerConfirm({ onConfirm, options, }: StorageCleanerConfirmProps) { - const selectedOptions = Object.entries(options) - .filter(([_, value]) => value) - .map(([key]) => - key in OPTION_LABELS ? OPTION_LABELS[key as keyof typeof OPTION_LABELS] : key, - ); + const selectedOptions = CLEAN_OPTION_KEYS.filter((key) => options[key]).map( + (key) => OPTION_LABELS[key], + ); return ( !isOpen && onClose()}> @@ -42,14 +40,14 @@ export function StorageCleanerConfirm({ {/* 头部标题区域 */} - {'确认清理数据?'} + 确认清理数据? {/* 内容主体:限制最大宽度,防止内部元素在大分辨率下被横向拉得太松散 */}
    - {'您将永久删除当前页面的以下选定存储项。'} + 您将永久删除当前页面的以下选定存储项。 {/* 待清理项目徽章群 */} @@ -69,7 +67,7 @@ export function StorageCleanerConfirm({
    - {'此操作不可撤销'} + 此操作不可撤销
    @@ -81,7 +79,7 @@ export function StorageCleanerConfirm({ onClick={onConfirm} className="w-full text-xs font-bold shadow-sm h-9" > - {'确认清理'} + 确认清理 diff --git a/src/pages/StorageCleaner/components/StorageOptionsGrid.tsx b/src/pages/StorageCleaner/components/StorageOptionsGrid.tsx index b8c23e5..b23323f 100644 --- a/src/pages/StorageCleaner/components/StorageOptionsGrid.tsx +++ b/src/pages/StorageCleaner/components/StorageOptionsGrid.tsx @@ -51,7 +51,7 @@ export default function StorageOptionsGrid({ className="border-t border-border flex justify-between items-center pl-3.5 pr-7 py-2.5 bg-muted/20 hover:bg-muted/40 cursor-pointer select-none transition-colors" > - {'正在读取数据...'} + 正在读取数据...
    ); @@ -73,7 +73,7 @@ export default function Index() { {loading ? ( <> - {'正在清理...'} + 正在清理... ) : ( '立即清理' diff --git a/src/pages/StorageCleaner/useStorageCleaner.ts b/src/pages/StorageCleaner/useStorageCleaner.ts index a3588a3..5dbe66e 100644 --- a/src/pages/StorageCleaner/useStorageCleaner.ts +++ b/src/pages/StorageCleaner/useStorageCleaner.ts @@ -117,7 +117,6 @@ export function useStorageCleaner(): UseStorageCleanerReturn { }; }, []); - // 核心数据拉取链条 const loadInfo = useCallback(async () => { const currentRequestId = ++requestIdRef.current; setIsRefreshingSizes(true); @@ -189,7 +188,6 @@ export function useStorageCleaner(): UseStorageCleanerReturn { }, 300); }, []); - // 监听浏览器标签行为 useEffect(() => { loadInfoRef.current().catch(console.error); @@ -216,15 +214,12 @@ export function useStorageCleaner(): UseStorageCleanerReturn { if (storageTimerRef.current) clearTimeout(storageTimerRef.current); storageTimerRef.current = setTimeout(async () => { - try { - await storageUtil.set('storageCleaner/preferences', { + await storageUtil + .set('storageCleaner/preferences', { reloadAfterClean, selectedTypes: options, - }); - } catch (err) { - console.error('Failed to save storage cleaner preferences:', err); - toast.warning('偏好保存失败,本次设置可能不会保留'); - } + }) + .catch(console.error); }, 500); }, [options, reloadAfterClean, isInitializing]); @@ -251,7 +246,7 @@ export function useStorageCleaner(): UseStorageCleanerReturn { if (loadingRef.current) return; const tab = await getCurrentTab(); - if (!tab || tab.id === undefined || !tab.url) { + if (!tab || !tab.id || !tab.url) { toast.warning('无法获取当前标签页'); return; } @@ -286,7 +281,6 @@ export function useStorageCleaner(): UseStorageCleanerReturn { toast.error(`清理失败: ${String(err)}`); } finally { setLoading(false); - setShowConfirm(false); } }, [options, reloadAfterClean, loadInfo]); diff --git a/src/pages/TestDataGenerator/components/DataPreview.tsx b/src/pages/TestDataGenerator/components/DataPreview.tsx index 5c09da4..151c520 100644 --- a/src/pages/TestDataGenerator/components/DataPreview.tsx +++ b/src/pages/TestDataGenerator/components/DataPreview.tsx @@ -75,7 +75,7 @@ export default function DataPreview({ fields }: DataPreviewProps) { return (
    -

    {'配置字段后点击「生成数据」按钮'}

    +

    配置字段后点击「生成数据」按钮

    ); } @@ -87,10 +87,10 @@ export default function DataPreview({ fields }: DataPreviewProps) {
    - {'示例数据'} + 示例数据
    - {Object.keys(sampleData).length} {'字段'} + {Object.keys(sampleData).length} 字段
    diff --git a/src/pages/TestDataGenerator/components/ExportPanel.tsx b/src/pages/TestDataGenerator/components/ExportPanel.tsx index 29f2591..483e6c9 100644 --- a/src/pages/TestDataGenerator/components/ExportPanel.tsx +++ b/src/pages/TestDataGenerator/components/ExportPanel.tsx @@ -50,27 +50,27 @@ export default function ExportPanel({ result }: ExportPanelProps) { return (
    -

    {'导出数据'}

    +

    导出数据

    diff --git a/src/pages/TestDataGenerator/components/FieldEditor.tsx b/src/pages/TestDataGenerator/components/FieldEditor.tsx index a72af42..d23fab9 100644 --- a/src/pages/TestDataGenerator/components/FieldEditor.tsx +++ b/src/pages/TestDataGenerator/components/FieldEditor.tsx @@ -102,13 +102,13 @@ export default function FieldEditor({ field, onChange, allFieldNames = [] }: Fie
    - + {field.name.length}/20
    handleNameChange(e.target.value)} - placeholder={'请输入字段名称'} + placeholder="请输入字段名称" maxLength={20} className={`h-9 ${nameError ? 'border-destructive' : ''}`} /> @@ -117,7 +117,7 @@ export default function FieldEditor({ field, onChange, allFieldNames = [] }: Fie
    - + {(field.description || '').length}/50 @@ -125,7 +125,7 @@ export default function FieldEditor({ field, onChange, allFieldNames = [] }: Fie handleDescriptionChange(e.target.value)} - placeholder={'可选,添加字段说明'} + placeholder="可选,添加字段说明" maxLength={50} className="h-9" /> @@ -134,14 +134,14 @@ export default function FieldEditor({ field, onChange, allFieldNames = [] }: Fie
    - +
    {!field.required && (
    - {'空值率'} + 空值率 {field.nullRate}% @@ -180,18 +180,18 @@ export default function FieldEditor({ field, onChange, allFieldNames = [] }: Fie
    - +
    - +
    {generator && (
    - + - {/* 第一行:字段名 + 标签 */}
    {field.name} @@ -41,11 +40,10 @@ export default function FieldItem({ field, onClick }: FieldItemProps) { )} {field.unique && ( - {'唯一'} + 唯一 )}
    - {/* 第二行:描述 */} {field.description && (

    {field.description} diff --git a/src/pages/TestDataGenerator/components/FieldList.tsx b/src/pages/TestDataGenerator/components/FieldList.tsx index 3b5fa65..1c4870a 100644 --- a/src/pages/TestDataGenerator/components/FieldList.tsx +++ b/src/pages/TestDataGenerator/components/FieldList.tsx @@ -86,7 +86,6 @@ function SortableFieldItem({ }`} >

    - {/* 拖拽手柄 */} ) : ( @@ -302,7 +301,7 @@ export default function FieldList({ className="h-8 gap-1.5 px-2.5" > - {'保存规则'} + 保存规则 )}
    @@ -322,8 +321,8 @@ export default function FieldList({ {fields.length === 0 ? (
    -

    {'暂无字段'}

    -

    {'点击上方按钮添加第一个字段'}

    +

    暂无字段

    +

    点击上方按钮添加第一个字段

    ) : ( - {/* 保存规则对话框 */} setRuleName(e.target.value)} - placeholder={'规则名称'} + placeholder="规则名称" className="h-9" /> setRuleDescription(e.target.value)} - placeholder={'规则描述(可选)'} + placeholder="规则描述(可选)" className="h-9" />
    - {/* 覆盖确认对话框 */}
    -

    {'已存在同名规则,是否覆盖保存?'}

    +

    已存在同名规则,是否覆盖保存?

    diff --git a/src/pages/TestDataGenerator/components/GenerateButton.tsx b/src/pages/TestDataGenerator/components/GenerateButton.tsx index 14b0e06..25802fa 100644 --- a/src/pages/TestDataGenerator/components/GenerateButton.tsx +++ b/src/pages/TestDataGenerator/components/GenerateButton.tsx @@ -1,6 +1,5 @@ /** * 生成按钮组件 - * 显示生成按钮、加载状态和进度条 */ import { Play, Square } from 'lucide-react'; @@ -28,14 +27,13 @@ export default function GenerateButton({ <> - {/* 进度条 */} {progress && (
    - {`已生成 ${progress.generated} / ${progress.total} 条`} + {`已生成 ${progress.generated.toLocaleString()} / ${progress.total.toLocaleString()} 条`} {progress.progress}%
    @@ -46,7 +44,7 @@ export default function GenerateButton({
    {progress.estimatedTimeLeft !== undefined && (

    - {`预计剩余 ${progress.estimatedTimeLeft} 秒`} + {`预计剩余 ${Math.ceil(progress.estimatedTimeLeft / 1000)} 秒`}

    )}
    @@ -55,7 +53,7 @@ export default function GenerateButton({ ) : ( )}
    diff --git a/src/pages/TestDataGenerator/components/GenerateOptions.tsx b/src/pages/TestDataGenerator/components/GenerateOptions.tsx index fdd62dd..105e48a 100644 --- a/src/pages/TestDataGenerator/components/GenerateOptions.tsx +++ b/src/pages/TestDataGenerator/components/GenerateOptions.tsx @@ -28,9 +28,8 @@ export default function GenerateOptions({ }: GenerateOptionsProps) { return (
    - {/* 生成数量 */}
    - +
    {COUNT_PRESETS.map((preset) => (
    - {/* 数据格式 */}
    - +
    {FORMAT_OPTIONS.map((option) => (
    - {/* 工具栏 */}
    - {/* 搜索框 */}
    setSearchQuery(e.target.value.slice(0, 20))} - placeholder={'搜索规则...'} + placeholder="搜索规则..." className="pl-9 pr-24 h-9" maxLength={20} /> @@ -234,7 +231,6 @@ export default function RuleManager({ onLoad, onEdit, onRulesChanged }: RuleMana
    - {/* 规则列表 */}
    {filteredRules.length === 0 ? (
    @@ -252,9 +248,7 @@ export default function RuleManager({ onLoad, onEdit, onRulesChanged }: RuleMana
    {rule.name} - - {rule.fields.length} {'字段'} - + {rule.fields.length} 字段
    {rule.description && (

    @@ -276,7 +270,7 @@ export default function RuleManager({ onLoad, onEdit, onRulesChanged }: RuleMana size="icon" className="h-7 w-7" onClick={() => handleLoad(rule)} - title={'加载'} + title="加载" > @@ -285,7 +279,7 @@ export default function RuleManager({ onLoad, onEdit, onRulesChanged }: RuleMana size="icon" className="h-7 w-7" onClick={() => handleEdit(rule)} - title={'编辑'} + title="编辑" > @@ -294,7 +288,7 @@ export default function RuleManager({ onLoad, onEdit, onRulesChanged }: RuleMana size="icon" className="h-7 w-7" onClick={() => handleDuplicate(rule.id)} - title={'复制'} + title="复制" > @@ -303,7 +297,7 @@ export default function RuleManager({ onLoad, onEdit, onRulesChanged }: RuleMana size="icon" className="h-7 w-7 text-destructive hover:text-destructive" onClick={() => setRuleToDelete(rule)} - title={'删除'} + title="删除" > diff --git a/src/pages/TestDataGenerator/index.tsx b/src/pages/TestDataGenerator/index.tsx index fd0700e..782edc7 100644 --- a/src/pages/TestDataGenerator/index.tsx +++ b/src/pages/TestDataGenerator/index.tsx @@ -1,21 +1,10 @@ -/** - * 测试数据生成器主页面 - */ - import { useState, useCallback, useRef, useEffect } from 'react'; import { Settings, Database, Tag } from 'lucide-react'; import { cn } from '@/lib/utils'; -import { useGenerator } from './hooks/useGenerator'; -import type { FieldConfig, GenerateResult, DataRule } from '@/types/testDataGenerator'; import { Dialog, DialogContent } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { toast } from 'sonner'; - -// 生成唯一 ID 的辅助函数 -function generateId(): string { - return `field_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`; -} - +import { useGenerator } from './hooks/useGenerator'; import FieldList, { MAX_FIELDS } from './components/FieldList'; import FieldEditor from './components/FieldEditor'; import GenerateOptions from './components/GenerateOptions'; @@ -24,39 +13,38 @@ import DataPreview from './components/DataPreview'; import ResultPanel from './components/ResultPanel'; import ExportPanel from './components/ExportPanel'; import RuleManager from './components/RuleManager'; +import type { FieldConfig, GenerateResult, DataRule } from '@/types/testDataGenerator'; + +function generateId(): string { + return `field_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`; +} type TabType = 'fields' | 'rules'; export default function TestDataGeneratorPage() { const { isGenerating, progress, result, error, generate, cancel, clearResult } = useGenerator(); - // 字段配置 const [fields, setFields] = useState([]); const [selectedIndex, setSelectedIndex] = useState(null); const [isEditorOpen, setIsEditorOpen] = useState(false); const [editingRule, setEditingRule] = useState(null); - // 生成选项 const [count, setCount] = useState(100); const [format, setFormat] = useState<'json' | 'csv'>('json'); - // 当前标签页 const [activeTab, setActiveTab] = useState('fields'); - // 生成完成时显示 toast 提示(避免重复触发) const lastToastResultRef = useRef(null); - // 生成完成时显示 toast 提示(避免重复触发) useEffect(() => { if (result?.success && result.stats && result !== lastToastResultRef.current) { lastToastResultRef.current = result; toast.success('生成完成', { - description: `${result.stats.total} ${'条数据'}`, + description: `${result.stats.total} 条数据`, }); } }, [result]); - // 添加新字段 const handleAddField = useCallback(() => { setFields((prev) => { if (prev.length >= MAX_FIELDS) return prev; @@ -74,7 +62,6 @@ export default function TestDataGeneratorPage() { }); }, []); - // 更新字段 const handleUpdateField = useCallback((index: number, field: FieldConfig) => { setFields((prev) => { const newFields = [...prev]; @@ -83,7 +70,6 @@ export default function TestDataGeneratorPage() { }); }, []); - // 删除字段 const handleRemoveField = useCallback((index: number) => { setFields((prev) => prev.filter((_, i) => i !== index)); setSelectedIndex((prev) => { @@ -93,7 +79,6 @@ export default function TestDataGeneratorPage() { }); }, []); - // 拖拽排序 const handleReorder = useCallback((oldIndex: number, newIndex: number) => { setFields((prev) => { const newFields = [...prev]; @@ -111,7 +96,6 @@ export default function TestDataGeneratorPage() { }); }, []); - // 加载规则 const handleLoadRule = useCallback( (loadedFields: FieldConfig[]) => { setFields(loadedFields); @@ -122,7 +106,6 @@ export default function TestDataGeneratorPage() { [clearResult], ); - // 编辑规则 const handleEditRule = useCallback( (rule: DataRule) => { setFields(rule.fields); @@ -135,21 +118,17 @@ export default function TestDataGeneratorPage() { [clearResult], ); - // 保存规则成功后清除编辑状态 const handleRuleSaved = useCallback(() => { setEditingRule(null); }, []); - // 开始生成 const handleGenerate = useCallback(() => { if (fields.length === 0) return; generate(fields, count, format === 'csv'); }, [fields, count, format, generate]); - // 获取选中的字段(-1 或 null 表示未选中) const selectedField = selectedIndex !== null && selectedIndex >= 0 ? fields[selectedIndex] : null; - // 打开字段编辑器 const handleOpenEditor = useCallback((index: number) => { setSelectedIndex(index); setIsEditorOpen(true); @@ -158,11 +137,8 @@ export default function TestDataGeneratorPage() { return (

    - {/* 主要内容区域 - 左右分栏 */}
    - {/* 左侧面板 - 字段配置 */}
    - {/* 标签页切换 */}
    - {/* 字段配置标签页 */} {activeTab === 'fields' && (
    )} - {/* 规则管理标签页 */} {activeTab === 'rules' && (
    )} - {/* 生成选项 */}
    - {/* 生成按钮 */}
    - {/* 右侧面板 - 数据预览和结果 */}
    - {/* 结果状态 */} - {/* 仅在失败或有警告时显示结果面板 */} {((result && !result.success) || (result?.warnings && result.warnings.length > 0) || error) && ( @@ -253,18 +222,16 @@ export default function TestDataGeneratorPage() {
    )} - {/* 数据预览 */}

    - {'数据预览'} + 数据预览

    - {/* 导出面板 */} {result?.data && result.data.length > 0 && (
    @@ -274,7 +241,6 @@ export default function TestDataGeneratorPage() {
    - {/* 字段编辑弹窗 */}
    diff --git a/src/pages/TextStatistics/components/StatCard.tsx b/src/pages/TextStatistics/components/StatCard.tsx deleted file mode 100644 index 8b132c1..0000000 --- a/src/pages/TextStatistics/components/StatCard.tsx +++ /dev/null @@ -1,17 +0,0 @@ -interface StatCardProps { - label: string; - value: React.ReactNode; -} - -export default function StatCard({ label, value }: StatCardProps) { - return ( -
    - - {label} - - - {value} - -
    - ); -} diff --git a/src/pages/TextStatistics/index.tsx b/src/pages/TextStatistics/index.tsx index bfd5696..b1bd735 100644 --- a/src/pages/TextStatistics/index.tsx +++ b/src/pages/TextStatistics/index.tsx @@ -1,17 +1,24 @@ import TextInputArea from '@/components/TextInputArea'; import { formatBytes } from '@/utils/format'; -import StatCard from './components/StatCard'; +import { cn } from '@/lib/utils'; import { useTextStatistics } from './useTextStatistics'; export default function Index() { const { text, stats, setText } = useTextStatistics(); + const statItems = [ + { label: '字符数', value: stats.characters }, + { label: '单词数', value: stats.words }, + { label: '行数', value: stats.lines }, + { label: '字节大小', value: formatBytes(stats.bytes) }, + ]; + return (
    - - - - + {statItems.map((item) => ( +
    + + {item.label} + + + {item.value} + +
    + ))}
    ); diff --git a/src/pages/TextStatistics/useTextStatistics.ts b/src/pages/TextStatistics/useTextStatistics.ts index 4fd68cb..2ca8339 100644 --- a/src/pages/TextStatistics/useTextStatistics.ts +++ b/src/pages/TextStatistics/useTextStatistics.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useState } from 'react'; import { getTextStats, type TextStats } from '@/utils/textStatistics'; import { useContextMenuData } from '@/utils/useContextMenuData'; @@ -10,12 +10,6 @@ export interface UseTextStatisticsReturn { export function useTextStatistics(): UseTextStatisticsReturn { const [text, setText] = useState(''); - const [debouncedText, setDebouncedText] = useState(''); - - useEffect(() => { - const handle = setTimeout(() => setDebouncedText(text), 200); - return () => clearTimeout(handle); - }, [text]); const handleContextMenuData = useCallback((payload: string) => { setText(payload); @@ -23,7 +17,7 @@ export function useTextStatistics(): UseTextStatisticsReturn { useContextMenuData({ featureKey: 'textStatistics', onData: handleContextMenuData }); - const stats = useMemo(() => getTextStats(debouncedText), [debouncedText]); + const stats = getTextStats(text); return { text, stats, setText }; } diff --git a/src/pages/Timestamp/__tests__/index.test.tsx b/src/pages/Timestamp/__tests__/index.test.tsx index 689b495..7a35303 100644 --- a/src/pages/Timestamp/__tests__/index.test.tsx +++ b/src/pages/Timestamp/__tests__/index.test.tsx @@ -5,7 +5,6 @@ import Index from '../index'; describe('Timestamp 页面', () => { it('应该渲染模式切换按钮', () => { render(); - // 基本渲染测试:页面含多个模式切换按钮,应至少渲染一个 expect(screen.getAllByRole('button').length).toBeGreaterThan(0); }); }); diff --git a/src/pages/Timestamp/components/ConverterForm.tsx b/src/pages/Timestamp/components/ConverterForm.tsx index 0653ddd..715574a 100644 --- a/src/pages/Timestamp/components/ConverterForm.tsx +++ b/src/pages/Timestamp/components/ConverterForm.tsx @@ -8,9 +8,15 @@ import { SelectValue, } from '@/components/ui/select'; import { cn } from '@/lib/utils'; -import { MODE_OPTIONS, UNIT_OPTIONS, ZONES, type ZoneType } from '../constants'; +import { + CARD_CLASS, + INPUT_PLACEHOLDERS, + MODE_OPTIONS, + UNIT_OPTIONS, + ZONES, + type ZoneType, +} from '../constants'; import type { UseTimestampConverterReturn } from '../useTimestampConverter'; -import ToolCard from './ToolCard'; type ConverterFormProps = Omit; @@ -20,20 +26,19 @@ export default function ConverterForm({ unit, zone, error, - inputPlaceholder, setMode, setInput, setUnit, setZone, }: ConverterFormProps) { return ( - +
    setInput(e.target.value)} className={cn( @@ -71,6 +76,6 @@ export default function ConverterForm({
    - +
    ); } diff --git a/src/pages/Timestamp/components/LiveClock.tsx b/src/pages/Timestamp/components/LiveClock.tsx index 2207a31..40748b4 100644 --- a/src/pages/Timestamp/components/LiveClock.tsx +++ b/src/pages/Timestamp/components/LiveClock.tsx @@ -34,7 +34,7 @@ export default function LiveClock({ unit, onUseNow, className, ...props }: LiveC {...props} > - {'当前时间戳'} + 当前时间戳 @@ -47,7 +47,7 @@ export default function LiveClock({ unit, onUseNow, className, ...props }: LiveC onUseNow(rawTime); toast.success('已使用当前时间戳'); }} - title={'填充到下方'} + title="填充到下方" variant="ghost" size="icon" className="h-7 w-7 text-muted-foreground hover:text-foreground" @@ -55,7 +55,7 @@ export default function LiveClock({ unit, onUseNow, className, ...props }: LiveC - +
    ); } diff --git a/src/pages/Timestamp/components/ResultView.tsx b/src/pages/Timestamp/components/ResultView.tsx index 06d70ba..464601a 100644 --- a/src/pages/Timestamp/components/ResultView.tsx +++ b/src/pages/Timestamp/components/ResultView.tsx @@ -14,15 +14,16 @@ export default function ResultView({ className, ...props }: ResultViewProps) { + if (!result && !showEmptyPlaceholder) return null; + if (!result) { - if (!showEmptyPlaceholder) return null; return ( - {'请输入并点击转换'} + 请输入并点击转换 ); } @@ -30,7 +31,7 @@ export default function ResultView({ return (
    - {'转换结果'} + 转换结果
    @@ -39,7 +40,7 @@ export default function ResultView({ { - children: React.ReactNode; -} - -export default function ToolCard({ children, className, ...props }: ToolCardProps) { - return ( -
    - {children} -
    - ); -} diff --git a/src/pages/Timestamp/index.tsx b/src/pages/Timestamp/index.tsx index 31f699d..4bf7d78 100644 --- a/src/pages/Timestamp/index.tsx +++ b/src/pages/Timestamp/index.tsx @@ -1,7 +1,8 @@ +import { cn } from '@/lib/utils'; import LiveClock from './components/LiveClock'; import ConverterForm from './components/ConverterForm'; import ResultView from './components/ResultView'; -import ToolCard from './components/ToolCard'; +import { CARD_CLASS } from './constants'; import { useTimestampConverter } from './useTimestampConverter'; export default function Index() { @@ -14,9 +15,9 @@ export default function Index() {
    - +
    - +
    ); diff --git a/src/pages/Timestamp/useTimestampConverter.ts b/src/pages/Timestamp/useTimestampConverter.ts index b8bda29..400258d 100644 --- a/src/pages/Timestamp/useTimestampConverter.ts +++ b/src/pages/Timestamp/useTimestampConverter.ts @@ -1,7 +1,7 @@ import { useMemo, useState } from 'react'; import dayjs from '@/utils/dayjs'; import type { UnitType, ZoneType, ModeType } from './constants'; -import { DATE_FORMAT, INPUT_PLACEHOLDERS, msToUnit, dayjsFromTimestamp } from './constants'; +import { DATE_FORMAT, msToUnit, dayjsFromTimestamp } from './constants'; import { useContextMenuData } from '@/utils/useContextMenuData'; export interface UseTimestampConverterReturn { @@ -11,7 +11,6 @@ export interface UseTimestampConverterReturn { zone: ZoneType; result: string; error: string; - inputPlaceholder: string; setMode: (mode: ModeType) => void; setInput: (value: string) => void; @@ -101,7 +100,6 @@ export function useTimestampConverter(): UseTimestampConverterReturn { zone, result, error, - inputPlaceholder: INPUT_PLACEHOLDERS[mode], setMode: handleSetMode, setInput, setUnit, diff --git a/src/providers/RouterProvider.tsx b/src/providers/RouterProvider.tsx index d72bf84..8790f09 100644 --- a/src/providers/RouterProvider.tsx +++ b/src/providers/RouterProvider.tsx @@ -108,9 +108,6 @@ export function RouterProvider({ const hasUserNavigatedRef = useRef(false); const canPersistRef = useRef(false); - /** - * 从异步存储中安全溯源初始数据 - */ const loadInitialData = useCallback(async () => { try { const savedRoute = await storageUtil.get(syncKey, defaultRoute); @@ -149,7 +146,6 @@ export function RouterProvider({ .then(() => { if (cancelled) return; - // Check URL params for context menu data if (typeof window !== 'undefined') { const params = new URLSearchParams(window.location.search); const feature = params.get('feature') as PageType | null; @@ -169,7 +165,6 @@ export function RouterProvider({ } } - // Check storage for pending context menu data storageUtil .get('contextMenu/pendingData') .then((pendingData) => { @@ -192,7 +187,7 @@ export function RouterProvider({ useEffect(() => { if (isLoaded && canPersistRef.current && syncRoute) { - void storageUtil.set(syncKey, currentPage as PageType).catch(console.error); + void storageUtil.set(syncKey, currentPage).catch(console.error); try { localStorage.setItem(`snapshot/${syncKey}`, JSON.stringify(currentPage)); } catch (err) { @@ -324,7 +319,7 @@ export function RouterProvider({ export function useRouter() { const context = useContext(RouterContext); if (!context) { - throw new Error('useRouter must be used within a valid RouterProvider context wrapper'); + throw new Error('useRouter must be used within RouterProvider'); } return context; } diff --git a/src/providers/ThemeModeProvider.tsx b/src/providers/ThemeModeProvider.tsx index f41921a..4ee6d32 100644 --- a/src/providers/ThemeModeProvider.tsx +++ b/src/providers/ThemeModeProvider.tsx @@ -59,7 +59,7 @@ export function ThemeModeProvider({ children }: ThemeModeProviderProps) { applyResolvedThemeClass(resolveThemeMode(next)); void storageUtil.set(THEME_MODE_STORAGE_KEY, next).catch((err) => { - console.error('[Theme Storage Error] Failed to persistent theme state:', err); + console.error('[Theme Storage Error] Failed to persist theme mode:', err); }); persistThemeModeSnapshot(next); @@ -82,7 +82,7 @@ export function ThemeModeProvider({ children }: ThemeModeProviderProps) { applyResolvedThemeClass(resolveThemeMode(saved)); }) .catch((err) => { - console.error('[Theme Restore Thread Failed]', err); + console.error('[Theme Restore Error]', err); }); return () => { @@ -135,9 +135,7 @@ export function ThemeModeProvider({ children }: ThemeModeProviderProps) { export function useThemeMode(): ThemeModeContextType { const ctx = useContext(ThemeModeContext); if (!ctx) { - throw new Error( - 'useThemeMode must be used within a valid ThemeModeProvider sandboxed container', - ); + throw new Error('useThemeMode must be used within ThemeModeProvider'); } return ctx; } diff --git a/src/utils/diffEngine.ts b/src/utils/diffEngine.ts index 0736a8c..bc63c15 100644 --- a/src/utils/diffEngine.ts +++ b/src/utils/diffEngine.ts @@ -1,4 +1,4 @@ -import type { DiffNode, DiffResult, DiffType } from '@/pages/JsonTools/types'; +import type { DiffNode, DiffResult } from '@/pages/JsonTools/types'; const ROOT_PATH = '$'; const SENTINEL = Symbol('missing'); @@ -37,7 +37,6 @@ const diffNode = ( path: string, diffPaths: string[], ): DiffNode => { - // 分支 1:节点增加行为拦截 (叶子节点状态) if (left === SENTINEL && right !== SENTINEL) { diffPaths.push(path); return { @@ -51,7 +50,6 @@ const diffNode = ( }; } - // 分支 2:节点删除行为拦截 (叶子节点状态) if (right === SENTINEL && left !== SENTINEL) { diffPaths.push(path); return { @@ -181,5 +179,3 @@ export const diffJson = (left: unknown, right: unknown): DiffResult => { diffCount: diffPaths.length, }; }; - -export type { DiffNode, DiffResult, DiffType }; diff --git a/src/utils/indexedDbCleaner.ts b/src/utils/indexedDbCleaner.ts index f101e43..07a5b63 100644 --- a/src/utils/indexedDbCleaner.ts +++ b/src/utils/indexedDbCleaner.ts @@ -70,12 +70,12 @@ export async function clearAllIndexedDBs( const deleteReq = indexedDB.deleteDatabase(dbName); const timeoutId = setTimeout(() => { - console.warn('IndexedDB delete timeout:', dbName); + console.warn(`IndexedDB 删除超时: ${dbName}`); settle('timeout'); }, timeoutMs); deleteReq.onblocked = () => { - console.warn('IndexedDB delete blocked:', dbName); + console.warn(`IndexedDB 删除被阻塞: ${dbName}`); settle('blocked'); }; deleteReq.onsuccess = () => settle('deleted'); @@ -137,7 +137,7 @@ export async function clearAllIndexedDBs( const transaction = db.transaction(storeNames, 'readwrite'); // 必须在 clear 请求完成前注册 oncomplete,否则事务可能已结束导致永久挂起 const transactionDone = waitForTransaction(transaction, clearStoreTimeoutMs); - void transactionDone.catch(() => undefined); + void transactionDone.catch(() => {}); const errors = ( await Promise.all( storeNames.map((storeName) => clearStore(transaction.objectStore(storeName), storeName)), @@ -145,23 +145,11 @@ export async function clearAllIndexedDBs( ).filter((error): error is string => Boolean(error)); if (errors.length > 0) { - try { - transaction.abort(); - } catch { - // ignore abort failures on already-finished transactions - } + transaction.abort(); return { success: false, errors }; } - try { - await transactionDone; - } catch { - return { - success: false, - errors: [`清空 IndexedDB 失败(${dbName}),请刷新后重试`], - }; - } - + await transactionDone; return { success: true, errors: [] }; } catch { return { diff --git a/src/utils/restrictedUrls.ts b/src/utils/restrictedUrls.ts index 898f83d..af447f7 100644 --- a/src/utils/restrictedUrls.ts +++ b/src/utils/restrictedUrls.ts @@ -23,7 +23,7 @@ export function isUnsupportedPageUrl(url: string | undefined): boolean { if (!url) return true; const protocol = getUrlProtocol(url); if (!protocol) return true; - return (RESTRICTED_PROTOCOLS as readonly string[]).includes(protocol); + return RESTRICTED_PROTOCOLS.some((p) => p === protocol); } /** 用于 storage cleaner tab 检测(前缀匹配,兼容无 protocol 的场景) */ diff --git a/src/utils/storageCleaner.ts b/src/utils/storageCleaner.ts index 0a23b65..4da5a6f 100644 --- a/src/utils/storageCleaner.ts +++ b/src/utils/storageCleaner.ts @@ -9,8 +9,6 @@ import { } from '@/utils/indexedDbCleaner'; import { browser } from 'wxt/browser'; -type CleanScriptResult = IndexedDBCleanResult; - export async function getCurrentTab() { // For popup pages, we need to get the active tab from the browser window that triggered the popup. // We should ONLY care about the currently active tab in the last focused window. @@ -220,7 +218,7 @@ export async function clearCookies(url: string): Promise { */ async function runCleanScript( tabId: number, - func: (...args: A) => CleanScriptResult | Promise, + func: (...args: A) => IndexedDBCleanResult | Promise, errorLabel: string, args?: A, ): Promise { diff --git a/src/utils/syncSnapshot.ts b/src/utils/syncSnapshot.ts index 62ce828..08af5c4 100644 --- a/src/utils/syncSnapshot.ts +++ b/src/utils/syncSnapshot.ts @@ -15,7 +15,7 @@ export function getSyncSnapshot( } return (parsed as T) ?? defaultValue; } catch (error) { - console.error(`[SyncSnapshot] Failed to read snapshot/${key}:`, error); + console.error(`读取快照失败 (${key}):`, error); return defaultValue; } } diff --git a/src/utils/themeSnapshot.ts b/src/utils/themeSnapshot.ts index 2ce539a..596b34d 100644 --- a/src/utils/themeSnapshot.ts +++ b/src/utils/themeSnapshot.ts @@ -35,7 +35,7 @@ export const persistThemeModeSnapshot = (mode: ThemeMode): void => { try { localStorage.setItem(THEME_MODE_SNAPSHOT_KEY, JSON.stringify(mode)); } catch (error) { - console.error('[Theme Snapshot Error] LocalStorage quota exceeded:', error); + console.error('保存主题快照失败:', error); } }; diff --git a/src/utils/useStorageState.ts b/src/utils/useStorageState.ts index 94782a4..345f827 100644 --- a/src/utils/useStorageState.ts +++ b/src/utils/useStorageState.ts @@ -28,7 +28,6 @@ export const useStorageState = ( setValueInternal(next); }, []); - // Only load from storage once on mount useEffect(() => { if (hasLoadedFromStorage.current) return; @@ -64,7 +63,6 @@ export const useStorageState = ( }; }, [defaultValue, key, validator]); - // Save to storage and localStorage snapshot when value changes (after initial load) useEffect(() => { if (!isInitialized) return; if (!loadSucceededRef.current && !userModifiedRef.current) return;