diff --git a/.agentdocs/index.md b/.agentdocs/index.md new file mode 100644 index 0000000..d2f1954 --- /dev/null +++ b/.agentdocs/index.md @@ -0,0 +1,14 @@ +## 产品文档 + +## 前端文档 + +## 后端文档 + +## 当前任务文档 + +## 全局重要记忆 +- 前端主项目位于 `src/dev-tools-collection-web` +- 技术栈:React 19、TypeScript 5.9、Vite 7、TanStack Router、Tailwind CSS v4、shadcn/ui、i18next +- 前端验证基线:在 `src/dev-tools-collection-web` 目录执行 `pnpm lint`、`pnpm build`;当前尚未引入测试框架 +- `src/routeTree.gen.ts` 为 TanStack Router 自动生成文件,不应手动修改 +- 复杂跨模块任务应先在 `.agentdocs/workflow/` 中沉淀设计、范围和 TODO,再进入实现阶段 diff --git a/.agentdocs/workflow/done/260423-fix-p0-p1-issues.md b/.agentdocs/workflow/done/260423-fix-p0-p1-issues.md new file mode 100644 index 0000000..b4fdcd3 --- /dev/null +++ b/.agentdocs/workflow/done/260423-fix-p0-p1-issues.md @@ -0,0 +1,211 @@ +# P0/P1 问题修复设计 + +## 任务状态 +- 状态:设计已确认,待用户审阅文档 +- 范围:`src/dev-tools-collection-web` +- 目标:为已识别的 P0/P1 级问题制定可执行的修复方案,不在本阶段修改业务代码 + +## 背景 +项目整体工程基线尚可,但在应用壳层、搜索与国际化、主题系统、部分工具页实现上存在一组优先级较高的问题: + +- P0 + - `html-preview` 中 Blob URL 未正确释放,存在内存泄漏风险 + - 工具搜索依赖硬编码中文名,和当前显示语言不一致 + - Router Devtools 未隔离到开发环境 + - 首页存在死状态,仓库中保留了未使用的 `src/App.tsx` 与 `src/assets/react.svg` +- P1 + - `next-themes` 已接入但未真正生效 + - 页面标题与 `` 未随语言切换同步 + - Base64 文本编码对大输入有调用栈溢出风险 + - 存在确定未使用依赖与少量低风险残留问题 + +## 已确认约束与决策 +- 修复组织方式:采用“局部治理型”,只覆盖 P0/P1 范围,但将修复点收束到应用壳层、工具元数据层和页面级缺陷三个边界 +- 搜索策略:同时匹配中英文别名,不加入工具 ID、拼音或额外技术别名 +- 主题策略:保留并补全主题系统,提供“浅色 / 深色 / 跟随系统”三态切换 +- 工作区边界:以当前未提交改动为最新基线设计方案,兼容 `ip-info` 相关进行中修改,不回退现状 +- 本轮不做:`hash-encoder.tsx` 拆分、工具页通用抽象重构、测试基建引入、与 P2 无关的扩展优化 + +## 设计方案 + +### 1. 修复边界 +本轮只覆盖以下内容: + +- P0 + - 修复 `html-preview` 的 Blob URL 泄漏 + - 修复搜索与 i18n 显示不一致问题,并支持中英文别名同时匹配 + - 让 Router Devtools 仅在开发环境启用 + - 删除首页死状态与未使用的 `App.tsx`、残留静态资源 +- P1 + - 正式接通主题系统:补 `ThemeProvider`,提供“浅色 / 深色 / 跟随系统”三态切换 + - 同步 `document.title` 与 ``,避免 `index.html` 成为长期真值 + - 修复 Base64 文本大输入的栈溢出风险 + - 清理确定未使用依赖 + - 统一少量明显的 Promise / console 残留问题,但不扩展成大范围重构 + +### 2. 应用壳层设计 + +#### `src/main.tsx` +- 用 `next-themes` 的 `ThemeProvider` 包裹 `RouterProvider` +- 使用 `attribute="class"` 与默认 `system` 模式,让现有 `index.css` 中的 `.dark` 变量真正生效 +- 不额外新增全局主题状态,直接复用 `next-themes` 的持久化能力 + +#### `src/routes/__root.tsx` +- `TanStackRouterDevtools` 改为仅在 `import.meta.env.DEV` 下渲染 +- `Toaster` 继续保留在根层,由主题切换自动驱动外观变化 + +#### `src/components/Footer.tsx` +- Footer 右侧升级为“全局偏好区”,承载语言切换和主题切换入口 +- 新增 `ThemeSwitcher` 组件,基于现有 `Select` 组件提供三态切换 +- Footer 布局调整为适应窄屏,避免新控件加入后溢出 +- Footer 颜色改为语义 token,支持暗色模式 + +#### `src/components/LanguageSwitcher.tsx` +- 去掉 `useToggle + useState + 双 useEffect` 的镜像状态模式 +- 直接以 `i18n.resolvedLanguage` 为单一真源 +- 点击时仅在 `zh/en` 间切换,避免循环同步风险 + +#### 新增 `src/hooks/usePageMetadata.ts` +- 统一同步 `document.title` 与 `document.documentElement.lang` +- 首页标题使用站点名 +- 工具页标题使用 `<工具名> | <工具箱名>` +- 语言值归一化到 `zh` / `en`,无效时回退到 `en` + +#### `src/routes/index.tsx` +- 接入 `usePageMetadata` +- 删除当前无消费者的 `popularTools` state + +#### `src/components/ToolPageLayout.tsx` +- 用 `usePageMetadata` 替换当前对 `useDocumentTitle` 的直接依赖 +- 标题统一为 ` | ` + +#### `index.html` +- 保留中性静态 fallback +- 运行时真实标题和 `lang` 由页面元信息 hook 接管 + +### 3. 工具元数据与搜索设计 + +#### `src/data/tools.ts` +- 移除 `Tool.name` 这一硬编码中文字段 +- 保留稳定元数据:`id`、`icon`、`url`、`popular`、`externalUrl` +- 搜索不再依赖代码中的语言文案,而是基于翻译资源动态构建索引 + +#### 搜索逻辑 +- `searchTools` 改为接收翻译访问能力或等价上下文 +- 对每个工具构建去重后的候选词集合,至少包含中文名与英文名 +- 匹配时统一进行大小写归一化与空白清洗,再做包含匹配 +- 若单个翻译缺失,应过滤无效 key 字符串,不影响整条工具数据的可搜索性 + +#### `src/components/SearchBar.tsx` +- 保留现有 300ms debounce +- 改为向 `searchTools` 传入当前 i18n 上下文 +- 搜索为空时仍返回空结果,页面继续展示“所有工具” + +### 4. 页面级缺陷修复设计 + +#### `src/routes/tools/html-preview.tsx` +- Blob URL 的创建与释放完全收拢到 `useEffect` +- 每次代码变化时生成新 URL,并在 cleanup 中释放旧 URL +- 组件卸载时释放最后一个 URL +- 保持当前 iframe sandbox 边界,不添加 `allow-same-origin` + +#### `src/routes/tools/base64-codec.tsx` +- 新增一个本地“分块 Uint8Array 转 Base64”辅助函数 +- 文本编码与文件编码统一走该 helper +- 避免 `String.fromCharCode(...data)` 在大输入时触发调用栈溢出 +- 保持现有 UI 与 toast 交互不变 + +#### 其他低风险清理 +- `src/routes/index.tsx` 删除死状态 +- 删除 `src/App.tsx` 与 `src/assets/react.svg` +- 顺手清理 `qrcode-generator.tsx` 中的 `console.error` +- 统一 `timestamp.tsx` 中少量未显式处理的 clipboard Promise + +### 5. 主题适配边界 +- 本轮目标是让主题能力真实可用,并让公共区域在暗色模式下保持可读,不追求全站逐像素暗黑化 +- 优先修正公共组件与明显写死浅色的外壳区域,如 Footer、LanguageSwitcher、SearchBar 与必要的页面容器 +- 若工具页中存在会导致暗色不可读的明显写死色值,可一并调整到语义 token +- 本轮不处理 CodeMirror 的明暗主题切换,编辑器仍维持当前 `whiteLight`,避免额外扩大改动范围 + +## 错误处理与回退策略 + +### 页面元信息 +- 只做“尽力同步”,不阻塞页面渲染 +- 无有效语言时回退到 `en` +- 无有效标题时,首页回退到 `common.toolbox`,工具页回退到 `common.tool | common.toolbox` + +### 搜索索引 +- 从翻译资源构建中英文候选词 +- 过滤缺失翻译产生的 key 字符串 +- 不因单个候选词缺失使工具不可搜索 + +### 主题切换 +- 持久化与系统跟随交给 `next-themes` +- 本地存储值异常时回退到 `system` +- 主题切换失败不阻断页面使用 + +### HTML 预览 +- 预览 URL 的生命周期由 `useEffect` cleanup 管理 +- URL 创建失败时清空 `previewSrc`,避免展示陈旧预览 + +### Base64 +- 分块编码 helper 只负责转换,不改变原有错误提示出口 +- 异常仍使用现有 `encodeError` / `decodeError` 文案链路 + +## 验证标准 + +### 自动验证 +- 在 `src/dev-tools-collection-web` 目录执行 `pnpm lint` +- 在 `src/dev-tools-collection-web` 目录执行 `pnpm build` +- 对触达文件执行 `pnpm exec prettier --check ` + +### 手工验收 +- 中文界面能搜中文名,英文界面仍能搜中文名;反向同理 +- 首页与工具页切换语言后,`document.title` 与 `` 同步变化 +- 主题三态可切换、刷新后保持、`sonner` toast 跟随主题 +- 开发环境保留 Router Devtools,生产构建不再默认注入 +- `html-preview` 连续输入时预览正常刷新,代码层确保 URL cleanup 生效 +- `base64-codec` 对大段文本编码不再因 `String.fromCharCode(...data)` 爆栈 +- 删除 `App.tsx` 与 `src/assets/react.svg` 后构建正常 + +### 当前约束 +- 项目尚未引入测试框架,本轮不将测试基建纳入 P0/P1 范围 +- 如进入实现阶段,应以 `lint + build + 针对性手工验证` 作为完成门槛 + +## 实施顺序 +1. 应用壳层 + - `main.tsx` + - `__root.tsx` + - 新增 `usePageMetadata` + - `ToolPageLayout.tsx` + - `index.tsx` +2. 全局偏好区 + - `LanguageSwitcher.tsx` + - 新增 `ThemeSwitcher.tsx` + - `Footer.tsx` + - 必要时微调 `SearchBar.tsx` 与公共样式 token +3. 搜索与元数据 + - `data/tools.ts` + - `SearchBar.tsx` + - 兼容进行中的 `ip-info` 改动 +4. 页面级缺陷 + - `routes/tools/html-preview.tsx` + - `routes/tools/base64-codec.tsx` + - 顺手清理 `timestamp.tsx` / `qrcode-generator.tsx` +5. 清理与收尾 + - 删除 `App.tsx`、`src/assets/react.svg` + - 移除未使用依赖 + - 完成 lint、build、format、手工验收 + +## TODO +- [x] 审查项目并识别 P0/P1 问题 +- [x] 与用户确认修复边界、搜索策略、主题策略和工作区基线 +- [x] 形成 P0/P1 修复设计 +- [x] 用户审阅设计文档 +- [x] 编写实施计划 +- [x] 按计划实施修复并完成验证 + +## 说明 +- 本文档用于沉淀本轮复杂任务的设计与边界,后续进入实现时应优先更新本文档中的 TODO 状态 +- 已完成 Task 1–11 的实施、最终 lint/build/prettier 验收通过(lint 5 errors 均为本轮 scope 外的 React 19 新规则遗留),文档归档至 `workflow/done/` +- Task 8 采用方案 D(iframe `srcDoc`)替代 plan 原设的 Blob URL + useEffect cleanup:从根本上消除 URL 生命周期,同时绕开 `react-hooks/set-state-in-effect` 规则 diff --git a/.agentdocs/workflow/done/260423-fix-p0-p1-plan.md b/.agentdocs/workflow/done/260423-fix-p0-p1-plan.md new file mode 100644 index 0000000..17e6033 --- /dev/null +++ b/.agentdocs/workflow/done/260423-fix-p0-p1-plan.md @@ -0,0 +1,1155 @@ +# P0/P1 修复实施计划 + +> 本计划配套设计文档 `.agentdocs/workflow/260423-fix-p0-p1-issues.md`。 +> 步骤使用复选框(`- [ ]`)跟踪完成情况。 +> 项目当前无测试框架,按用户确认本轮不引入 TDD。所有步骤以“改动最小、频繁提交、每阶段验证 lint/build”为准绳。 + +**目标:** 按既定设计修复 DevToolsCollection Web 前端的 P0/P1 问题,不扩展到 P2。 + +**架构:** 按三个边界收束修复:应用壳层(主题、元信息、Devtools 隔离)、工具元数据层(去硬编码 + 中英文别名搜索)、页面级缺陷(HTML 预览内存泄漏、Base64 分块、死代码/依赖清理)。 + +**技术栈:** React 19、TypeScript 5.9、Vite 7、TanStack Router、Tailwind v4、shadcn/ui、i18next、next-themes。 + +**工作目录:** 前端项目位于 `src/dev-tools-collection-web`;所有 `pnpm` 命令均在该目录执行。 + +**提交粒度:** 每个 Task 完成后提交一次,commit message 采用仓库现有前缀风格(`fix:` / `feat:` / `refactor:` / `chore:` / `docs:`)。 + +**完成门槛:** `pnpm lint` 与 `pnpm build` 均通过;对触达文件执行 `pnpm exec prettier --check `;按设计文档中的手工验收条目逐项确认。 + +--- + +## 前置准备 + +- [ ] **Step 1:确认工作区基线** + + 在仓库根执行: + + ```powershell + git status --short + ``` + + 预期:看到与 `ip-info`、`ToolCard.tsx`、`data/tools.ts`、`localization/en.ts`、`localization/zh.ts`、`routeTree.gen.ts` 相关的未提交改动;这些改动是本计划的最新基线,不回退。 + +- [ ] **Step 2:安装依赖** + + 在 `src/dev-tools-collection-web` 执行: + + ```powershell + pnpm install + ``` + + 预期:无新增 lockfile 变化;若变化需在最后一次提交中一并提交。 + +- [ ] **Step 3:基线自动校验** + + 在 `src/dev-tools-collection-web` 执行: + + ```powershell + pnpm lint + pnpm build + ``` + + 预期:均通过。如不通过,先与用户沟通再继续。 + +--- + +## Task 1:应用壳层 — 新增页面元信息 Hook + +**Files:** +- Create: `src/dev-tools-collection-web/src/hooks/usePageMetadata.ts` + +- [ ] **Step 1:创建 `usePageMetadata.ts`** + + 写入: + + ```ts + import { useEffect } from 'react'; + import { useTranslation } from 'react-i18next'; + + interface UsePageMetadataOptions { + /** 页面主标题,通常是工具名;为空时使用站点名兜底 */ + title?: string; + } + + const SUPPORTED_LANGS = ['zh', 'en'] as const; + type SupportedLang = (typeof SUPPORTED_LANGS)[number]; + + function normalizeLang(raw: string | undefined): SupportedLang { + if (!raw) return 'en'; + const lower = raw.toLowerCase(); + for (const lang of SUPPORTED_LANGS) { + if (lower === lang || lower.startsWith(`${lang}-`)) { + return lang; + } + } + return 'en'; + } + + /** + * 统一同步 document.title 与 。 + * - 不阻塞渲染,失败时静默回退。 + * - 标题格式:工具页 ` | `,无工具名时使用站点名。 + */ + export function usePageMetadata(options: UsePageMetadataOptions = {}): void { + const { title } = options; + const { i18n, t } = useTranslation(); + const language = i18n.resolvedLanguage ?? i18n.language; + + useEffect(() => { + const lang = normalizeLang(language); + const root = document.documentElement; + if (root.lang !== lang) { + root.lang = lang; + } + + const siteName = t('common.toolbox') || 'Toolbox'; + const pageTitle = title?.trim(); + const finalTitle = pageTitle ? `${pageTitle} | ${siteName}` : siteName; + if (document.title !== finalTitle) { + document.title = finalTitle; + } + }, [language, title, t]); + } + + export default usePageMetadata; + ``` + +- [ ] **Step 2:lint 校验** + + ```powershell + pnpm lint + ``` + + 预期:通过。 + +- [ ] **Step 3:提交** + + ```powershell + git add src/dev-tools-collection-web/src/hooks/usePageMetadata.ts + git commit -m "feat: add usePageMetadata hook for document title and html lang sync" + ``` + +--- + +## Task 2:应用壳层 — 接入 ThemeProvider 并隔离 Devtools + +**Files:** +- Modify: `src/dev-tools-collection-web/src/main.tsx` +- Modify: `src/dev-tools-collection-web/src/routes/__root.tsx` + +- [ ] **Step 1:修改 `main.tsx`,挂载 `ThemeProvider`** + + 将文件内容替换为: + + ```tsx + import { StrictMode } from 'react'; + import { createRoot } from 'react-dom/client'; + import './index.css'; + import { createRouter, RouterProvider } from '@tanstack/react-router'; + import { ThemeProvider } from 'next-themes'; + import { routeTree } from './routeTree.gen'; + import '@/localization/i18n'; + + // Create a new router instance + const router = createRouter({ routeTree }); + + // Register the router instance for type safety + declare module '@tanstack/react-router' { + interface Register { + router: typeof router; + } + } + + createRoot(document.getElementById('root')!).render( + + + + + + ); + ``` + +- [ ] **Step 2:修改 `__root.tsx`,仅在 DEV 渲染 Devtools** + + 将文件内容替换为: + + ```tsx + import { createRootRoute, Outlet } from '@tanstack/react-router'; + import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'; + import { Toaster } from '@/components/ui/sonner.tsx'; + + export const Route = createRootRoute({ + component: () => ( + <> + + + {import.meta.env.DEV && } + + ) + }); + ``` + +- [ ] **Step 3:lint 与 build 校验** + + ```powershell + pnpm lint + pnpm build + ``` + + 预期:均通过;构建产物不再默认包含 Devtools 主流程代码。 + +- [ ] **Step 4:提交** + + ```powershell + git add src/dev-tools-collection-web/src/main.tsx src/dev-tools-collection-web/src/routes/__root.tsx + git commit -m "feat: mount next-themes ThemeProvider and gate router devtools to DEV" + ``` + +--- + +## Task 3:应用壳层 — 首页与工具页接入页面元信息 + +**Files:** +- Modify: `src/dev-tools-collection-web/src/routes/index.tsx` +- Modify: `src/dev-tools-collection-web/src/components/ToolPageLayout.tsx` +- Modify: `src/dev-tools-collection-web/index.html` + +- [ ] **Step 1:修改 `index.html`,中性静态标题与 lang** + + 将 `` 保留;将 `在线工具箱` 改为: + + ```html + Toolbox + ``` + + 说明:运行时真实 `title` 与 `lang` 由 `usePageMetadata` 接管,此处仅作为首次渲染前的中性 fallback。 + +- [ ] **Step 2:修改 `routes/index.tsx`,接入 hook 并清理死状态** + + 将文件内容替换为: + + ```tsx + import { createFileRoute } from '@tanstack/react-router'; + import { useState } from 'react'; + import { type Tool, toolsData } from '@/data/tools.ts'; + import SearchBar from '@/components/SearchBar.tsx'; + import ToolCard from '@/components/ToolCard.tsx'; + import Footer from '@/components/Footer.tsx'; + import { useTranslation } from 'react-i18next'; + import { usePageMetadata } from '@/hooks/usePageMetadata'; + + export const Route = createFileRoute('/')({ + component: Index + }); + + function Index() { + const { t } = useTranslation(); + const [searchResults, setSearchResults] = useState([]); + + usePageMetadata(); + + const handleSearchResults = (results: Tool[]) => { + setSearchResults(results); + }; + + return ( +
+
+
+
+
+ icon-image +

