diff --git a/.github/CODING_STANDARDS.md b/.github/CODING_STANDARDS.md index 8e36816..46c6bee 100644 --- a/.github/CODING_STANDARDS.md +++ b/.github/CODING_STANDARDS.md @@ -79,12 +79,12 @@ export function isSupportedImageType(mimeType: string): boolean { | 场景 | 导出方式 | 示例 | | ----------- | ------------------------------------------------ | --------------------------------- | -| 页面组件 | `export default function ComponentName()` | `pages/Timestamp/index.tsx` | +| 页面组件 | `export default function ComponentName()` | `src/pages/Timestamp/index.tsx` | | 业务组件 | `const X = React.memo(...)` + `export default X` | `LiveClock.tsx`, `ResultView.tsx` | -| UI 原子组件 | `React.forwardRef(...)` + `export { X }` | `components/ui/button.tsx` | -| 工具函数 | `export function xxx()` | `utils/clipboard.ts` | -| 自定义 Hook | `export function useXxx()` | `utils/useStorageState.ts` | -| 类型/接口 | `export interface` / `export type` | `types/storage.d.ts` | +| UI 原子组件 | `React.forwardRef(...)` + `export { X }` | `src/components/ui/button.tsx` | +| 工具函数 | `export function xxx()` | `src/utils/clipboard.ts` | +| 自定义 Hook | `export function useXxx()` | `src/utils/useStorageState.ts` | +| 类型/接口 | `export interface` / `export type` | `src/types/storage.d.ts` | ```typescript // ✅ 页面组件 — default export @@ -473,7 +473,7 @@ import ErrorBoundary from '@/components/ErrorBoundary'; | 类型 | 命名模式 | 示例 | | ----------- | -------------------------- | -------------------------------------------------------- | | 页面目录 | PascalCase | `Timestamp/`, `Base64Converter/`, `StorageCleaner/` | -| 页面入口 | `index.tsx` | `pages/Timestamp/index.tsx` | +| 页面入口 | `index.tsx` | `src/pages/Timestamp/index.tsx` | | 组件文件 | PascalCase `.tsx` | `TopBar.tsx`, `LiveClock.tsx`, `ResultView.tsx` | | 自定义 Hook | camelCase `.ts` | `useTimestampConverter.ts`, `useStorageCleaner.ts` | | 工具函数 | camelCase `.ts` | `chromeStorage.ts`, `base64Converter.ts`, `clipboard.ts` | @@ -527,12 +527,12 @@ const handleClean = useCallback(async () => { ... }, []); 测试文件放在源代码同级的 `__tests__/` 目录下: ``` -utils/__tests__/jwt.test.ts -utils/__tests__/base64Converter.test.ts -utils/__tests__/useStorageState.test.ts -components/__tests__/SwitchButtonGroup.test.tsx -components/__tests__/ErrorBoundary.test.tsx -pages/Timestamp/__tests__/index.test.tsx +src/utils/__tests__/jwt.test.ts +src/utils/__tests__/base64Converter.test.ts +src/utils/__tests__/useStorageState.test.ts +src/components/__tests__/SwitchButtonGroup.test.tsx +src/components/__tests__/ErrorBoundary.test.tsx +src/pages/Timestamp/__tests__/index.test.tsx ``` ### 7.2 describe / it 命名 @@ -653,16 +653,16 @@ export function useTimestampConverter(): UseTimestampConverterReturn { ... } ### 8.2 Hook 存放位置 -- **全局通用 Hook**:放在 `utils/` 目录下 +- **全局通用 Hook**:放在 `src/utils/` 目录下 - **页面专属 Hook**:与页面组件同目录 ``` -utils/useStorageState.ts — Chrome Storage 状态持久化 -utils/useLazyTranslation.ts — i18n 懒加载 -utils/useContextMenuData.ts — 右键菜单数据 -utils/useDebounce.ts — 防抖 -pages/Timestamp/useTimestampConverter.ts — 页面级 Hook -pages/StorageCleaner/useStorageCleaner.ts — 页面级 Hook +src/utils/useStorageState.ts — Chrome Storage 状态持久化 +src/utils/useLazyTranslation.ts — i18n 懒加载 +src/utils/useContextMenuData.ts — 右键菜单数据 +src/utils/useDebounce.ts — 防抖 +src/pages/Timestamp/useTimestampConverter.ts — 页面级 Hook +src/pages/StorageCleaner/useStorageCleaner.ts — 页面级 Hook ``` --- @@ -767,7 +767,7 @@ const [themeMode, setThemeMode, isInitialized] = useStorageState( ### 11.1 页面组件结构 ``` -pages/FeatureName/ +src/pages/FeatureName/ ├── index.tsx # 页面 UI(纯展示,使用 shadcn/ui 组件) ├── useFeatureName.ts # 业务逻辑 Hook(状态管理 + 转换逻辑) ├── constants.ts # 常量定义(可选) @@ -779,20 +779,19 @@ pages/FeatureName/ ### 11.2 目录职责 -| 目录 | 职责 | -| ---------------- | ------------------------------------------------------------ | -| `config/` | 应用配置(功能定义、路由映射) | -| `entrypoints/` | 扩展入口点(popup、options、sidepanel、background、content) | -| `pages/` | 功能页面组件(懒加载) | -| `components/` | 可复用 UI 组件 | -| `components/ui/` | shadcn/ui 基础组件(button、dialog、select 等) | -| `providers/` | React Context(Router、Theme 等) | -| `hooks/` | 自定义 React Hooks | -| `utils/` | 工具函数与服务抽象 | -| `types/` | TypeScript 类型声明 | -| `lib/` | 通用工具函数(cn、utils) | -| `i18n/` | 国际化资源 | -| `public/` | 静态资源 | +| 目录 | 职责 | +| -------------------- | ------------------------------------------------------------ | +| `src/config/` | 应用配置(功能定义、路由映射) | +| `src/entrypoints/` | 扩展入口点(popup、options、sidepanel、background、content) | +| `src/pages/` | 功能页面组件(懒加载) | +| `src/components/` | 可复用 UI 组件 | +| `src/components/ui/` | shadcn/ui 基础组件(button、dialog、select 等) | +| `src/providers/` | React Context(Router、Theme 等) | +| `src/hooks/` | 自定义 React Hooks | +| `src/utils/` | 工具函数与服务抽象 | +| `src/types/` | TypeScript 类型声明 | +| `src/lib/` | 通用工具函数(cn、utils) | +| `public/` | 静态资源 | --- diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index ff35225..4fbef56 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -102,7 +102,7 @@ pages/FeatureName/ ### 代码分割 -`wxt.config.ts` 通过 `manualChunksForHtmlOnly()` 自动分组 vendor 依赖(vendor-react、vendor-i18n、vendor-qr、vendor-dnd、vendor-markdown),无需手动配置。 +`wxt.config.ts` 通过 `manualChunksForHtmlOnly()` 自动分组 vendor 依赖(vendor-react、vendor-i18n、vendor-qr、vendor-dnd),无需手动配置。 ### 代码风格 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae16e93..c494b8a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,8 +13,6 @@ concurrency: cancel-in-progress: true jobs: - # 💡 1. 提速核心:前置基建节点(Infrastructure Initialization) - # 专门负责锁死环境、同步下载并缓存 node_modules,下游节点直接满血复用! setup: name: Prepare Dependencies runs-on: ubuntu-latest @@ -29,7 +27,6 @@ jobs: with: node-version: '22' - # 建立基于 package-lock.json 唯一哈希的缓存大闸 - name: Cache Node Modules id: cache-nodemodules uses: actions/cache@v4 @@ -47,7 +44,6 @@ jobs: id: cache-info run: echo "key=${{ runner.os }}-node-v22-${{ hashFiles('**/package-lock.json') }}" >> $GITHUB_OUTPUT - # 💡 2. 静态语法质检节点(依赖前置节点完成) lint: name: Lint runs-on: ubuntu-latest @@ -70,7 +66,6 @@ jobs: - name: Run ESLint run: npm run lint - # 💡 3. 强类型守卫节点(2秒瞬时恢复,开箱即查) typecheck: name: TypeScript Check runs-on: ubuntu-latest @@ -96,7 +91,6 @@ jobs: - name: Run TypeScript type check run: npm run typecheck - # 💡 4. 单元测试节点(无缝运行你刚刚修复完的 setupTests.ts 套件) test: name: Unit Tests runs-on: ubuntu-latest @@ -122,16 +116,12 @@ jobs: - name: Run tests run: npm run test - # 💡 5. 多端分布式最终编译节点(Production Matrix Compliance) build: name: Build (${{ matrix.browser }}) runs-on: ubuntu-latest - # 只有当 Linter、类型大闸、Vitest 单元测试全数满分通过,才放行最终打包编译 needs: [ lint, typecheck, test ] strategy: matrix: - # 💡 完美对齐 WXT 跨端架构:将 firefox 同步纳入生产编译大矩阵, - # 如果 firefox 编译因任何多端不兼容挂掉,CI 会立刻拉起警报,防护力拉满! browser: [ chrome, firefox ] fail-fast: false steps: @@ -149,7 +139,6 @@ jobs: path: node_modules key: ${{ runner.os }}-node-v22-${{ hashFiles('**/package-lock.json') }} - # 💡 动态代理编译指令:完美匹配 WXT / 各类多端打包器的标准构建命令 - name: Generate WXT types run: npx wxt prepare diff --git a/AGENTS.md b/AGENTS.md index bdf1672..82efa64 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -43,16 +43,18 @@ Pre-commit hook(`.husky/pre-commit` 调用 `lint-staged`,任一步骤返回 ## 项目结构 ``` -config/features.tsx # 功能定义(路由 + 元数据的单一事实来源) -entrypoints/ # 扩展入口点 (popup/, options/, sidepanel/, background.ts, content.ts) -pages/ # 功能页面组件 (懒加载) -components/ # 可复用 UI 组件 -components/ui/ # shadcn/ui 基础组件 (button, dialog, select 等) -providers/ # React Context (Router, Theme 等) -hooks/ # 自定义 React Hooks -utils/ # 工具函数与服务抽象 -types/ # TypeScript 类型声明 -i18n/locales/{zh,en}/ # 国际化资源 (common.json, features.json 及各功能独立 JSON) +src/ # 源代码根目录 + config/features.tsx # 功能定义(路由 + 元数据的单一事实来源) + entrypoints/ # 扩展入口点 (popup/, sidepanel/, background.ts, content.ts) + pages/ # 功能页面组件 (懒加载) + components/ # 可复用 UI 组件 + components/ui/ # shadcn/ui 基础组件 (button, dialog, select 等) + providers/ # React Context (Router, Theme 等) + hooks/ # 自定义 React Hooks + utils/ # 工具函数与服务抽象 + types/ # TypeScript 类型声明 + lib/ # 通用工具函数(cn、utils) +public/ # 静态资源(图标、_locales 等) ``` ### 页面组件模式 @@ -60,7 +62,7 @@ i18n/locales/{zh,en}/ # 国际化资源 (common.json, features.json 及各功 典型功能页面遵循 **UI + Hook 分离** 模式: ``` -pages/FeatureName/ +src/pages/FeatureName/ ├── index.tsx # 页面 UI(纯展示,使用 shadcn/ui 组件) ├── useFeatureName.ts # 业务逻辑 Hook(状态管理 + 转换逻辑) └── constants.ts # 常量定义 @@ -72,15 +74,15 @@ pages/FeatureName/ ## 关键架构决策 -**路由**: 不使用 React Router。通过 `config/features.tsx` 的 `FEATURES` 数组管理,`RouterProvider` 根据 `PageType` +**路由**: 不使用 React Router。通过 `src/config/features.tsx` 的 `FEATURES` 数组管理,`RouterProvider` 根据 `PageType` 渲染对应组件。支持三种渲染模式:popup(弹窗)、sidepanel(侧边栏)和 browser-tab(浏览器新标签页,通过 `open_in_tab` 打开)。 每种模式有独立的路由和可见页面配置(`app/popupRoute`、`app/sidepanelRoute`、`app/tabRoute` 等)。 -**存储**: 所有 Chrome Storage 键必须在 `types/storage.d.ts` 的 `StorageSchema` 中定义,键名使用 kebab-case 格式(如 `app/currentRoute`)。 -使用 `utils/chromeStorage.ts` 及其 Hook。Router 同时使用 `chrome.storage.local` 和 `localStorage` 做快照以消除首屏闪烁。 -修改 StorageSchema 时,必须在 `utils/chromeStorage.ts` 添加版本迁移函数,并在测试中覆盖迁移场景。 +**存储**: 所有 Chrome Storage 键必须在 `src/types/storage.d.ts` 的 `StorageSchema` 中定义,键名使用 kebab-case 格式(如 `app/currentRoute`)。 +使用 `src/utils/chromeStorage.ts` 及其 Hook。Router 同时使用 `chrome.storage.local` 和 `localStorage` 做快照以消除首屏闪烁。 +修改 StorageSchema 时,必须在 `src/utils/chromeStorage.ts` 添加版本迁移函数,并在测试中覆盖迁移场景。 -**通信**: 使用 `@webext-core/messaging`,协议定义在 `utils/messages.ts`。 +**通信**: 使用 `@webext-core/messaging`,协议定义在 `src/utils/messages.ts`。 **路径别名**: `@/` 映射到项目根目录 (已在 tsconfig 和 vitest.config 中配置)。 @@ -116,9 +118,9 @@ pages/FeatureName/ ## 新功能开发清单 -1. 在 `types/storage.d.ts` 添加 `PageType` 联合类型 -2. 在 `config/features.tsx` 的 `FEATURES` 数组添加配置(指定 key、翻译键、图标、三种渲染模式的组件) -3. 在 `pages/` 创建页面组件 (懒加载): +1. 在 `src/types/storage.d.ts` 添加 `PageType` 联合类型 +2. 在 `src/config/features.tsx` 的 `FEATURES` 数组添加配置(指定 key、翻译键、图标、三种渲染模式的组件) +3. 在 `src/pages/` 创建页面组件 (懒加载): - `index.tsx` — UI 组件,使用 `useLazyTranslation` 获取翻译 - `useFeatureName.ts` — 业务逻辑 Hook - `constants.ts` — 常量(可选) @@ -131,7 +133,7 @@ pages/FeatureName/ - 禁止使用 `any` (测试文件除外) - 未使用变量/参数: 使用 `_` 前缀 (如 `_unused`) - 样式: 使用 Tailwind CSS + shadcn/ui (通过 `className` 和 `cn()` 工具) -- UI 组件: 优先使用 `components/ui/` 下的 shadcn/ui 组件 (button, dialog, select 等) +- UI 组件: 优先使用 `src/components/ui/` 下的 shadcn/ui 组件 (button, dialog, select 等) - 图标: 使用 `lucide-react` 图标库 - 格式: Prettier (`.prettierrc`: 100 字符宽, 单引号, 尾逗号 all, LF 换行) - ESLint 使用 `typescript-eslint` 的 `projectService: true`(无需手动维护 project 路径) @@ -140,7 +142,6 @@ pages/FeatureName/ - `@webext-core/messaging` — 扩展消息通信 - `@dnd-kit` — 拖拽排序(用于页面顺序管理) -- `marked` — Markdown 解析 - `qrious` + `qr-scanner` — 二维码生成与解析 - `dayjs` — 日期处理(时间戳转换) - `sonner` — Toast 通知(替代传统 snackbar) diff --git a/README.md b/README.md index 4c56054..bc99572 100644 --- a/README.md +++ b/README.md @@ -63,11 +63,6 @@ - **文件转换**: 支持文件与 Base64 字符串互转. - **图像预览**: 支持图片 Base64 编码与实时预览. -### 📝 Markdown ↔ HTML 转换 - -- **Markdown 转 HTML**: 实时预览 Markdown 渲染结果,支持导出 HTML 源码. -- **HTML 转 Markdown**: 将 HTML 内容转换为 Markdown 格式. - ## 技术栈 - **框架**: [WXT (Web Extension Toolkit)](https://wxt.dev/) @@ -84,25 +79,24 @@ ## 项目结构 ```text -├── components/ # 可复用 React 组件 -├── config/ # 应用配置 -│ └── features.tsx # 功能定义与路由映射 -├── entrypoints/ # 扩展程序入口点 -│ ├── popup/ # 点击图标弹出的主界面 -│ ├── options/ # 扩展程序设置页面 -│ ├── sidepanel/ # 浏览器侧边栏集成 -│ ├── background.ts # 后台 Service Worker -│ └── content.ts # 网页注入脚本 -├── pages/ # 各功能模块的页面组件 -├── providers/ # 全局状态提供者 (Router, Theme 等) -├── hooks/ # 自定义 React Hooks -├── utils/ # 工具函数与服务抽象 -├── types/ # TypeScript 类型声明 -├── lib/ # 通用工具函数 (cn, utils 等) -├── i18n/ # 国际化资源 -├── public/ # 静态资源 (图标、manifest 资源等) -├── wxt.config.ts # WXT 框架核心配置 -└── package.json # 项目元数据与依赖管理 +├── src/ # 源代码根目录 +│ ├── components/ # 可复用 React 组件 +│ ├── config/ # 应用配置 +│ │ └── features.tsx # 功能定义与路由映射 +│ ├── entrypoints/ # 扩展程序入口点 +│ │ ├── popup/ # 点击图标弹出的主界面 +│ │ ├── sidepanel/ # 浏览器侧边栏集成 +│ │ ├── background.ts # 后台 Service Worker +│ │ └── content.ts # 网页注入脚本 +│ ├── pages/ # 各功能模块的页面组件 +│ ├── providers/ # 全局状态提供者 (Router, Theme 等) +│ ├── hooks/ # 自定义 React Hooks +│ ├── utils/ # 工具函数与服务抽象 +│ ├── types/ # TypeScript 类型声明 +│ └── lib/ # 通用工具函数 (cn, utils 等) +├── public/ # 静态资源 (图标、manifest 资源等) +├── wxt.config.ts # WXT 框架核心配置 +└── package.json # 项目元数据与依赖管理 ``` ## 开发与部署 diff --git a/entrypoints/options/App.tsx b/entrypoints/options/App.tsx deleted file mode 100644 index 346be9f..0000000 --- a/entrypoints/options/App.tsx +++ /dev/null @@ -1,383 +0,0 @@ -import { SyntheticEvent, useEffect, useMemo, useState } from 'react'; -import { GripVertical, RefreshCw } from 'lucide-react'; -import { - closestCenter, - DndContext, - type DragEndEvent, - KeyboardSensor, - PointerSensor, - useSensor, - useSensors, -} from '@dnd-kit/core'; -import { - arrayMove, - SortableContext, - sortableKeyboardCoordinates, - useSortable, - verticalListSortingStrategy, -} from '@dnd-kit/sortable'; -import { CSS } from '@dnd-kit/utilities'; -import type { PageType, StorageSchema } from '@/types/storage'; -import { storageUtil } from '@/utils/chromeStorage'; -import type { PaletteColorKey } from '@/config/features'; -import { - getAllFeatureKeys, - getDefaultPageOrder, - getDefaultVisibleFeatureKeys, - getFeatureByKey, -} from '@/config/features'; -import GlobalSnackbar, { useSnackbarState } from '@/components/GlobalSnackbar'; -import PageErrorBoundary from '@/components/PageErrorBoundary'; -import { useTranslation } from 'react-i18next'; - -const PALETTE_COLORS: Record = { - primary: '#1976d2', - success: '#2e7d32', - warning: '#e65100', - error: '#c62828', - secondary: '#9c27b0', - info: '#0288d1', -}; - -const getColorCode = (key: PaletteColorKey): string => PALETTE_COLORS[key]; - -const isValidPage = (page: unknown): page is PageType => { - return typeof page === 'string' && (getAllFeatureKeys() as string[]).includes(page); -}; - -const isValidPageList = (pages: unknown): pages is PageType[] => { - return Array.isArray(pages) && pages.every(isValidPage); -}; - -type WindowType = 'popup' | 'sidepanel' | 'tab'; - -interface SortableFeatureRowProps { - pageKey: PageType; - isLast: boolean; - isChecked: boolean; - isDisabled: boolean; - onToggle: (key: PageType) => void; -} - -function SortableFeatureRow({ - pageKey, - isLast, - isChecked, - isDisabled, - onToggle, -}: SortableFeatureRowProps) { - const { t } = useTranslation(['features']); - const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ - id: pageKey, - }); - - const feature = getFeatureByKey(pageKey); - if (!feature) return null; - - const colorKey = feature.themeColorKey ?? 'primary'; - const colorCode = getColorCode(colorKey); - - const style = { - transform: CSS.Transform.toString(transform), - transition, - zIndex: isDragging ? 1 : 'auto', - position: 'relative' as const, - backgroundColor: isDragging ? `${colorCode}0a` : 'transparent', - boxShadow: isDragging ? '0 8px 20px rgba(0,0,0,0.08)' : 'none', - }; - - return ( -
-
- {/* 拖拽手柄 */} -
- -
- - {/* 功能图标 */} -
- {feature.icon && } -
- - {/* 文本信息 */} -
-
- {t(feature.labelKey)} -
- {feature.descriptionKey && ( -
- {t(feature.descriptionKey)} -
- )} -
-
- - {/* 开关 */} - -
- ); -} - -export default function App() { - const { t } = useTranslation(['features', 'common']); - - const initialWindowType = useMemo(() => { - if (typeof window === 'undefined') return 'popup'; - const params = new URLSearchParams(window.location.search); - const tab = params.get('tab'); - if (tab === 'popup' || tab === 'sidepanel' || tab === 'tab') { - return tab as WindowType; - } - return 'popup'; - }, []); - - const [windowType, setWindowType] = useState(initialWindowType); - const [visiblePages, setVisiblePages] = useState([]); - const [pageOrder, setPageOrder] = useState([]); - const [isLoaded, setIsLoaded] = useState(false); - const { snackbarProps, showMessage } = useSnackbarState(); - - const sensors = useSensors( - useSensor(PointerSensor, { activationConstraint: { distance: 4 } }), - useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }), - ); - - const configKeys = useMemo(() => { - switch (windowType) { - case 'sidepanel': - return { - visible: 'app/sidepanelVisiblePages' as keyof StorageSchema, - order: 'app/sidepanelPageOrder' as keyof StorageSchema, - }; - case 'tab': - return { - visible: 'app/tabVisiblePages' as keyof StorageSchema, - order: 'app/tabPageOrder' as keyof StorageSchema, - }; - case 'popup': - default: - return { - visible: 'app/popupVisiblePages' as keyof StorageSchema, - order: 'app/popupPageOrder' as keyof StorageSchema, - }; - } - }, [windowType]); - - useEffect(() => { - const url = new URL(window.location.href); - url.searchParams.set('tab', windowType); - window.history.replaceState({}, '', url.toString()); - }, [windowType]); - - useEffect(() => { - const loadConfig = async () => { - setIsLoaded(false); - try { - const [savedVisible, savedOrder] = await Promise.all([ - storageUtil.get(configKeys.visible, getDefaultVisibleFeatureKeys()), - storageUtil.get(configKeys.order, getDefaultPageOrder()), - ]); - setVisiblePages( - isValidPageList(savedVisible) ? savedVisible : getDefaultVisibleFeatureKeys(), - ); - setPageOrder(isValidPageList(savedOrder) ? savedOrder : getDefaultPageOrder()); - } catch (error) { - console.error('Failed to load config:', error); - setVisiblePages(getDefaultVisibleFeatureKeys()); - setPageOrder(getDefaultPageOrder()); - } finally { - setIsLoaded(true); - } - }; - - loadConfig().catch(console.error); - }, [configKeys]); - - const showToast = (message: string, severity: 'success' | 'info' | 'warning') => { - showMessage(message, { severity }); - }; - - const handlePageToggle = async (page: PageType) => { - const isCurrentlyVisible = visiblePages.includes(page); - let newPages: PageType[]; - - if (isCurrentlyVisible) { - if (visiblePages.length <= 1) { - showToast('至少需要保留一个可见页面', 'warning'); - return; - } - newPages = visiblePages.filter((p) => p !== page); - } else { - newPages = [...visiblePages, page]; - } - - try { - await storageUtil.set(configKeys.visible, newPages); - setVisiblePages(newPages); - const feature = getFeatureByKey(page); - const label = feature ? t(feature.labelKey) : page; - showToast(`已${isCurrentlyVisible ? '隐藏' : '显示'} ${label}`, 'success'); - } catch (error) { - console.error('Failed to save config:', error); - showToast('保存失败', 'warning'); - } - }; - - const handleDragEnd = async (event: DragEndEvent) => { - const { active, over } = event; - if (!over || active.id === over.id) return; - - const oldIndex = pageOrder.indexOf(active.id as PageType); - const newIndex = pageOrder.indexOf(over.id as PageType); - if (oldIndex < 0 || newIndex < 0) return; - - const newOrder = arrayMove(pageOrder, oldIndex, newIndex); - setPageOrder(newOrder); - - try { - await storageUtil.set(configKeys.order, newOrder); - } catch (error) { - console.error('Failed to save order:', error); - showToast('排序保存失败', 'warning'); - } - }; - - const handleRestoreDefaults = async () => { - try { - const defaults = getDefaultVisibleFeatureKeys(); - const defaultOrder = getDefaultPageOrder(); - - await Promise.all([ - storageUtil.set(configKeys.visible, defaults), - storageUtil.set(configKeys.order, defaultOrder), - ]); - - setVisiblePages(defaults); - setPageOrder(defaultOrder); - showToast('已恢复当前模式默认设置', 'success'); - } catch (error) { - console.error('Failed to restore defaults:', error); - showToast('恢复失败', 'warning'); - } - }; - - const handleWindowTypeChange = (_event: SyntheticEvent, newType: WindowType) => { - if (newType !== null) { - setWindowType(newType); - } - }; - - if (!isLoaded) { - return ( -
-
-
- ); - } - - return ( -
- - {/* 顶部标题与导航栏 */} -
-
- {/* Tab 与恢复按钮同行 */} -
-
- {(['popup', 'sidepanel', 'tab'] as WindowType[]).map((type) => ( - - ))} -
- -
-
-
- - {/* 主内容区域 */} -
-
-
- - -
- {pageOrder.map((key, index, array) => { - const isChecked = visiblePages.includes(key); - const isDisabled = isChecked && visiblePages.length === 1; - return ( - - ); - })} -
-
-
-
-
-
-
- - -
- ); -} diff --git a/entrypoints/options/__tests__/App.test.tsx b/entrypoints/options/__tests__/App.test.tsx deleted file mode 100644 index d627a0c..0000000 --- a/entrypoints/options/__tests__/App.test.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import { describe, expect, it, vi, beforeEach } from 'vitest'; -import { render, screen, waitFor } from '@testing-library/react'; -import App from '../App'; -import { storageUtil } from '@/utils/chromeStorage'; -import { - getDefaultVisibleFeatureKeys, - getDefaultPageOrder, - getFeatureByKey, -} from '@/config/features'; - -// Mock storageUtil -vi.mock('@/utils/chromeStorage', () => ({ - storageUtil: { - get: vi.fn(), - set: vi.fn(() => Promise.resolve()), - }, -})); - -// Mock i18next -vi.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), - withTranslation: () => (Component: any) => Component, -})); - -describe('Options App', () => { - beforeEach(() => { - vi.clearAllMocks(); - localStorage.clear(); - }); - - it('合法数据应正常加载并显示正确的功能列表', async () => { - const defaultOrder = getDefaultPageOrder(); - - (storageUtil.get as any).mockImplementation((_key: string, defaultValue: any) => - Promise.resolve(defaultValue), - ); - - render(); - - await waitFor(() => { - expect(screen.queryByRole('progressbar')).not.toBeInTheDocument(); - }); - - // 验证默认可见的功能都被渲染出来了 - for (const key of defaultOrder) { - const feature = getFeatureByKey(key); - if (feature) { - expect(screen.getByText(feature.labelKey)).toBeInTheDocument(); - } - } - }); - - it('非法 visiblePages 数据应回退到默认值', async () => { - const defaultVisible = getDefaultVisibleFeatureKeys(); - - (storageUtil.get as any).mockImplementation((key: string, defaultValue: any) => { - if (key.includes('VisiblePages')) { - return Promise.resolve(['invalidPage', 'anotherInvalid']); - } - return Promise.resolve(defaultValue); - }); - - render(); - - await waitFor(() => { - expect(screen.queryByRole('progressbar')).not.toBeInTheDocument(); - }); - - // 验证默认值的功能仍然被渲染(非法数据被回退) - for (const key of defaultVisible) { - const feature = getFeatureByKey(key); - if (feature && key !== 'dashboard') { - expect(screen.getByText(feature.labelKey)).toBeInTheDocument(); - } - } - }); - - it('非法 pageOrder 数据应回退到默认值', async () => { - const defaultOrder = getDefaultPageOrder(); - - (storageUtil.get as any).mockImplementation((key: string, defaultValue: any) => { - if (key.includes('PageOrder')) { - return Promise.resolve(['notARealPage', 123, null]); - } - return Promise.resolve(defaultValue); - }); - - render(); - - await waitFor(() => { - expect(screen.queryByRole('progressbar')).not.toBeInTheDocument(); - }); - - // 验证默认顺序的功能都被渲染 - for (const key of defaultOrder) { - const feature = getFeatureByKey(key); - if (feature) { - expect(screen.getByText(feature.labelKey)).toBeInTheDocument(); - } - } - }); - - it('非数组数据应回退到默认值', async () => { - const defaultOrder = getDefaultPageOrder(); - - (storageUtil.get as any).mockImplementation((key: string, defaultValue: any) => { - if (key.includes('VisiblePages')) { - return Promise.resolve('not-an-array'); - } - if (key.includes('PageOrder')) { - return Promise.resolve({ foo: 'bar' }); - } - return Promise.resolve(defaultValue); - }); - - render(); - - await waitFor(() => { - expect(screen.queryByRole('progressbar')).not.toBeInTheDocument(); - }); - - // 验证默认功能都被渲染 - for (const key of defaultOrder) { - const feature = getFeatureByKey(key); - if (feature) { - expect(screen.getByText(feature.labelKey)).toBeInTheDocument(); - } - } - }); -}); diff --git a/entrypoints/options/index.html b/entrypoints/options/index.html deleted file mode 100644 index e74b0f1..0000000 --- a/entrypoints/options/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - 扩展设置 - Testing Tools - - -
- - - diff --git a/entrypoints/options/main.tsx b/entrypoints/options/main.tsx deleted file mode 100644 index d423c3a..0000000 --- a/entrypoints/options/main.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import ReactDOM from 'react-dom/client'; -import AppRoot from '@/providers/AppRoot'; -import '@/i18n'; -import '@/src/index.css'; -import App from './App'; - -ReactDOM.createRoot(document.getElementById('root')!).render( - - - , -); diff --git a/eslint.config.ts b/eslint.config.ts index db7eb21..28b8cab 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -5,16 +5,13 @@ import reactPlugin from 'eslint-plugin-react'; import globals from 'globals'; export default tseslint.config( - // 1. 全局物理隔离:彻底掐灭对构建产物与配置本身的干扰 { ignores: ['dist', '.output', '.wxt', 'node_modules', 'eslint.config.ts', 'eslint.config.js'], }, - // 2. 注入 JavaScript 与 TypeScript 的官方大师级推荐规则集 js.configs.recommended, ...tseslint.configs.recommended, - // 3. 针对测试文件专属沙箱:解耦强类型死锁,放行 any,容忍未消费变量 { files: ['**/__tests__/**', '**/*.test.{ts,tsx}', '**/*.spec.{ts,tsx}', 'setupTests.ts'], rules: { @@ -26,16 +23,8 @@ export default tseslint.config( }, }, - // 4. 核心业务全受控大管线(Hooks, Entrypoints, Components 统一护航) { - files: [ - 'hooks/**/*.{ts,tsx}', - 'entrypoints/**/*.{ts,tsx}', - 'pages/**/*.{ts,tsx}', - 'utils/**/*.{ts,tsx}', - 'components/**/*.{ts,tsx}', - 'services/**/*.{ts,tsx}', - ], + files: ['src/**/*.{ts,tsx}'], ignores: ['**/__tests__/**', '**/*.test.{ts,tsx}', '**/*.spec.{ts,tsx}'], languageOptions: { @@ -44,17 +33,13 @@ export default tseslint.config( ...globals.browser, ...globals.node, }, - // 💡 修复点 1(史诗级治愈):废除脆弱的 project 硬编码路径! - // 拥抱 typescript-eslint 官方推荐的 projectService 常驻动态类型调度中枢。 - // 它会在内存中全自动、流式为所有新建、悬空或暂存文件分配编译上下文, - // 彻底终结 "file is not included in any tsconfig" 的全量崩溃黑洞! + parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname, }, }, - // 挂载插件沙箱 plugins: { react: reactPlugin, 'react-hooks': reactHooks, @@ -66,24 +51,19 @@ export default tseslint.config( }, }, - // 💡 修复点 2:高精对齐 React 19 / JSX Runtime 的全量生产质检规则大闸 rules: { - // 激活 react-hooks 官方推荐规则 ...reactHooks.configs.recommended.rules, - // 激活 react 官方精选规则(排除旧版 React 必须手动 import 的历史包袱) ...reactPlugin.configs.recommended.rules, ...reactPlugin.configs['jsx-runtime'].rules, 'react/prop-types': 'off', - // 清洗原生未消费变量冲突,统一交由 TS 高阶哨兵接管 'no-unused-vars': 'off', '@typescript-eslint/no-unused-vars': [ 'warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }, ], - // 彻底关闭老旧的 JSX 作用域检查,全面契合 React 19 核心美学 'react/react-in-jsx-scope': 'off', }, }, diff --git a/i18n/index.ts b/i18n/index.ts deleted file mode 100644 index e467f98..0000000 --- a/i18n/index.ts +++ /dev/null @@ -1,144 +0,0 @@ -import i18n from 'i18next'; -import { initReactI18next } from 'react-i18next'; -import type { CustomDetector } from 'i18next-browser-languagedetector'; // 💡 1. 引入官方强类型探测器接口 -import LanguageDetector from 'i18next-browser-languagedetector'; -import { storageUtil } from '@/utils/chromeStorage'; -import dayjs from 'dayjs'; - -// 导入 Day.js 本地化语言包 -import 'dayjs/locale/zh-cn'; - -// 同步加载全局核心命名空间 -import commonZh from './locales/zh/common.json'; -import featuresZh from './locales/zh/features.json'; -import commonEn from './locales/en/common.json'; -import featuresEn from './locales/en/features.json'; - -const resources = { - zh: { - common: commonZh, - features: featuresZh, - }, - en: { - common: commonEn, - features: featuresEn, - }, -}; - -export const SUPPORTED_LANGUAGES = ['zh', 'en'] as const; -export type SupportedLanguage = (typeof SUPPORTED_LANGUAGES)[number]; - -const LANGUAGE_STORAGE_KEY = 'app/language'; -const LANGUAGE_SNAPSHOT_KEY = 'snapshot/app/language'; - -/** - * 将任意语言标识归一化为受支持的核心代码 - */ -export const normalizeLanguage = (lng: string): SupportedLanguage => { - if (!lng) return 'en'; - return lng.toLowerCase().startsWith('zh') ? 'zh' : 'en'; -}; - -/** - * 严格校验语言安全边界 - */ -const isValidLanguage = (lng: unknown): lng is SupportedLanguage => { - return typeof lng === 'string' && (SUPPORTED_LANGUAGES as readonly string[]).includes(lng); -}; - -/** - * 同步从 localStorage 获取语言快照(消除异步闪烁) - */ -const getSyncLanguageSnapshot = (): SupportedLanguage | null => { - try { - const val = localStorage.getItem(LANGUAGE_SNAPSHOT_KEY); - if (!val) return null; - const parsed = JSON.parse(val) as unknown; - return isValidLanguage(parsed) ? parsed : null; - } catch (error) { - console.error('[i18n] Failed to parse sync language snapshot from localStorage:', error); - return null; - } -}; - -// 💡 2. 强类型接口重塑:显式绑定 CustomDetector 类型, -// 告诉 TS 编译器这些方法将被全局 Languagedetector 框架隐式调用,彻底治愈“未使用函数”报错! -const chromeStorageDetector: CustomDetector = { - name: 'chromeStorage', - lookup() { - return undefined; - }, - cacheUserLanguage(lng: string) { - const target = normalizeLanguage(lng); - // 💡 修复点:对异步写盘操作追加 void 算子或 catch,吞掉 Promise 被忽略警告 - storageUtil.set(LANGUAGE_STORAGE_KEY, target).catch((err) => { - console.error('[i18n Detector Error] Failed to write back language state:', err); - }); - }, -}; - -const detector = new LanguageDetector(); -detector.addDetector(chromeStorageDetector); - -const syncLng = getSyncLanguageSnapshot(); - -// 💡 3. 修复点:对 i18n.init() 返回的异步 Promise 前方追加 void 斩断依赖链,放行编译 -void i18n - .use(detector) - .use(initReactI18next) - .init({ - resources, - fallbackLng: 'en', - lng: syncLng || undefined, - ns: ['common', 'features'], - defaultNS: 'common', - debug: false, - interpolation: { - escapeValue: false, - }, - detection: { - order: ['chromeStorage', 'navigator'], - caches: ['chromeStorage'], - }, - }); - -// 监听语言变更 -i18n.on('languageChanged', (lng) => { - const normalizedLng = normalizeLanguage(lng); - dayjs.locale(normalizedLng === 'zh' ? 'zh-cn' : 'en'); - localStorage.setItem(LANGUAGE_SNAPSHOT_KEY, JSON.stringify(normalizedLng)); -}); - -// 初始化时从长期异步存储中恢复校准 -storageUtil - .get(LANGUAGE_STORAGE_KEY) - .then((lng) => { - const rawTargetLng = lng || syncLng; - - if (!rawTargetLng) { - const initialLng = normalizeLanguage(i18n.language); - - // 💡 修复点:对初始化同步写盘追加安全的 Promise .catch() 异常隔离防护罩 - storageUtil.set(LANGUAGE_STORAGE_KEY, initialLng).catch((err) => { - console.error('[i18n Init Error] Persistent sync collapsed:', err); - }); - - if (initialLng !== normalizeLanguage(i18n.language)) { - // 💡 修复点:对 changeLanguage 异步微任务进行显式 void 断链安全隔离 - void i18n.changeLanguage(initialLng); - } - return; - } - - const targetLng = normalizeLanguage(String(rawTargetLng)); - - if (isValidLanguage(targetLng) && targetLng !== normalizeLanguage(i18n.language)) { - // 💡 修复点:对 changeLanguage 异步微任务进行显式 void 断链安全隔离 - void i18n.changeLanguage(targetLng); - } - }) - .catch((err) => { - console.error('[i18n Context Error] Async local storage lookup collapsed:', err); - }); - -export default i18n; diff --git a/i18n/locales/en/base64Converter.json b/i18n/locales/en/base64Converter.json deleted file mode 100644 index 0a59008..0000000 --- a/i18n/locales/en/base64Converter.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "pageTitle": "Base64 Converter", - "pageSubtitle": "Encode and decode text, files, and images with Base64", - "textMode": "Text", - "fileMode": "File", - "imageMode": "Image", - "encode": "Encode", - "decode": "Decode", - "clear": "Clear", - "textInputPlaceholder": "Enter text to encode to Base64...", - "base64InputPlaceholder": "Enter Base64 string to decode...", - "base64Output": "Base64 Output", - "textOutput": "Decoded Text Output", - "copyRaw": "Copy Raw Base64", - "copyDataUri": "Copy Data URI", - "clickOrDropToFile": "Click or drop a file here", - "clickOrDropToImage": "Click or drop an image here", - "clickOrDropToReplace": "Click or drop to replace the file", - "maxFileSize": "Maximum file size: {{max}}", - "supportedFormats": "Supports PNG, JPG, WEBP, GIF, BMP, SVG, etc.", - "fileSizeExceeded": "File size exceeds the limit (max {{max}})", - "unsupportedImageType": "Unsupported image format", - "conversionFailed": "Conversion failed", - "originalSize": "Original Size", - "encodedSize": "Encoded Size", - "invalidBase64": "Invalid Base64 string", - "binaryDataDetected": "Input appears to be binary data (e.g. an image). Please switch to the Image tab.", - "imageDataUriHint": "Detected an image data URI — please use the Image tab to decode it.", - "switchToImageMode": "Switch to Image mode", - "download": "Download", - "decodedFileName": "Decoded file name", - "decodeBase64Placeholder": "Enter Base64 or data URI to decode...", - "decodedFileOutput": "Decoded File", - "decodedImageOutput": "Decoded Image", - "inferredMimeType": "Inferred MIME type", - "decodedSize": "Decoded size" -} diff --git a/i18n/locales/en/common.json b/i18n/locales/en/common.json deleted file mode 100644 index 7e26906..0000000 --- a/i18n/locales/en/common.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "appName": "Testing Tools", - "buttons": { - "save": "Save", - "cancel": "Cancel", - "confirm": "Confirm", - "copy": "Copy", - "clear": "Clear", - "refresh": "Refresh", - "toggleLanguage": "Switch Language", - "toggleTheme": "Toggle theme", - "themeMode": { - "light": "Switch to dark mode", - "dark": "Switch to system mode", - "system": "Switch to light mode" - }, - "search": "Search tools...", - "back": "Back", - "clearSearch": "Clear search", - "recentSearch": "Recent search", - "noResults": "No tools found", - "openInTab": "Open in tab", - "settings": "Settings" - }, - "messages": { - "copySuccess": "Copied to clipboard", - "copyError": "Copy failed", - "copyEmpty": "Nothing to copy" - }, - "errorBoundary": { - "title": "Oops, something went wrong", - "description": "The app encountered an unexpected error. You can try refreshing the page or resetting the app.", - "refresh": "Refresh App", - "retry": "Retry" - }, - "pageErrorBoundary": { - "title": "This feature has encountered an error", - "description": "An internal script error occurred while loading or rendering this page. You can try again or switch to another tool from the navigation menu." - }, - "router": { - "notFound": "Page Not Found", - "notFoundDescription": "This feature is not available or has been removed in the current runtime ({{entryPointType}})." - }, - "textInputArea": { - "clear": "Clear", - "copyContent": "Copy content", - "cleared": "Cleared", - "placeholder": "placeholder text" - } -} diff --git a/i18n/locales/en/features.json b/i18n/locales/en/features.json deleted file mode 100644 index 96dc086..0000000 --- a/i18n/locales/en/features.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "dashboard": { - "title": "Dashboard" - }, - "timestamp": { - "title": "Timestamp", - "description": "Unix millisecond conversion and formatting" - }, - "storageCleaner": { - "title": "Storage Cleaner", - "description": "Clean cache, cookies, and local storage" - }, - "qrCode": { - "title": "QR Code Tools", - "description": "Generate QR code for the selected URL" - }, - "textStatistics": { - "title": "Text Statistics", - "description": "Real-time analysis of text characters, words, and bytes" - }, - "jwt": { - "title": "JWT Parser", - "description": "JSON Web Token decoding and viewing" - }, - "jsonDiff": { - "title": "JSON Tools", - "description": "Diff, format, YAML/TOML conversion, and minify" - }, - "base64Converter": { - "title": "Base64 Converter", - "description": "Encode text, files, and images to Base64" - }, - "markdownToHtml": { - "title": "Markdown to HTML", - "description": "Real-time Markdown conversion and HTML preview" - }, - "htmlToMarkdown": { - "title": "HTML to Markdown", - "description": "Real-time HTML conversion and Markdown preview" - }, - "rightClickRestorer": { - "title": "Right Click Restorer", - "description": "Detect and restore disabled browser right-click menus" - } -} diff --git a/i18n/locales/en/htmlToMarkdown.json b/i18n/locales/en/htmlToMarkdown.json deleted file mode 100644 index f3d1660..0000000 --- a/i18n/locales/en/htmlToMarkdown.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "pageTitle": "HTML to Markdown", - "pageSubtitle": "Convert HTML to Markdown in real-time", - "splitMode": "Split", - "previewMode": "Preview", - "markdownMode": "Markdown", - "clear": "Clear", - "inputLabel": "HTML Input", - "previewLabel": "Markdown Preview", - "markdownOutputLabel": "Markdown Output", - "inputPlaceholder": "Enter your HTML content here...", - "charCount": "{{count}} chars", - "copyMarkdown": "Copy Markdown", - "download": "Download", - "emptyHint": "Enter HTML content to see the conversion result" -} diff --git a/i18n/locales/en/jsonDiff.json b/i18n/locales/en/jsonDiff.json deleted file mode 100644 index 41cb13f..0000000 --- a/i18n/locales/en/jsonDiff.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "pageTitle": "JSON Diff", - "pageSubtitle": "Compare differences between two JSON values", - "leftPlaceholder": "Enter the original JSON...", - "rightPlaceholder": "Enter the target JSON...", - "leftLabel": "Original JSON", - "rightLabel": "Target JSON", - "compareButton": "Compare", - "clearButton": "Clear", - "sideBySideMode": "Side by side", - "unifiedMode": "Unified", - "previousDiff": "Previous", - "nextDiff": "Next", - "noDiffs": "No differences", - "diffCount": "{{count}} differences", - "invalidJson": "Invalid JSON format", - "emptyHint": "Enter JSON on both sides and click Compare", - "fixErrorHint": "Fix the JSON syntax errors above to enable live comparison", - "added": "Added", - "removed": "Removed", - "modified": "Modified" -} diff --git a/i18n/locales/en/jsonFormat.json b/i18n/locales/en/jsonFormat.json deleted file mode 100644 index 05aeb97..0000000 --- a/i18n/locales/en/jsonFormat.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "formatTitle": "JSON Formatter", - "formatSubtitle": "Beautify and format JSON data", - "inputPlaceholder": "Enter JSON to format...", - "formatButton": "Format", - "clearButton": "Clear", - "sortKeys": "Sort Keys", - "sortKeysTooltip": "Sort JSON object keys in alphabetical order", - "indentSize": "Indent", - "indentSpaces": "{{count}} spaces", - "outputLabel": "Formatted Result", - "copySuccess": "Copied", - "copyFail": "Copy failed", - "noContent": "Nothing to copy", - "invalidJson": "Invalid JSON format", - "emptyHint": "Enter JSON and click Format", - "fixErrorHint": "Fix the JSON syntax errors above to enable live formatting", - "originalSize": "Original size", - "formattedSize": "Formatted size", - "diffMode": "Diff", - "formatMode": "Format", - "yamlMode": "YAML", - "tomlMode": "TOML", - "minifyMode": "Minify", - "yamlTitle": "JSON to YAML", - "yamlSubtitle": "Convert JSON data to YAML format", - "yamlModeInputPlaceholder": "Enter JSON to convert...", - "yamlModeOutputLabel": "YAML Result", - "yamlModeEmptyHint": "Enter JSON and click Convert", - "convertButton": "Convert", - "tomlTitle": "JSON to TOML", - "tomlSubtitle": "Convert JSON data to TOML format", - "tomlModeInputPlaceholder": "Enter JSON to convert...", - "tomlModeOutputLabel": "TOML Result", - "tomlModeEmptyHint": "Enter JSON and click Convert", - "minifyTitle": "JSON Minifier", - "minifySubtitle": "Compress JSON into a compact single-line format", - "minifyModeInputPlaceholder": "Enter JSON to minify...", - "minifyModeOutputLabel": "Minified Result", - "minifyModeEmptyHint": "Enter JSON and click Minify", - "minifyButton": "Minify" -} diff --git a/i18n/locales/en/jwt.json b/i18n/locales/en/jwt.json deleted file mode 100644 index aec555f..0000000 --- a/i18n/locales/en/jwt.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "pageTitle": "JWT Parser", - "pageSubtitle": "JSON Web Token decoding and viewing", - "placeholder": "Paste Encoded JWT here...", - "headerTitle": "HEADER: Algorithm & Token Type", - "payloadTitle": "PAYLOAD: Data", - "signatureTitle": "Signature", - "noSignature": "No Signature", - "invalidFormat": "Unable to parse", - "errors": { - "invalidBase64String": "Invalid base64url string", - "failedToDecode": "Failed to decode base64url: ", - "invalidFormat": "Invalid JWT format: expected 3 parts separated by .", - "parseHeaderFailed": "Failed to parse Header: ", - "parsePayloadFailed": "Failed to parse Payload: " - } -} diff --git a/i18n/locales/en/markdownToHtml.json b/i18n/locales/en/markdownToHtml.json deleted file mode 100644 index e1147bb..0000000 --- a/i18n/locales/en/markdownToHtml.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "pageTitle": "Markdown to HTML", - "pageSubtitle": "Convert Markdown to HTML in real-time with preview", - "splitMode": "Split", - "previewMode": "Preview", - "htmlMode": "HTML", - "clear": "Clear", - "print": "Print", - "download": "Download", - "inputLabel": "Markdown Input", - "previewLabel": "Live Preview", - "htmlOutputLabel": "HTML Output", - "inputPlaceholder": "Enter your Markdown content here...", - "charCount": "{{count}} chars", - "copyHtml": "Copy HTML", - "downloadSuccess": "File downloaded successfully", - "printSuccess": "Print window opened" -} diff --git a/i18n/locales/en/qrCode.json b/i18n/locales/en/qrCode.json deleted file mode 100644 index ac529b9..0000000 --- a/i18n/locales/en/qrCode.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "pageTitle": "QR Code Tools", - "pageSubtitle": "Generate and parse QR codes", - - "generateMode": "Generate QR Code", - "parseMode": "Parse QR Code", - - "urlToQr": "Text to QR Code", - "qrToUrl": "QR Code to Text", - - "urlInputLabel": "Enter URL or Text", - "urlInputPlaceholder": "Enter URL or text content, QR code will be generated automatically", - "generateButton": "Generate QR Code", - "generating": "Generating...", - - "qrCodeWillShow": "QR code will be shown here", - "downloadButton": "Download QR Code", - "copyQrButton": "Copy QR Code Image", - - "qrCodeSuccess": "QR code generated successfully", - "qrCodeDownloadSuccess": "QR code downloaded successfully", - "qrCodeCopySuccess": "QR code copied to clipboard", - - "selectImage": "Please select a QR code image", - "parseSuccess": "QR code parsed successfully", - "noQrDetected": "No QR code detected, please ensure the image is clear and contains a QR code", - "parseError": "Failed to parse QR code, please try again", - "generateError": "Failed to generate QR code, please check the input", - "copyError": "Copy failed, please try again", - - "imagePasted": "Image pasted, parsing...", - "imagePasteError": "Failed to paste image, please try again", - "imageCleared": "Image cleared", - "clickToUpload": "Click, drag, or paste to upload QR code image", - "supportFormats": "Supports PNG, JPG, WEBP, Base64 formats", - - "parseButton": "Parse QR Code", - "parsing": "Parsing...", - "resultLabel": "Parsing Result", - "copyTooltip": "Copy", - - "enterUrlError": "Please enter URL or text", - "clickToChange": "Click to change image", - - "pasteHint": "Supports Ctrl+V to paste images or Base64 strings", - "autoGenerateHint": "QR code will be generated automatically after input", - "autoParseHint": "QR code will be parsed automatically after upload" -} diff --git a/i18n/locales/en/rightClickRestorer.json b/i18n/locales/en/rightClickRestorer.json deleted file mode 100644 index 897a416..0000000 --- a/i18n/locales/en/rightClickRestorer.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "title": "Right Click Restorer", - "description": "Detect and restore disabled browser right-click menus", - "loading": "Loading...", - "currentDomain": "Current Domain", - "statusLocked": "Locked", - "statusUnlocked": "Unlocked", - "unsupported": "Unsupported", - "unsupportedDesc": "The current page is a browser internal or extension page. Right-click unlock is not available. Please switch to a regular webpage.", - "unlockDesc": "Click the button below to temporarily unlock the right-click menu for the current website. You will need to unlock again after refreshing the page.", - "unlockBtn": "Unlock Right Click", - "alreadyUnlocked": "Right Click Unlocked" -} diff --git a/i18n/locales/en/storageCleaner.json b/i18n/locales/en/storageCleaner.json deleted file mode 100644 index 6679b99..0000000 --- a/i18n/locales/en/storageCleaner.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "pageTitle": "Storage Cleaner", - "pageSubtitle": "Clear cache, cookies, and local storage", - "loading": "Loading...", - "initializing": "Reading site data...", - "occupied": "Occupied {{size}}", - "cleaning": "Cleaning...", - "cleanNow": "Clean Now", - "autoRefresh": "Auto refresh page after cleaning", - "selectAll": "Select all items", - "cleanSuccess": "Cleaning complete", - "cleanError": "Cleaning failed", - "noData": "No data", - "countUnit": "items", - "errorNoTab": "Unable to get current tab", - "errorRestricted": "Storage cleaning is not supported on this page", - "cleanSuccessReload": "Cleaning successful, reloading page...", - "errorStandardOnly": "Storage cleaning only works on standard web pages", - "confirmTitle": "Confirm Clear Data?", - "confirmDesc": "You are about to permanently delete the following selected storage items from the current page.", - "irreversible": "This action is irreversible", - "confirmAction": "Confirm Clear", - "cleanedSummary": "Cleaned {{items}}", - "noDataToClean": "No storage data found to clean", - "partialFailure": "Some items failed to clean", - "options": { - "localStorage": "Local Storage", - "sessionStorage": "Session Storage", - "indexedDB": "IndexedDB", - "cookies": "Cookies", - "cacheStorage": "Cache Storage", - "serviceWorkers": "Service Workers" - } -} diff --git a/i18n/locales/en/textStatistics.json b/i18n/locales/en/textStatistics.json deleted file mode 100644 index 7fb8f7d..0000000 --- a/i18n/locales/en/textStatistics.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "pageTitle": "Text Statistics", - "pageSubtitle": "Real-time analysis of characters, words, lines, and byte size", - "placeholder": "Type or paste text here...", - "characters": "Characters", - "words": "Words", - "lines": "Lines", - "bytes": "Byte Size" -} diff --git a/i18n/locales/en/timestamp.json b/i18n/locales/en/timestamp.json deleted file mode 100644 index 2b2d4b8..0000000 --- a/i18n/locales/en/timestamp.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "pageTitle": "Timestamp Conversion", - "pageSubtitle": "Unix millisecond conversion and formatting", - "tsToDate": "Timestamp → Date", - "dateToTs": "Date → Timestamp", - "placeholderTs": "Enter timestamp...", - "placeholderDate": "YYYY-MM-DD HH:mm:ss", - "unitMs": "Millisecond (ms)", - "unitS": "Second (s)", - "convertButton": "Convert Now", - "currentTs": "Current Timestamp", - "useNowTooltip": "Use this value", - "copyTsTooltip": "Copy timestamp", - "usedSuccess": "Using current timestamp", - "resultLabel": "Conversion Result", - "copyResultTooltip": "Copy result", - "relativeTime": "Relative Time", - "iso8601": "ISO 8601", - "utcTime": "UTC Time", - "copyTooltip": "Copy", - "resultEmpty": "Enter a value and click convert", - "errors": { - "invalidNumber": "Invalid number", - "invalidTimestamp": "Invalid timestamp", - "invalidFormat": "Format error" - } -} diff --git a/i18n/locales/zh/base64Converter.json b/i18n/locales/zh/base64Converter.json deleted file mode 100644 index 1b58f73..0000000 --- a/i18n/locales/zh/base64Converter.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "pageTitle": "Base64 转换器", - "pageSubtitle": "文本、文件与图像的 Base64 编码与解码", - "textMode": "文本", - "fileMode": "文件", - "imageMode": "图像", - "encode": "编码", - "decode": "解码", - "clear": "清空", - "textInputPlaceholder": "输入需要编码为 Base64 的文本...", - "base64InputPlaceholder": "输入需要解码的 Base64 字符串...", - "base64Output": "Base64 编码结果", - "textOutput": "解码文本结果", - "copyRaw": "复制纯 Base64", - "copyDataUri": "复制 Data URI", - "clickOrDropToFile": "点击或拖拽文件到此处", - "clickOrDropToImage": "点击或拖拽图像到此处", - "clickOrDropToReplace": "点击或拖拽以替换文件", - "maxFileSize": "最大文件大小:{{max}}", - "supportedFormats": "支持 PNG、JPG、WEBP、GIF、BMP、SVG 等格式", - "fileSizeExceeded": "文件大小超出限制(最大 {{max}})", - "unsupportedImageType": "不支持的图像格式", - "conversionFailed": "转换失败", - "originalSize": "原始大小", - "encodedSize": "编码大小", - "invalidBase64": "Base64 字符串无效", - "binaryDataDetected": "输入似乎是二进制数据(如图片)。请切换到「图像」选项卡。", - "imageDataUriHint": "检测到图片的 data URI,请使用「图像」选项卡进行解码。", - "switchToImageMode": "切换到图像模式", - "download": "下载", - "decodedFileName": "解码后文件名", - "decodeBase64Placeholder": "输入需要解码的 Base64 或 data URI...", - "decodedFileOutput": "解码文件", - "decodedImageOutput": "解码图像", - "inferredMimeType": "推断的 MIME 类型", - "decodedSize": "解码大小" -} diff --git a/i18n/locales/zh/common.json b/i18n/locales/zh/common.json deleted file mode 100644 index ef5dcf3..0000000 --- a/i18n/locales/zh/common.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "appName": "测试工具", - "buttons": { - "save": "保存", - "cancel": "取消", - "confirm": "确认", - "copy": "复制", - "clear": "清理", - "refresh": "刷新", - "toggleLanguage": "切换语言", - "toggleTheme": "切换主题", - "themeMode": { - "light": "切换到深色模式", - "dark": "切换到系统模式", - "system": "切换到浅色模式" - }, - "search": "搜索工具...", - "back": "返回", - "clearSearch": "清除搜索", - "recentSearch": "最近搜索", - "noResults": "未找到相关工具", - "openInTab": "在标签页打开", - "settings": "设置" - }, - "messages": { - "copySuccess": "已复制到剪贴板", - "copyError": "复制失败", - "copyEmpty": "无内容可复制" - }, - "errorBoundary": { - "title": "糟糕,出了点问题", - "description": "应用遇到了一些意外错误。您可以尝试刷新页面或重置应用。", - "refresh": "刷新应用", - "retry": "重新尝试" - }, - "pageErrorBoundary": { - "title": "该功能运行异常", - "description": "该页面在加载或渲染时遇到了内部脚本错误。您可以尝试重试,或者通过导航菜单切换到其他工具。" - }, - "router": { - "notFound": "页面未找到", - "notFoundDescription": "该功能在当前运行环境({{entryPointType}})下不可用或已被移除。" - }, - "textInputArea": { - "clear": "清空", - "copyContent": "复制内容", - "cleared": "已清空", - "placeholder": "请输入文本" - } -} diff --git a/i18n/locales/zh/features.json b/i18n/locales/zh/features.json deleted file mode 100644 index 3cbfe0b..0000000 --- a/i18n/locales/zh/features.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "dashboard": { - "title": "仪表盘" - }, - "timestamp": { - "title": "时间戳", - "description": "Unix 毫秒数转换与格式化" - }, - "storageCleaner": { - "title": "存储清理", - "description": "清理缓存、Cookies 及本地存储" - }, - "qrCode": { - "title": "二维码工具", - "description": "生成当前选中的 URL 的二维码" - }, - "textStatistics": { - "title": "文本统计", - "description": "实时分析文本字符、单词及字节" - }, - "jwt": { - "title": "JWT 解析", - "description": "JSON Web Token 解码与查看" - }, - "jsonDiff": { - "title": "JSON 工具", - "description": "差异比较、格式化、YAML/TOML 转换及压缩" - }, - "base64Converter": { - "title": "Base64 转换器", - "description": "文本、文件与图像的 Base64 编码转换" - }, - "markdownToHtml": { - "title": "Markdown 转 HTML", - "description": "实时 Markdown 转换与 HTML 预览" - }, - "htmlToMarkdown": { - "title": "HTML 转 Markdown", - "description": "实时 HTML 转换与 Markdown 预览" - }, - "rightClickRestorer": { - "title": "右键恢复", - "description": "检测并恢复被网站禁用的浏览器右键菜单" - } -} diff --git a/i18n/locales/zh/htmlToMarkdown.json b/i18n/locales/zh/htmlToMarkdown.json deleted file mode 100644 index 9f1e4ca..0000000 --- a/i18n/locales/zh/htmlToMarkdown.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "pageTitle": "HTML 转 Markdown", - "pageSubtitle": "实时将 HTML 转换为 Markdown 格式", - "splitMode": "分屏", - "previewMode": "预览", - "markdownMode": "Markdown", - "clear": "清空", - "inputLabel": "HTML 输入", - "previewLabel": "Markdown 预览", - "markdownOutputLabel": "Markdown 输出", - "inputPlaceholder": "在此输入 HTML 内容...", - "charCount": "{{count}} 字符", - "copyMarkdown": "复制 Markdown", - "download": "下载", - "emptyHint": "输入 HTML 内容以查看转换结果" -} diff --git a/i18n/locales/zh/jsonDiff.json b/i18n/locales/zh/jsonDiff.json deleted file mode 100644 index bc10e13..0000000 --- a/i18n/locales/zh/jsonDiff.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "pageTitle": "JSON 差异比较", - "pageSubtitle": "对比两个 JSON 数据的差异", - "leftPlaceholder": "输入原始 JSON...", - "rightPlaceholder": "输入目标 JSON...", - "leftLabel": "原始 JSON", - "rightLabel": "目标 JSON", - "compareButton": "比较", - "clearButton": "清空", - "sideBySideMode": "并排", - "unifiedMode": "统一", - "previousDiff": "上一个", - "nextDiff": "下一个", - "noDiffs": "无差异", - "diffCount": "{{count}} 处差异", - "invalidJson": "无效的 JSON 格式", - "emptyHint": "输入两侧 JSON 后点击比较", - "fixErrorHint": "请修正上方 JSON 的语法错误以开启实时流式比对", - "added": "新增", - "removed": "删除", - "modified": "修改" -} diff --git a/i18n/locales/zh/jsonFormat.json b/i18n/locales/zh/jsonFormat.json deleted file mode 100644 index 2f0b584..0000000 --- a/i18n/locales/zh/jsonFormat.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "formatTitle": "JSON 格式化", - "formatSubtitle": "美化和格式化 JSON 数据", - "inputPlaceholder": "输入需要格式化的 JSON...", - "formatButton": "格式化", - "clearButton": "清空", - "sortKeys": "键名排序", - "sortKeysTooltip": "按字母顺序对 JSON 对象键进行排序", - "indentSize": "缩进", - "indentSpaces": "{{count}} 个空格", - "outputLabel": "格式化结果", - "copySuccess": "复制成功", - "copyFail": "复制失败", - "noContent": "无内容可复制", - "invalidJson": "无效的 JSON 格式", - "emptyHint": "输入 JSON 后点击格式化", - "fixErrorHint": "请修正上方 JSON 的语法错误以开启实时流式格式化", - "originalSize": "原始大小", - "formattedSize": "格式化后大小", - "diffMode": "差异比较", - "formatMode": "格式化", - "yamlMode": "YAML", - "tomlMode": "TOML", - "minifyMode": "压缩", - "yamlTitle": "JSON 转 YAML", - "yamlSubtitle": "将 JSON 数据转换为 YAML 格式", - "yamlModeInputPlaceholder": "输入需要转换的 JSON...", - "yamlModeOutputLabel": "YAML 结果", - "yamlModeEmptyHint": "输入 JSON 后点击转换", - "convertButton": "转换", - "tomlTitle": "JSON 转 TOML", - "tomlSubtitle": "将 JSON 数据转换为 TOML 格式", - "tomlModeInputPlaceholder": "输入需要转换的 JSON...", - "tomlModeOutputLabel": "TOML 结果", - "tomlModeEmptyHint": "输入 JSON 后点击转换", - "minifyTitle": "JSON 压缩", - "minifySubtitle": "将 JSON 压缩为紧凑的单行格式", - "minifyModeInputPlaceholder": "输入需要压缩的 JSON...", - "minifyModeOutputLabel": "压缩结果", - "minifyModeEmptyHint": "输入 JSON 后点击压缩", - "minifyButton": "压缩" -} diff --git a/i18n/locales/zh/jwt.json b/i18n/locales/zh/jwt.json deleted file mode 100644 index fed6481..0000000 --- a/i18n/locales/zh/jwt.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "pageTitle": "JWT 解析", - "pageSubtitle": "JSON Web Token 解码与查看", - "placeholder": "在此粘贴 JWT 令牌 (Encoded JWT)...", - "headerTitle": "HEADER: 算法 & 令牌类型", - "payloadTitle": "PAYLOAD: 数据", - "signatureTitle": "签名", - "noSignature": "无签名", - "invalidFormat": "无法解析", - "errors": { - "invalidBase64String": "无效的 Base64URL 字符串", - "failedToDecode": "Base64URL 解码失败:", - "invalidFormat": "JWT 格式错误:必须包含三个由 . 分隔的部分", - "parseHeaderFailed": "解析 Header 失败:", - "parsePayloadFailed": "解析 Payload 失败:" - } -} diff --git a/i18n/locales/zh/markdownToHtml.json b/i18n/locales/zh/markdownToHtml.json deleted file mode 100644 index b754a56..0000000 --- a/i18n/locales/zh/markdownToHtml.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "pageTitle": "Markdown 转 HTML", - "pageSubtitle": "实时将 Markdown 转换为 HTML 并预览", - "splitMode": "分屏", - "previewMode": "预览", - "htmlMode": "HTML", - "clear": "清空", - "print": "打印", - "download": "下载", - "inputLabel": "Markdown 输入", - "previewLabel": "实时预览", - "htmlOutputLabel": "HTML 输出", - "inputPlaceholder": "在此输入 Markdown 内容...", - "charCount": "{{count}} 字符", - "copyHtml": "复制 HTML", - "downloadSuccess": "文件下载成功", - "printSuccess": "打印窗口已打开" -} diff --git a/i18n/locales/zh/qrCode.json b/i18n/locales/zh/qrCode.json deleted file mode 100644 index 4a77e84..0000000 --- a/i18n/locales/zh/qrCode.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "pageTitle": "二维码工具", - "pageSubtitle": "生成和解析二维码", - - "generateMode": "生成二维码", - "parseMode": "解析二维码", - - "urlToQr": "文本转二维码", - "qrToUrl": "二维码转文本", - - "urlInputLabel": "输入 URL 或文本", - "urlInputPlaceholder": "请输入 URL 或文本内容,将自动生成二维码", - "generateButton": "生成二维码", - "generating": "生成中...", - - "qrCodeWillShow": "二维码将显示在这里", - "downloadButton": "下载二维码", - "copyQrButton": "复制二维码", - - "qrCodeSuccess": "二维码生成成功", - "qrCodeDownloadSuccess": "二维码下载成功", - "qrCodeCopySuccess": "二维码已复制到剪贴板", - - "selectImage": "请选择二维码图片", - "parseSuccess": "二维码解析成功", - "noQrDetected": "未检测到二维码,请确保图片清晰且包含二维码", - "parseError": "解析二维码失败,请重试", - "generateError": "生成二维码失败,请检查输入内容", - "copyError": "复制失败,请重试", - - "imagePasted": "图片粘贴成功,正在解析...", - "imagePasteError": "粘贴图片失败,请重试", - "imageCleared": "图片已清除", - "clickToUpload": "点击、拖拽或粘贴上传二维码图片", - "supportFormats": "支持 PNG、JPG、WEBP、Base64 格式", - - "parseButton": "解析二维码", - "parsing": "解析中...", - "resultLabel": "解析结果", - "copyTooltip": "复制", - - "enterUrlError": "请输入 URL 或文本", - "clickToChange": "点击更换图片", - - "pasteHint": "支持 Ctrl+V 粘贴图片或 Base64 字符串", - "autoGenerateHint": "输入内容后将自动生成二维码", - "autoParseHint": "上传图片后将自动解析二维码" -} diff --git a/i18n/locales/zh/rightClickRestorer.json b/i18n/locales/zh/rightClickRestorer.json deleted file mode 100644 index 4c6bd34..0000000 --- a/i18n/locales/zh/rightClickRestorer.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "title": "右键恢复", - "description": "检测并恢复被网站禁用的浏览器右键菜单", - "loading": "正在加载...", - "currentDomain": "当前域名", - "statusLocked": "未解锁", - "statusUnlocked": "已解锁", - "unsupported": "不支持", - "unsupportedDesc": "当前页面为浏览器内部页面或扩展页面,无法解锁右键功能。请切换到普通网页后重试。", - "unlockDesc": "点击下方按钮,为当前网站临时解锁右键菜单。刷新页面后需要重新解锁。", - "unlockBtn": "解锁当前网站右键", - "alreadyUnlocked": "右键已解锁" -} diff --git a/i18n/locales/zh/storageCleaner.json b/i18n/locales/zh/storageCleaner.json deleted file mode 100644 index 6265097..0000000 --- a/i18n/locales/zh/storageCleaner.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "pageTitle": "存储清理", - "pageSubtitle": "清理缓存、Cookies 及本地存储", - "loading": "加载中...", - "initializing": "正在读取站点数据...", - "occupied": "已占用 {{size}}", - "cleaning": "正在清理...", - "cleanNow": "立即清理", - "autoRefresh": "清理后自动刷新页面", - "selectAll": "全选所有项", - "cleanSuccess": "清理完成", - "cleanError": "清理失败", - "noData": "无数据", - "countUnit": "个", - "errorNoTab": "无法获取当前标签页", - "errorRestricted": "存储清理功能不支持此页面", - "cleanSuccessReload": "清理成功,即将刷新页面", - "errorStandardOnly": "存储清理功能仅适用于标准网页", - "confirmTitle": "确认清理数据?", - "confirmDesc": "您将永久删除当前页面的以下选定存储项。", - "irreversible": "此操作不可撤销", - "confirmAction": "确认清理", - "cleanedSummary": "清理了 {{items}}", - "noDataToClean": "该页面没有可清理的存储数据", - "partialFailure": "部分清理失败", - "options": { - "localStorage": "Local Storage", - "sessionStorage": "Session Storage", - "indexedDB": "IndexedDB", - "cookies": "Cookies", - "cacheStorage": "Cache Storage", - "serviceWorkers": "Service Workers" - } -} diff --git a/i18n/locales/zh/textStatistics.json b/i18n/locales/zh/textStatistics.json deleted file mode 100644 index e8efba4..0000000 --- a/i18n/locales/zh/textStatistics.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "pageTitle": "文本统计", - "pageSubtitle": "实时分析文本的字符、单词、行数及字节大小", - "placeholder": "在此输入或粘贴文本...", - "characters": "字符数", - "words": "单词数", - "lines": "行数", - "bytes": "字节大小" -} diff --git a/i18n/locales/zh/timestamp.json b/i18n/locales/zh/timestamp.json deleted file mode 100644 index e99b245..0000000 --- a/i18n/locales/zh/timestamp.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "pageTitle": "时间戳转换", - "pageSubtitle": "Unix 毫秒数转换与格式化", - "tsToDate": "时间戳 → 日期", - "dateToTs": "日期 → 时间戳", - "placeholderTs": "输入时间戳...", - "placeholderDate": "YYYY-MM-DD HH:mm:ss", - "unitMs": "毫秒 (ms)", - "unitS": "秒 (s)", - "convertButton": "立即转换", - "currentTs": "当前时间戳", - "useNowTooltip": "填充到下方", - "copyTsTooltip": "复制时间戳", - "usedSuccess": "已使用当前时间戳", - "resultLabel": "转换结果", - "copyResultTooltip": "复制结果", - "relativeTime": "相对时间", - "iso8601": "ISO 8601", - "utcTime": "UTC 时间", - "copyTooltip": "复制", - "resultEmpty": "请输入并点击转换", - "errors": { - "invalidNumber": "无效数字", - "invalidTimestamp": "无效时间戳", - "invalidFormat": "格式错误" - } -} diff --git a/package-lock.json b/package-lock.json index 73195ad..af81474 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,15 +25,11 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "dayjs": "^1.11.20", - "i18next": "^26.2.0", - "i18next-browser-languagedetector": "^8.2.1", "lucide-react": "^1.16.0", - "marked": "^18.0.4", "qr-scanner": "^1.4.2", "qrious": "^4.0.2", "react": "^19.2.6", "react-dom": "^19.2.6", - "react-i18next": "^17.0.8", "sonner": "^2.0.7", "tailwind-merge": "^3.6.0" }, @@ -6706,15 +6702,6 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "license": "MIT" }, - "node_modules/html-parse-stringify": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", - "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", - "license": "MIT", - "dependencies": { - "void-elements": "3.1.0" - } - }, "node_modules/htmlparser2": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", @@ -6764,43 +6751,6 @@ "url": "https://github.com/sponsors/typicode" } }, - "node_modules/i18next": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.2.0.tgz", - "integrity": "sha512-zwBHldHdTmwN7r6UNc7lC6GWNN+YYg3DrRSeHR5PRRBf5QnJZcYHrQc0uaU26qZeYxR7iFZD+Y315dPnKP47wA==", - "funding": [ - { - "type": "individual", - "url": "https://www.locize.com/i18next" - }, - { - "type": "individual", - "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" - }, - { - "type": "individual", - "url": "https://www.locize.com" - } - ], - "license": "MIT", - "peerDependencies": { - "typescript": "^5 || ^6" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/i18next-browser-languagedetector": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.1.tgz", - "integrity": "sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.2" - } - }, "node_modules/ignore": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", @@ -8513,18 +8463,6 @@ "url": "https://github.com/sponsors/fregante" } }, - "node_modules/marked": { - "version": "18.0.4", - "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.4.tgz", - "integrity": "sha512-c/BTaKzg0G6ezQx97DAkYU7k0HM6ys0FqYeKBL6hlBByZwy+ycA1+f0vDdjMHKKeEjdgkx0GOv9Il6D+85cOqA==", - "license": "MIT", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 20" - } - }, "node_modules/marky": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz", @@ -9887,33 +9825,6 @@ "react": "^19.2.6" } }, - "node_modules/react-i18next": { - "version": "17.0.8", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.8.tgz", - "integrity": "sha512-0ooKbGLU8JXhe1zwpQUWIeXSgLPOfwJmgheWRIUpcoA0CpyabpGhayjdG+/eA5esC1AQ8h2jWpXjJfzQzeDOCw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.29.2", - "html-parse-stringify": "^3.0.1", - "use-sync-external-store": "^1.6.0" - }, - "peerDependencies": { - "i18next": ">= 26.2.0", - "react": ">= 16.8.0", - "typescript": "^5 || ^6" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, "node_modules/react-remove-scroll": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", @@ -11828,15 +11739,6 @@ } } }, - "node_modules/use-sync-external-store": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", - "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -12046,15 +11948,6 @@ } } }, - "node_modules/void-elements": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", - "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", diff --git a/package.json b/package.json index cb87e44..07dd898 100644 --- a/package.json +++ b/package.json @@ -36,15 +36,11 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "dayjs": "^1.11.20", - "i18next": "^26.2.0", - "i18next-browser-languagedetector": "^8.2.1", "lucide-react": "^1.16.0", - "marked": "^18.0.4", "qr-scanner": "^1.4.2", "qrious": "^4.0.2", "react": "^19.2.6", "react-dom": "^19.2.6", - "react-i18next": "^17.0.8", "sonner": "^2.0.7", "tailwind-merge": "^3.6.0" }, diff --git a/pages/Dashboard/index.tsx b/pages/Dashboard/index.tsx deleted file mode 100644 index 8479dda..0000000 --- a/pages/Dashboard/index.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { useRouter } from '@/providers/RouterProvider'; -import ToolCard from '@/pages/Dashboard/ToolCard'; -import { getFeatureByKey } from '@/config/features'; -import type { PageType } from '@/types/storage'; -import { useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { cn } from '@/lib/utils'; - -export default function DashboardPage() { - const { navigateTo, visiblePages, pageOrder } = useRouter(); - const { t } = useTranslation(['features']); - - const visibleSet = useMemo(() => new Set(visiblePages), [visiblePages]); - - return ( -
- {pageOrder.map((key) => { - if (!visibleSet.has(key)) return null; - - const feature = getFeatureByKey(key); - if (!feature?.themeColorKey || feature.icon == null) return null; - - return ( - navigateTo(key as PageType)} - /> - ); - })} -
- ); -} diff --git a/pages/HtmlToMarkdown/index.tsx b/pages/HtmlToMarkdown/index.tsx deleted file mode 100644 index f860989..0000000 --- a/pages/HtmlToMarkdown/index.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import { useCallback, useMemo, useState } from 'react'; -import { Download, Trash2 } from 'lucide-react'; -import { useLazyTranslation } from '@/utils/useLazyTranslation'; -import CopyButton from '@/components/CopyButton'; -import SwitchButtonGroup from '@/components/SwitchButtonGroup'; -import { Button } from '@/components/ui/button'; // 💡 1. 全面回归规范:引入原生的 shadcn 原子 Button -import { useStorageState } from '@/utils/useStorageState'; -import type { HtmlToMarkdownPreviewMode } from '@/types/storage'; -import { downloadMarkdownFile, htmlToMarkdown, SAMPLE_HTML } from '@/utils/htmlToMarkdown'; -import { cn } from '@/lib/utils'; - -const isValidPreviewMode = (val: unknown): val is HtmlToMarkdownPreviewMode => - typeof val === 'string' && ['split', 'preview', 'markdown'].includes(val); - -export default function HtmlToMarkdownPage() { - const { t } = useLazyTranslation('htmlToMarkdown'); - const [previewMode, setPreviewMode] = useStorageState( - 'htmlToMarkdown/previewMode', - 'split' as HtmlToMarkdownPreviewMode, - isValidPreviewMode, - ); - const [html, setHtml] = useState(SAMPLE_HTML); - - const result = useMemo(() => htmlToMarkdown(html), [html]); - const error = result.hasError ? (result.error ?? null) : null; - - const handleModeChange = useCallback( - (newMode: HtmlToMarkdownPreviewMode) => { - setPreviewMode(newMode); - }, - [setPreviewMode], - ); - - const handleClear = useCallback(() => { - setHtml(''); - }, []); - - const handleDownload = useCallback(() => { - if (result.markdown) { - downloadMarkdownFile(result.markdown, 'converted.md'); - } - }, [result.markdown]); - - const showInput = previewMode !== 'preview'; - const showOutput = previewMode !== 'markdown'; - - return ( - /* 💡 统一间距尺寸: - - 彻底清除多余的 container max-w-7xl 这种网页大边距, - - 统一收拢为我们先前在 Dashboard 页、JSON 工具箱制定的 p-4 space-y-4 标准极客桌面规格。 - */ -
-
- {/* 工具栏集成区 */} -
- - -
- {/* 2. 重塑下载按钮:接入受控 Button,追加 active 物理微缩放动效 */} - - - {/* 重塑清空按钮 */} - -
-
- - {/* 错误提示: - - 💡 核心修复点:将硬编码的 bg-red-50 实色,完美超进化为系统的全自适应透明色变体 - */} - {error && ( -
- {error} -
- )} - - {/* 双翼/单栏联动面板展示区 */} -
- {/* HTML 输入端卡片面板 */} - {showInput && ( - /* 3. 智能聚焦框联动(Focus Ring Clamping): - - 外层容器追加 focus-within 变量追踪大闸。 - - 只要用户用鼠标点击了内部的 textarea,外层整块精巧的圆角大边框会一帧内亮起 primary 系统的深色呼吸发光环, - - 这种“全外包裹层框聚焦”的体验极大模仿了本地原生 IDE 的硬核专业体验! - */ -
- {/* 卡片头部:改用标准的灰色 bg-muted/50 */} -
- - {t('inputLabel')} - - - {t('charCount', { count: html.length })} - -
-