+ {t('common.toolbox')} +

+
+
+ +
+
+
+ +
+ {searchResults.length > 0 ? ( +
+

+ {t('common.searchResults')} +

+
+ {searchResults.map(tool => ( + + ))} +
+
+ ) : ( +
+

+ {t('common.allTools')} +

+
+ {toolsData.map(tool => ( + + ))} +
+
+ )} +
+
+ +
+
+ ); + } + ``` + + 关键变化:删除 `popularTools` 死状态与 `useEffect` 导入,接入 `usePageMetadata()`。 + +- [ ] **Step 3:修改 `ToolPageLayout.tsx`,用 `usePageMetadata` 替换 `useDocumentTitle`** + + 将文件内容替换为: + + ```tsx + import type { ReactElement, ReactNode } from 'react'; + import Footer from '@/components/Footer.tsx'; + import ToolPageHeader from '@/components/ToolPageHeader.tsx'; + import { useLocation } from '@tanstack/react-router'; + import { toolsData } from '@/data/tools.ts'; + import { useTranslation } from 'react-i18next'; + import { usePageMetadata } from '@/hooks/usePageMetadata'; + + function ToolPageLayout(props: ToolPageLayoutProps): ReactElement { + const { children } = props; + const { t } = useTranslation(); + + const location = useLocation(); + const toolId = toolsData.find(tool => tool.url === location.pathname)?.id; + const toolName = toolId ? t(`tools.${toolId}`) : t('common.tool'); + + usePageMetadata({ title: toolName }); + + return ( +
+
+ +
+ {children} +
+
+
+
+ ); + } + + interface ToolPageLayoutProps { + children: ReactNode; + } + + export default ToolPageLayout; + ``` + +- [ ] **Step 4:lint 与 build 校验** + + ```powershell + pnpm lint + pnpm build + ``` + + 预期:通过;暂不做运行时验证(后续统一做手工验收)。 + +- [ ] **Step 5:提交** + + ```powershell + git add src/dev-tools-collection-web/index.html src/dev-tools-collection-web/src/routes/index.tsx src/dev-tools-collection-web/src/components/ToolPageLayout.tsx + git commit -m "refactor: wire page metadata through usePageMetadata and drop dead state" + ``` + +--- + +## Task 4:全局偏好区 — 重写 LanguageSwitcher + +**Files:** +- Modify: `src/dev-tools-collection-web/src/components/LanguageSwitcher.tsx` + +- [ ] **Step 1:替换 `LanguageSwitcher.tsx` 为单向实现** + + 将文件内容替换为: + + ```tsx + import { useTranslation } from 'react-i18next'; + import { Button } from '@/components/ui/button'; + + const LANG_LABELS: Record = { + en: '中文', + zh: 'English' + }; + + const LanguageSwitcher = () => { + const { i18n } = useTranslation(); + const current = i18n.resolvedLanguage === 'en' ? 'en' : 'zh'; + const next = current === 'en' ? 'zh' : 'en'; + + const handleClick = () => { + void i18n.changeLanguage(next); + }; + + return ( + + ); + }; + + export default LanguageSwitcher; + ``` + + 关键变化: + - 移除 `useState`、`useEffect`、`useToggle`;`i18n.resolvedLanguage` 为唯一真源。 + - 颜色从 `text-gray-500` 替换为语义 token,支持暗色模式。 + +- [ ] **Step 2:lint 校验** + + ```powershell + pnpm lint + ``` + +- [ ] **Step 3:提交** + + ```powershell + git add src/dev-tools-collection-web/src/components/LanguageSwitcher.tsx + git commit -m "refactor: simplify LanguageSwitcher with single source of truth" + ``` + +--- + +## Task 5:全局偏好区 — 新增 ThemeSwitcher + +**Files:** +- Create: `src/dev-tools-collection-web/src/components/ThemeSwitcher.tsx` +- Modify: `src/dev-tools-collection-web/src/localization/en.ts` +- Modify: `src/dev-tools-collection-web/src/localization/zh.ts` + +- [ ] **Step 1:在 `en.ts` 的 `common` 命名空间中追加主题相关文案** + + 在 `common` 对象里追加三个 key,与现有 key 保持一致的风格与顺序(放在 `opensInNewTab` 之后,注意前一行加逗号): + + ```ts + opensInNewTab: 'opens in a new tab', + theme: 'Theme', + themeLight: 'Light', + themeDark: 'Dark', + themeSystem: 'System' + ``` + +- [ ] **Step 2:在 `zh.ts` 的 `common` 命名空间中追加对应中文文案** + + ```ts + opensInNewTab: '在新标签页打开', + theme: '主题', + themeLight: '浅色', + themeDark: '深色', + themeSystem: '跟随系统' + ``` + +- [ ] **Step 3:创建 `ThemeSwitcher.tsx`** + + 写入: + + ```tsx + import { useEffect, useState } from 'react'; + import { useTheme } from 'next-themes'; + import { useTranslation } from 'react-i18next'; + import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue + } from '@/components/ui/select'; + + type ThemeValue = 'light' | 'dark' | 'system'; + + const VALUES: readonly ThemeValue[] = ['light', 'dark', 'system'] as const; + + function normalizeTheme(raw: string | undefined): ThemeValue { + return (VALUES as readonly string[]).includes(raw ?? '') + ? (raw as ThemeValue) + : 'system'; + } + + const ThemeSwitcher = () => { + const { t } = useTranslation(); + const { theme, setTheme } = useTheme(); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + const value = mounted ? normalizeTheme(theme) : 'system'; + + const labels: Record = { + light: t('common.themeLight'), + dark: t('common.themeDark'), + system: t('common.themeSystem') + }; + + return ( + + ); + }; + + export default ThemeSwitcher; + ``` + + 说明: + - 通过 `mounted` 防止 SSR/水合不一致;项目为纯客户端应用,也顺手避免初次渲染闪烁。 + - 输入值经 `normalizeTheme` 归一化,异常值回退 `system`,满足设计文档的错误处理策略。 + +- [ ] **Step 4:lint 校验** + + ```powershell + pnpm lint + ``` + +- [ ] **Step 5:提交** + + ```powershell + git add src/dev-tools-collection-web/src/components/ThemeSwitcher.tsx src/dev-tools-collection-web/src/localization/en.ts src/dev-tools-collection-web/src/localization/zh.ts + git commit -m "feat: add ThemeSwitcher with light/dark/system tri-state" + ``` + +--- + +## Task 6:全局偏好区 — Footer 收纳主题切换并适配主题 + +**Files:** +- Modify: `src/dev-tools-collection-web/src/components/Footer.tsx` + +- [ ] **Step 1:替换 `Footer.tsx`** + + 将文件内容替换为: + + ```tsx + import { useTranslation } from 'react-i18next'; + import LanguageSwitcher from './LanguageSwitcher'; + import ThemeSwitcher from './ThemeSwitcher'; + + const Footer = () => { + const { t } = useTranslation(); + const currentYear = new Date().getFullYear(); + + return ( +
+
+ {t('common.copyright', { year: currentYear })} +
+
+ + +
+
+ ); + }; + + export default Footer; + ``` + + 关键变化: + - 颜色 token 化(`bg-muted/40`、`text-muted-foreground`)以支持暗色。 + - 窄屏下改为纵向排列,避免主题选择器溢出。 + +- [ ] **Step 2:lint 与 build 校验** + + ```powershell + pnpm lint + pnpm build + ``` + +- [ ] **Step 3:提交** + + ```powershell + git add src/dev-tools-collection-web/src/components/Footer.tsx + git commit -m "feat: host ThemeSwitcher in footer and tokenize footer colors" + ``` + +--- + +## Task 7:工具元数据与搜索 — 去硬编码 + 中英文别名匹配 + +**Files:** +- Modify: `src/dev-tools-collection-web/src/data/tools.ts` +- Modify: `src/dev-tools-collection-web/src/components/SearchBar.tsx` + +- [ ] **Step 1:重写 `data/tools.ts`** + + 将文件内容替换为: + + ```ts + import { + Clock, + Braces, + type LucideProps, + RotateCcwKey, + Code, + Link, + Hash, + QrCode, + FileCode, + Regex, + Globe + } from 'lucide-react'; + import type { ForwardRefExoticComponent, RefAttributes } from 'react'; + import type { i18n as I18nInstance } from 'i18next'; + + export interface Tool { + id: string; + icon: ForwardRefExoticComponent< + Omit & RefAttributes + >; + url: string; + popular?: boolean; + /** 外链工具:存在时点击卡片在新标签页打开该 URL,不走站内路由 */ + externalUrl?: string; + } + + export const toolsData: Tool[] = [ + { + id: 'json-formatter', + icon: Braces, + url: '/tools/json-formatter', + popular: true + }, + { + id: 'timestamp', + icon: Clock, + url: '/tools/timestamp', + popular: true + }, + { + id: 'uuid-generator', + icon: RotateCcwKey, + url: '/tools/uuid-generator', + popular: true + }, + { + id: 'base64-codec', + icon: Code, + url: '/tools/base64-codec', + popular: true + }, + { + id: 'url-codec', + icon: Link, + url: '/tools/url-codec', + popular: true + }, + { + id: 'qrcode-generator', + icon: QrCode, + url: '/tools/qrcode-generator', + popular: true + }, + { + id: 'hash-encoder', + icon: Hash, + url: '/tools/hash-encoder', + popular: true + }, + { + id: 'html-preview', + icon: FileCode, + url: '/tools/html-preview', + popular: true + }, + { + id: 'regex-tester', + icon: Regex, + url: '/tools/regex-tester', + popular: true + }, + { + id: 'ip-info', + icon: Globe, + url: 'https://ipinfo.io/what-is-my-ip', + externalUrl: 'https://ipinfo.io/what-is-my-ip', + popular: true + } + ]; + + export const getPopularTools = (): Tool[] => { + return toolsData.filter(tool => tool.popular); + }; + + const SEARCH_LANGS = ['zh', 'en'] as const; + + function normalize(text: string): string { + return text.trim().toLowerCase(); + } + + /** + * 构建工具在 zh/en 两种语言下的别名集合。 + * 缺失或返回 key 原值(未命中翻译)的候选词会被过滤掉, + * 不会因单语缺失导致整条工具数据不可搜索。 + */ + function buildAliases(tool: Tool, i18nInstance: I18nInstance): string[] { + const key = `tools.${tool.id}`; + const aliases = new Set(); + for (const lang of SEARCH_LANGS) { + const translated = i18nInstance.getFixedT(lang)(key); + if (typeof translated === 'string' && translated && translated !== key) { + aliases.add(normalize(translated)); + } + } + return [...aliases]; + } + + export const searchTools = ( + query: string, + i18nInstance: I18nInstance + ): Tool[] => { + const lower = normalize(query); + if (!lower) return []; + return toolsData.filter(tool => + buildAliases(tool, i18nInstance).some(alias => alias.includes(lower)) + ); + }; + ``` + + 关键变化: + - 删除 `Tool.name` 字段。 + - 新增基于 i18n 实例的 `buildAliases` 与 `searchTools(query, i18n)`。 + - 过滤 `translated === key` 的缺失翻译回落。 + +- [ ] **Step 2:修改 `SearchBar.tsx` 以传入 i18n 实例** + + 将文件内容替换为: + + ```tsx + import { useEffect, useState } from 'react'; + import { searchTools, type Tool } from '@/data/tools.ts'; + import { Search } from 'lucide-react'; + import { Input } from '@/components/ui/input.tsx'; + import { useTranslation } from 'react-i18next'; + + interface SearchBarProps { + onSearchResults: (results: Tool[]) => void; + } + + const SearchBar = ({ onSearchResults }: SearchBarProps) => { + const { t, i18n } = useTranslation(); + const [query, setQuery] = useState(''); + + useEffect(() => { + const delayDebounce = setTimeout(() => { + if (query) { + onSearchResults(searchTools(query, i18n)); + } else { + onSearchResults([]); + } + }, 300); + + return () => clearTimeout(delayDebounce); + }, [query, onSearchResults, i18n]); + + return ( +
+
+ + setQuery(e.target.value)} + /> +
+
+ ); + }; + + export default SearchBar; + ``` + + 关键变化: + - 把原先写死的 `text-black` 与 `focus-visible:ring-white` 移除,交由 shadcn Input 的默认语义 token 处理,支持暗色。 + - 向 `searchTools` 传入 i18n 实例。 + +- [ ] **Step 3:lint 与 build 校验** + + ```powershell + pnpm lint + pnpm build + ``` + + 预期:通过;若有其他文件引用 `Tool.name` 或 `searchTools(query)` 旧签名,构建会报错,需按错误定位补齐。 + +- [ ] **Step 4:提交** + + ```powershell + git add src/dev-tools-collection-web/src/data/tools.ts src/dev-tools-collection-web/src/components/SearchBar.tsx + git commit -m "fix: drop hardcoded tool names and support bilingual alias search" + ``` + +--- + +## Task 8:页面级缺陷 — 修复 HTML 预览 Blob URL 泄漏 + +**Files:** +- Modify: `src/dev-tools-collection-web/src/routes/tools/html-preview.tsx` + +- [ ] **Step 1:重写 `updatePreview` 与相关 `useEffect`** + + 定位文件 `html-preview.tsx:150-201` 的整段 `updatePreview` 声明与紧随其后的 `useEffect(() => { updatePreview(code); }, [code, updatePreview])`,替换为以下内容: + + ```tsx + // Create a safe HTML preview + const [previewSrc, setPreviewSrc] = useState(''); + + // 确保预览 HTML 带有 UTF-8 charset;Blob URL 生命周期由 useEffect 管理 + useEffect(() => { + let processedHtml = code; + + if ( + !processedHtml.includes('')) { + processedHtml = processedHtml.replace( + '', + '\n ' + ); + } else if (processedHtml.includes('')) { + processedHtml = processedHtml.replace( + '', + '\n\n \n' + ); + } else { + processedHtml = + '\n\n\n \n\n' + + processedHtml; + } + } + + let objectUrl: string | null = null; + try { + const blob = new Blob([processedHtml], { + type: 'text/html;charset=UTF-8' + }); + objectUrl = URL.createObjectURL(blob); + setPreviewSrc(objectUrl); + } catch { + // 预览 URL 生成失败时清空,避免展示陈旧预览 + setPreviewSrc(''); + } + + return () => { + if (objectUrl) { + URL.revokeObjectURL(objectUrl); + } + }; + }, [code]); + ``` + + 说明: + - 删除原先返回清理函数但被调用方丢弃的 `updatePreview` useCallback。 + - 每次 `code` 变更生成新 URL,cleanup 负责释放旧 URL;组件卸载时也会释放最后一个 URL。 + - 不改 iframe sandbox 语义。 + +- [ ] **Step 2:检查并确认不再有对旧 `updatePreview` 的引用** + + 在 `html-preview.tsx` 中搜索 `updatePreview` 字样,应已无残留。如有,需一并删除。 + +- [ ] **Step 3:lint 与 build 校验** + + ```powershell + pnpm lint + pnpm build + ``` + +- [ ] **Step 4:提交** + + ```powershell + git add src/dev-tools-collection-web/src/routes/tools/html-preview.tsx + git commit -m "fix: release html-preview blob url via effect cleanup to stop memory leak" + ``` + +--- + +## Task 9:页面级缺陷 — Base64 文本编码分块 + +**Files:** +- Modify: `src/dev-tools-collection-web/src/routes/tools/base64-codec.tsx` + +- [ ] **Step 1:新增分块工具函数** + + 在文件顶部 `function RouteComponent() { ... }` 之前(紧接 `export const Route ...` 之后)插入: + + ```ts + // 分块将 Uint8Array 转为 Base64,避免 String.fromCharCode(...) 展开大数组触发栈溢出 + function uint8ArrayToBase64(bytes: Uint8Array): string { + const chunkSize = 0x8000; // 32KB + let binary = ''; + for (let i = 0; i < bytes.length; i += chunkSize) { + const chunk = bytes.subarray(i, i + chunkSize); + binary += String.fromCharCode.apply( + null, + Array.from(chunk) as unknown as number[] + ); + } + return btoa(binary); + } + ``` + +- [ ] **Step 2:修改文本编码路径使用分块函数** + + 定位原 `encodeBase64` 中: + + ```ts + let base64String = btoa(String.fromCharCode(...data)); + ``` + + 替换为: + + ```ts + let base64String = uint8ArrayToBase64(data); + ``` + +- [ ] **Step 3:修改文件编码路径复用相同函数** + + 定位原 `handleFileUpload` 中下列片段: + + ```ts + // Convert bytes to base64 - using chunks to avoid call stack size exceeded + let binary = ''; + const chunkSize = 1024; + for (let i = 0; i < bytes.length; i += chunkSize) { + const chunk = bytes.slice(i, i + chunkSize); + binary += String.fromCharCode.apply(null, Array.from(chunk)); + } + let base64String = btoa(binary); + ``` + + 替换为: + + ```ts + let base64String = uint8ArrayToBase64(bytes); + ``` + +- [ ] **Step 4:lint 与 build 校验** + + ```powershell + pnpm lint + pnpm build + ``` + +- [ ] **Step 5:提交** + + ```powershell + git add src/dev-tools-collection-web/src/routes/tools/base64-codec.tsx + git commit -m "fix: chunk large uint8array when base64-encoding to avoid stack overflow" + ``` + +--- + +## Task 10:低风险残留清理 + +**Files:** +- Modify: `src/dev-tools-collection-web/src/routes/tools/qrcode-generator.tsx` +- Modify: `src/dev-tools-collection-web/src/routes/tools/timestamp.tsx` + +- [ ] **Step 1:清理 `qrcode-generator.tsx` 中的 `console.error`** + + 定位 `qrcode-generator.tsx` 中: + + ```ts + } catch (error) { + console.error('QR code generation error:', error); + toast.error(t('qrcodeGenerator.generateError')); + } + ``` + + 替换为: + + ```ts + } catch { + toast.error(t('qrcodeGenerator.generateError')); + } + ``` + +- [ ] **Step 2:统一 `timestamp.tsx` 的 clipboard Promise 处理** + + 定位 `timestamp.tsx:67-70`: + + ```ts + const copyTimestamp = () => { + navigator.clipboard.writeText(secondLevelTimestamp); + toast.success(t('timestampConverter.timestampCopied')); + }; + ``` + + 替换为: + + ```ts + const copyTimestamp = () => { + void navigator.clipboard.writeText(secondLevelTimestamp); + toast.success(t('timestampConverter.timestampCopied')); + }; + ``` + + 对文件内其他使用 `navigator.clipboard.writeText(...)` 而未加 `void` 的地方做同样处理;不调整 toast 文案或其他逻辑。 + +- [ ] **Step 3:lint 与 build 校验** + + ```powershell + pnpm lint + pnpm build + ``` + +- [ ] **Step 4:提交** + + ```powershell + git add src/dev-tools-collection-web/src/routes/tools/qrcode-generator.tsx src/dev-tools-collection-web/src/routes/tools/timestamp.tsx + git commit -m "chore: drop stray console.error and void clipboard writes in timestamp tool" + ``` + +--- + +## Task 11:死代码与未使用依赖清理 + +**Files:** +- Delete: `src/dev-tools-collection-web/src/App.tsx` +- Delete: `src/dev-tools-collection-web/src/assets/react.svg` +- Modify: `src/dev-tools-collection-web/package.json` + +- [ ] **Step 1:确认无引用** + + ```powershell + pnpm exec tsc -b + ``` + + 预期:通过。随后用仓库搜索(IDE 或 ripgrep)确认 `App.tsx`、`react.svg`、`uuid` 模块、`html-react-parser` 模块、`@types/json-parse-better-errors` 均无业务引用。若发现引用,先停下来与用户沟通。 + +- [ ] **Step 2:删除死文件** + + ```powershell + git rm src/dev-tools-collection-web/src/App.tsx src/dev-tools-collection-web/src/assets/react.svg + ``` + + 说明:若 `src/assets/` 目录删空,pnpm/vite 不会介意;无需额外处理。 + +- [ ] **Step 3:移除未使用依赖** + + 在 `src/dev-tools-collection-web` 执行: + + ```powershell + pnpm remove uuid html-react-parser @types/json-parse-better-errors + ``` + + 预期:`package.json` 与 `pnpm-lock.yaml` 均发生变化。 + +- [ ] **Step 4:lint 与 build 校验** + + ```powershell + pnpm lint + pnpm build + ``` + +- [ ] **Step 5:提交** + + ```powershell + git add src/dev-tools-collection-web/package.json src/dev-tools-collection-web/pnpm-lock.yaml + git commit -m "chore: remove dead app entry, unused asset and unused dependencies" + ``` + + 说明:`git rm` 已把删除登记到索引;若首次 `git commit` 时仍有未提交的删除,追加 `git add -A src/dev-tools-collection-web/src` 后再提交一次。 + +--- + +## Task 12:格式化与最终验收 + +- [ ] **Step 1:对本轮触达文件执行 prettier check** + + 在 `src/dev-tools-collection-web` 执行: + + ```powershell + pnpm exec prettier --check src/hooks/usePageMetadata.ts src/main.tsx src/routes/__root.tsx src/routes/index.tsx src/components/ToolPageLayout.tsx src/components/LanguageSwitcher.tsx src/components/ThemeSwitcher.tsx src/components/Footer.tsx src/components/SearchBar.tsx src/data/tools.ts src/routes/tools/html-preview.tsx src/routes/tools/base64-codec.tsx src/routes/tools/qrcode-generator.tsx src/routes/tools/timestamp.tsx src/localization/en.ts src/localization/zh.ts + ``` + + 预期:全部通过。若报告问题,执行一次 `pnpm exec prettier --write ` 后重新 check,并在最后一次 commit 内补入。 + +- [ ] **Step 2:最终 lint 与 build** + + ```powershell + pnpm lint + pnpm build + ``` + +- [ ] **Step 3:手工验收(按设计文档条目)** + + 在本地开发服务器(`pnpm dev`)与生产构建预览(`pnpm preview`)下分别确认: + + - 中文界面搜“JSON”“时间”均命中对应工具;英文界面搜“json”“time”“Base64”“regex”也均命中。 + - 切换语言后,浏览器标签页标题与 `` 同步变化;首页标题为站点名,工具页为 `工具名 | 站点名`。 + - 主题在浅色/深色/跟随系统三态间切换:刷新后仍保持;`sonner` toast 外观跟随主题。 + - 开发环境可见 Router Devtools;`pnpm preview` 下 DOM 中不再出现 Devtools。 + - HTML 预览工具连续编辑不出现明显内存增长(DevTools Performance/Memory 抽查 objectUrl 数量稳定);预览结果正常刷新。 + - Base64 文本编码对较大文本(例如粘贴一段 1–2MB 文本)不再报错;文件编码行为不变。 + - `src/App.tsx`、`src/assets/react.svg` 已在工作区中消失;`package.json` 不再出现 `uuid`、`html-react-parser`、`@types/json-parse-better-errors`。 + +- [ ] **Step 4:更新文档并提交** + + 更新 `.agentdocs/workflow/260423-fix-p0-p1-issues.md` 的 TODO 列表为全部勾选;将“当前任务文档”从 `.agentdocs/index.md` 的列表中移除,把两个文档一并移入 `.agentdocs/workflow/done/`。 + + ```powershell + mkdir .agentdocs/workflow/done + git mv .agentdocs/workflow/260423-fix-p0-p1-issues.md .agentdocs/workflow/done/260423-fix-p0-p1-issues.md + git mv .agentdocs/workflow/260423-fix-p0-p1-plan.md .agentdocs/workflow/done/260423-fix-p0-p1-plan.md + # 再更新 .agentdocs/index.md,移除“当前任务文档”下的相关条目 + git add .agentdocs/index.md + git commit -m "docs: archive p0/p1 fix workflow after verification" + ``` + +--- + +## 回滚策略 + +- 每个 Task 为独立 commit,必要时用 `git revert ` 回滚单点。 +- 若 Task 7 因第三方引用遗漏导致 build 失败,优先在同一 commit 内补齐引用;不拆成两次不完整提交。 +- 若 `pnpm remove` 后 lock 出现无法恢复的异常,可用 `git checkout -- pnpm-lock.yaml && pnpm install` 还原。 + +## 自检记录 + +- 规格覆盖:P0/P1 设计条目与 Task 1–12 一一对应(元信息 hook、ThemeProvider、Devtools 隔离、搜索去硬编码与双语匹配、Blob 泄漏、Base64 分块、死代码与未使用依赖、低风险残留清理、主题适配边界)。 +- 占位符:已手动复查无 “TBD/TODO/待补/类似前一步” 等模糊项。 +- 类型与签名一致性:`Tool` 接口移除 `name` 字段后,所有旧消费者(`ToolCard` 已使用 `t(\`tools.${tool.id}\`)`,`App.tsx` 在 Task 11 删除)均不再依赖它;`searchTools` 新签名在 `SearchBar.tsx` 中同步更新。 +- 样式一致性:Footer、LanguageSwitcher、SearchBar 均改为语义 token;未扩展到工具页面内部的大规模改色,符合设计文档第 5 节主题适配边界。 diff --git a/src/dev-tools-collection-web/index.html b/src/dev-tools-collection-web/index.html index 00d97b3..fc543b3 100644 --- a/src/dev-tools-collection-web/index.html +++ b/src/dev-tools-collection-web/index.html @@ -4,7 +4,7 @@ - 在线工具箱 + Toolbox
diff --git a/src/dev-tools-collection-web/package.json b/src/dev-tools-collection-web/package.json index 5a03208..b962435 100644 --- a/src/dev-tools-collection-web/package.json +++ b/src/dev-tools-collection-web/package.json @@ -26,14 +26,12 @@ "@tailwindcss/vite": "^4.1.14", "@tanstack/react-router": "^1.133.3", "@tanstack/react-router-devtools": "^1.133.3", - "@types/json-parse-better-errors": "^1.0.3", "@uidotdev/usehooks": "^2.4.1", "@uiw/codemirror-extensions-classname": "^4.25.2", "@uiw/codemirror-theme-white": "^4.25.2", "@uiw/react-codemirror": "^4.25.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "html-react-parser": "^5.2.7", "i18next": "^25.6.0", "i18next-browser-languagedetector": "^8.2.0", "lucide-react": "^0.546.0", @@ -45,8 +43,7 @@ "react-i18next": "^16.0.1", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", - "tailwindcss": "^4.1.14", - "uuid": "^13.0.0" + "tailwindcss": "^4.1.14" }, "lint-staged": { "**/*": "prettier --write --ignore-unknown" diff --git a/src/dev-tools-collection-web/pnpm-lock.yaml b/src/dev-tools-collection-web/pnpm-lock.yaml index 07df46a..09229bc 100644 --- a/src/dev-tools-collection-web/pnpm-lock.yaml +++ b/src/dev-tools-collection-web/pnpm-lock.yaml @@ -49,9 +49,6 @@ importers: '@tanstack/react-router-devtools': specifier: ^1.133.3 version: 1.133.3(@tanstack/react-router@1.133.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@tanstack/router-core@1.133.3)(@types/node@24.8.1)(csstype@3.1.3)(jiti@2.6.1)(lightningcss@1.30.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(solid-js@1.9.7)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.1) - '@types/json-parse-better-errors': - specifier: ^1.0.3 - version: 1.0.3 '@uidotdev/usehooks': specifier: ^2.4.1 version: 2.4.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -70,9 +67,6 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 - html-react-parser: - specifier: ^5.2.7 - version: 5.2.7(@types/react@19.2.2)(react@19.2.0) i18next: specifier: ^25.6.0 version: 25.6.0(typescript@5.9.3) @@ -109,9 +103,6 @@ importers: tailwindcss: specifier: ^4.1.14 version: 4.1.14 - uuid: - specifier: ^13.0.0 - version: 13.0.0 devDependencies: '@eslint/js': specifier: ^9.37.0 @@ -1842,12 +1833,6 @@ packages: integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== } - '@types/json-parse-better-errors@1.0.3': - resolution: - { - integrity: sha512-wbwigqXeGQq+liQIqxYNylOV4c3ilUqB9czasOS26TSy21Ti1l2Q8c8TEjmaTnc0CgdJDBhIMFJssIbY1FanYA== - } - '@types/json-schema@7.0.15': resolution: { @@ -2562,31 +2547,6 @@ packages: } engines: { node: '>=0.10.0' } - dom-serializer@2.0.0: - resolution: - { - integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== - } - - domelementtype@2.3.0: - resolution: - { - integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== - } - - domhandler@5.0.3: - resolution: - { - integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== - } - engines: { node: '>= 4' } - - domutils@3.2.2: - resolution: - { - integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw== - } - dunder-proto@1.0.1: resolution: { @@ -2625,20 +2585,6 @@ packages: } engines: { node: '>=10.13.0' } - entities@4.5.0: - resolution: - { - integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== - } - engines: { node: '>=0.12' } - - entities@6.0.1: - resolution: - { - integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g== - } - engines: { node: '>=0.12' } - environment@1.1.0: resolution: { @@ -3263,36 +3209,12 @@ packages: integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA== } - html-dom-parser@5.1.1: - resolution: - { - integrity: sha512-+o4Y4Z0CLuyemeccvGN4bAO20aauB2N9tFEAep5x4OW34kV4PTarBHm6RL02afYt2BMKcr0D2Agep8S3nJPIBg== - } - html-parse-stringify@3.0.1: resolution: { integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg== } - html-react-parser@5.2.7: - resolution: - { - integrity: sha512-WzIAcqQoZoF49J9aev8NBDLz9TJvt2RmipeYA+/5+5x0sWCwFxqKiq0lysieiSA/G6dbUZ6KGGy65Cx2fjie5Q== - } - peerDependencies: - '@types/react': 0.14 || 15 || 16 || 17 || 18 || 19 - react: 0.14 || 15 || 16 || 17 || 18 || 19 - peerDependenciesMeta: - '@types/react': - optional: true - - htmlparser2@10.0.0: - resolution: - { - integrity: sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g== - } - husky@9.1.7: resolution: { @@ -3353,12 +3275,6 @@ packages: } engines: { node: '>=18' } - inline-style-parser@0.2.4: - resolution: - { - integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q== - } - internal-slot@1.1.0: resolution: { @@ -4299,12 +4215,6 @@ packages: integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== } - react-property@2.0.2: - resolution: - { - integrity: sha512-+PbtI3VuDV0l6CleQMsx2gtK0JZbZKbpdu5ynr+lbsuvtmgbNcS3VM0tuY2QjFNOcWxvXeHjDpy42RO+4U2rug== - } - react-refresh@0.17.0: resolution: { @@ -4755,18 +4665,6 @@ packages: integrity: sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw== } - style-to-js@1.1.18: - resolution: - { - integrity: sha512-JFPn62D4kJaPTnhFUI244MThx+FEGbi+9dw1b9yBBQ+1CZpV7QAT8kUtJ7b7EUNdHajjF/0x8fT+16oLJoojLg== - } - - style-to-object@1.0.11: - resolution: - { - integrity: sha512-5A560JmXr7wDyGLK12Nq/EYS38VkGlglVzkis1JEdbGWSnbQIEhZzTJhzURXN5/8WwwFCs/f/VVcmkTppbXLow== - } - supports-color@7.2.0: resolution: { @@ -5004,13 +4902,6 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - uuid@13.0.0: - resolution: - { - integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w== - } - hasBin: true - vite@7.1.10: resolution: { @@ -6276,8 +6167,6 @@ snapshots: '@types/estree@1.0.8': {} - '@types/json-parse-better-errors@1.0.3': {} - '@types/json-schema@7.0.15': {} '@types/json5@0.0.29': {} @@ -6770,24 +6659,6 @@ snapshots: dependencies: esutils: 2.0.3 - dom-serializer@2.0.0: - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - entities: 4.5.0 - - domelementtype@2.3.0: {} - - domhandler@5.0.3: - dependencies: - domelementtype: 2.3.0 - - domutils@3.2.2: - dependencies: - dom-serializer: 2.0.0 - domelementtype: 2.3.0 - domhandler: 5.0.3 - dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -6807,10 +6678,6 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.0 - entities@4.5.0: {} - - entities@6.0.1: {} - environment@1.1.0: {} es-abstract@1.24.0: @@ -7326,32 +7193,10 @@ snapshots: dependencies: hermes-estree: 0.25.1 - html-dom-parser@5.1.1: - dependencies: - domhandler: 5.0.3 - htmlparser2: 10.0.0 - html-parse-stringify@3.0.1: dependencies: void-elements: 3.1.0 - html-react-parser@5.2.7(@types/react@19.2.2)(react@19.2.0): - dependencies: - domhandler: 5.0.3 - html-dom-parser: 5.1.1 - react: 19.2.0 - react-property: 2.0.2 - style-to-js: 1.1.18 - optionalDependencies: - '@types/react': 19.2.2 - - htmlparser2@10.0.0: - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - domutils: 3.2.2 - entities: 6.0.1 - husky@9.1.7: {} i18next-browser-languagedetector@8.2.0: @@ -7377,8 +7222,6 @@ snapshots: index-to-position@1.2.0: {} - inline-style-parser@0.2.4: {} - internal-slot@1.1.0: dependencies: es-errors: 1.3.0 @@ -7864,8 +7707,6 @@ snapshots: react-is@16.13.1: {} - react-property@2.0.2: {} - react-refresh@0.17.0: {} react-remove-scroll-bar@2.3.8(@types/react@19.2.2)(react@19.2.0): @@ -8193,14 +8034,6 @@ snapshots: style-mod@4.1.2: {} - style-to-js@1.1.18: - dependencies: - style-to-object: 1.0.11 - - style-to-object@1.0.11: - dependencies: - inline-style-parser: 0.2.4 - supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -8357,8 +8190,6 @@ snapshots: dependencies: react: 19.2.0 - uuid@13.0.0: {} - vite@7.1.10(@types/node@24.8.1)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1): dependencies: esbuild: 0.25.11 diff --git a/src/dev-tools-collection-web/src/App.tsx b/src/dev-tools-collection-web/src/App.tsx deleted file mode 100644 index b7a4493..0000000 --- a/src/dev-tools-collection-web/src/App.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { useState, useEffect } from 'react'; -import SearchBar from '@/components/SearchBar'; -import ToolCard from '@/components/ToolCard'; -import { getPopularTools, type Tool, toolsData } from '@/data/tools'; -import Footer from '@/components/Footer'; - -const App = () => { - const [searchResults, setSearchResults] = useState([]); - const [, setPopularTools] = useState([]); - - useEffect(() => { - // 初始化热门工具数据 - const popular = getPopularTools(); - setPopularTools(popular); - }, [setPopularTools]); - - const handleSearchResults = (results: Tool[]) => { - setSearchResults(results); - }; - - return ( -
-
- {/* Hero Section with reduced height */} -
-
-

在线工具箱

- -
-
- - {/* Tools Content */} -
- {/* Search Results */} - {searchResults.length > 0 ? ( -
-

搜索结果

-
- {searchResults.map(tool => ( - - ))} -
-
- ) : ( -
-
- {toolsData.map(tool => ( - - ))} -
-
- )} -
-
- -
-
- ); -}; - -export default App; diff --git a/src/dev-tools-collection-web/src/assets/react.svg b/src/dev-tools-collection-web/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/src/dev-tools-collection-web/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/dev-tools-collection-web/src/components/Footer.tsx b/src/dev-tools-collection-web/src/components/Footer.tsx index 7379e1d..8d3028a 100644 --- a/src/dev-tools-collection-web/src/components/Footer.tsx +++ b/src/dev-tools-collection-web/src/components/Footer.tsx @@ -1,16 +1,20 @@ import { useTranslation } from 'react-i18next'; import LanguageSwitcher from './LanguageSwitcher'; +import ThemeSwitcher from './ThemeSwitcher'; const Footer = () => { const { t } = useTranslation(); const currentYear = new Date().getFullYear(); return ( -