Compare commits

..

347 Commits

Author SHA1 Message Date
rsgltzyd 6be9b31ca2 docs: 将spec目录移动到docs目录中 2026-07-05 21:34:41 +08:00
rsgltzyd 6975e38f79 refactor: 重构 Tailwind 配置以支持模块化和共享主题
- 更新 components.json,修改 CSS 文件路径为 src/styles/shell.css。
- 移除 tailwind.config.js 中的冗余配置,改为导入 tailwind.shell.config.js。
- 新增 tailwind.pages.config.js 和 tailwind.shared.js,集中管理主题和样式配置。
- 更新多个组件以使用新的样式类和配置,提升代码一致性和可维护性。
2026-06-30 23:15:35 +08:00
rsgltzyd 17a4dbd8a7 refactor: 优化 RouterProvider 组件的数据加载逻辑
- 使用批量获取方法 `getMany` 替代多个单独的 `get` 调用,简化数据加载过程。
- 更新 `loadInitialData` 函数,确保从存储中获取的默认值更为一致。
- 修改测试用例以验证批量获取的功能,确保组件在不同情况下的正确性。
2026-06-30 22:56:31 +08:00
rsgltzyd 27e03ae038 refactor: 移除 RouterContainer 组件中的 isLoaded 状态检查
- 简化 RouterContainer 组件逻辑,直接渲染页面内容,无需等待加载状态。
- 更新测试用例,确保在组件挂载后直接渲染页面结构。
2026-06-30 22:51:38 +08:00
rsgltzyd c7f4305fc3 refactor: 重构 RouterContainer 组件以支持懒加载页面模块
- 移除不必要的 Suspense 组件,改为使用 LoadedPage 组件进行懒加载。
- 更新特性获取逻辑,使用 getFeatureByKey 函数替代直接查找。
- 新增 featureMeta.ts 文件,集中管理功能配置和相关方法。
- 实现 pageLoaders 以支持按需加载页面模块,提升性能和用户体验。
2026-06-30 22:45:32 +08:00
rsgltzyd d18ae5d41c refactor: 移除 twMerge 以简化 cn() 函数实现 2026-06-30 22:00:08 +08:00
rsgltzyd 47e08f80b3 refactor: 统一功能组件结构,简化特性配置 2026-06-30 21:18:28 +08:00
rsgltzyd ece5c6135e fix: 更新中文提示信息格式以提升可读性
在多个组件中移除模板字符串,直接使用普通字符串格式化中文提示信息,增强了代码的可读性和一致性。
2026-06-30 21:06:39 +08:00
rsgltzyd d0825c5ec0 refactor: 移除无意义的 cn() 包装
对仅含静态类名或单一三元表达式的 className 改用普通字符串,保留有条件合并或 prop 覆盖场景下的 cn() 用法。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 21:05:10 +08:00
Cursor Agent 149883565d docs: 同步测试数据生成器 Worker 任务 ID 与 ruleStorage 写入失败说明
Co-authored-by: LingandRX <LingandRX@users.noreply.github.com>
2026-06-30 20:43:38 +08:00
Cursor Agent ff3c66531c fix(测试数据生成器): 修复规则覆盖保存误创建重复规则
覆盖同名规则时 handleSave(true) 未传入已有规则 id,save() 走新建分支
导致旧规则保留且产生同名重复项,getByName 仍返回旧数据。
2026-06-30 20:43:33 +08:00
Cursor Agent 4084dafc29 fix(测试数据生成器): 修复 Worker 并发生成导致的数据污染
Co-authored-by: LingandRX <LingandRX@users.noreply.github.com>
2026-06-30 20:43:26 +08:00
Cursor Agent 647e3dd308 fix(StorageCleaner): 同一标签页 URL 变更时阻止误清理
handleClean 原先仅校验 tab id,未校验 url。用户在页面内导航后、
存储信息刷新完成前确认清理,会按旧页面展示的数据清理新页面存储。
2026-06-30 20:40:57 +08:00
雨霖铃 2967a6f79f fix: 修复 ruleStorage 写入失败处理与 CopyButton 成功态样式 2026-06-27 17:53:08 +08:00
雨霖铃 92bca1c728 refactor: 更新 Worker 消息类型以支持生成任务 ID 2026-06-27 17:47:58 +08:00
雨霖铃 ce0af7dff3 Merge branch 'main' into develop 2026-06-27 17:42:43 +08:00
雨霖铃 d38bafe237 refactor: 统一中文文案并简化组件结构,提升代码可读性
移除 chrome.i18n 后,将各功能页面、布局与工具模块的 UI 文案改为内联中文;
删除冗余注释与过度抽象(如 ToolCard、StatCard),精简 props 与状态管理;
同步优化 JsonTools、StorageCleaner、Timestamp、TestDataGenerator、Dashboard、
Base64Converter、RightClickRestorer、TextStatistics、TopBar、RouterProvider、
ThemeModeProvider 及生成器库与 utils 工具文件。
2026-06-27 17:26:19 +08:00
cursor[bot] f3a9838017 docs: 同步 i18n 移除与存储初始化防覆盖说明 (#73)
- 移除已废弃的 chrome.i18n / _locales 文档引用
- 补充 UI 文案规范(features.tsx + 组件内中文)
- 记录 RouterProvider / useStorageState 初始化防覆盖机制
- 更新 syncSnapshot、themeSnapshot、restrictedUrls 工具说明
2026-06-27 10:36:18 +08:00
Cursor Agent 0545910b16 fix: 修复路由初始化竞态、存储状态静默覆盖与存储清理误删 2026-06-26 17:17:17 +08:00
cursor[bot] 1948ba67a7 docs: 同步工程文档与当前架构 (#70)
* docs(工程文档): 同步当前架构说明
* docs(工程文档): 格式化文档表格
2026-06-26 17:16:10 +08:00
cursor[bot] 4b38133cf8 fix(测试数据生成器): 修复 Worker 取消失效与规则存储静默失败 (#69)
- 为生成任务引入 generationId,忽略过期 Worker 响应,避免取消后展示错误数据
- Worker 每 100 行让出事件循环,使 cancel 消息能被及时处理
- ruleStorage.setAll 返回写入结果,save/update/delete 在 QuotaExceeded 时返回失败
- 补充 useGenerator 与 ruleStorage 单元测试
2026-06-26 17:07:35 +08:00
cursor[bot] 5ae9cf065f docs: 同步工程文档与当前架构 (#70)
* docs(工程文档): 同步当前架构说明
* docs(工程文档): 格式化文档表格
2026-06-26 16:59:53 +08:00
rsgltzyd 1041b3ece1 docs: 更新AGENTS.md文档并清理废弃文件
1. 重构AGENTS.md内容,补充功能列表、项目结构说明
2. 删除CLAUDE.md、src/entrypoints/README.md、src/pages/README.md、.github/copilot-instructions.md等废弃文档
2026-06-26 13:54:14 +08:00
rsgltzyd edd4cf266c fix(StorageCleaner): 修复 IndexedDB 清理竞态、部分成功计数与刷新后状态同步 2026-06-25 23:31:44 +08:00
雨霖铃 ebf9f7a153 refactor(StorageCleaner): 移动 isRestrictedUrl 函数到独立模块并更新相关引用
- 将 isRestrictedUrl 函数从 storageCleaner 移动到 restrictedUrls 模块,优化代码结构。
- 更新相关文件以引用新的位置,移除冗余的测试用例。
2026-06-19 21:09:22 +08:00
雨霖铃 c98d09d14c feat(TopBar): 添加搜索框快捷键提示功能及相关测试
- 新增 getSearchShortcutLabel 函数,根据平台返回搜索框的快捷键提示文案。
- 更新 SearchInput 组件,使用 getSearchShortcutLabel 显示正确的快捷键。
- 新增 constants.test.ts 测试文件,验证 getSearchShortcutLabel 函数在不同操作系统下的行为。
- 新增 features.lazy.test.ts 文件,测试懒加载功能,确保页面模块按需加载。
2026-06-19 21:06:51 +08:00
雨霖铃 bf06b61f10 fix(RouterProvider): 修复初始加载状态逻辑,确保在加载前不覆盖 chrome.storage
- 将 isLoaded 的初始状态设置为 false,避免在加载初始数据前错误地覆盖存储。
- 新增测试用例,验证在 localStorage 快照过期时,loadInitialData 完成前不应覆盖 chrome.storage 的行为。
2026-06-19 21:00:27 +08:00
雨霖铃 0f3060dcf3 refactor(代码重复): 抽取共享工具函数与 UI 组件,消除多处重复实现
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-19 20:55:39 +08:00
雨霖铃 47d74f999e refactor(RouterProvider, chromeStorage, useContextMenuData): 更新存储获取逻辑以移除默认值参数
- 在 RouterProvider 和 useContextMenuData 中更新对 storageUtil.get 的调用,移除默认值参数,简化逻辑。
- 修改 chromeStorage 中的 get 方法签名,确保在没有默认值时返回可选类型,并添加相应的单元测试以验证类型推断。
2026-06-19 20:47:43 +08:00
雨霖铃 ed383dd319 refactor(CopyButton.test): 移除冗余测试用例 2026-06-19 20:34:38 +08:00
雨霖铃 e1b3a26a2c feat(TestDataGenerator): 更新规则编辑提示和警告显示逻辑
- 修改规则编辑时的 toast 提示,确保显示正确的规则名称。
- 更新 ResultPanel 组件,优化警告数量的显示逻辑,确保超过 5 条警告时正确显示剩余警告数量。
- 新增 TestDataGeneratorPage 和 ResultPanel 组件的单元测试,验证功能的正确性。
2026-06-19 20:31:59 +08:00
雨霖铃 e372fc6855 refactor(ImageUploader): 移除预览 URL 相关逻辑并简化粘贴事件处理
- 删除 ImageUploader 组件中的 onPreviewUrlChange 属性及相关逻辑,简化文件处理流程。
- 更新测试用例,确保只调用 onFileChange 方法,移除对预览 URL 的依赖。
- 新增 ParsePanel 组件的测试,确保粘贴功能的正确性和唯一性。
2026-06-19 20:29:19 +08:00
雨霖铃 9d6213e75e fix(business): 修复 skewedRandom 函数中的数学计算错误
- 更新 skewedRandom 函数中的公式,确保正确计算随机值,提升函数的准确性和可靠性。
2026-06-19 20:27:30 +08:00
雨霖铃 228cadf526 refactor(RightClickRestorer): 用状态配置表简化页面 UI 结构
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-19 10:41:22 +08:00
雨霖铃 06b9d29270 feat(Jwt): 重构 JWT 组件,新增常量和结果视图
- 新增 constants.ts 文件,定义 JWT 输入占位符和配置常量,提升代码可读性。
- 删除 JwtSection 组件,重构为 JwtResultView 组件,简化结果展示逻辑。
- 更新 index.tsx,使用 JwtResultView 组件替代原有的 JWT 结果展示方式,优化页面结构。
2026-06-19 10:31:40 +08:00
雨霖铃 0906d1f425 feat(Timestamp): 重构时间戳转换功能,新增 ConverterForm 组件
- 在 Timestamp 页面中引入 ConverterForm 组件,简化输入和转换逻辑。
- 新增常量定义,包括模式选项、单位选项和输入占位符,提升代码可读性。
- 更新 useTimestampConverter 钩子,添加输入占位符支持。
- 引入 ToolCard 组件以统一样式和结构,优化页面布局。
2026-06-19 10:27:01 +08:00
雨霖铃 9c993b6cea feat(TextStatistics): 引入 StatCard 组件以优化统计信息展示
- 新增 StatCard 组件,简化统计信息的展示逻辑。
- 更新 TextStatistics 页面,使用 StatCard 组件替代原有的统计项渲染方式,提升代码可读性和维护性。
2026-06-19 10:22:49 +08:00
雨霖铃 ca0143f3bd feat: 添加 EmptyPlaceholder 组件并更新相关页面
- 新增 EmptyPlaceholder 组件,用于统一处理空状态提示,提升用户体验。
- 更新多个页面(JsonTools、Jwt、QrCode、Timestamp)以使用 EmptyPlaceholder 组件,简化空状态的实现。
- 更新 README.md,添加 EmptyPlaceholder 组件的说明。
2026-06-19 10:16:59 +08:00
雨霖铃 3ab7194501 refactor: 统一使用 Button 组件替换原有 button 元素
- 在 CopyButton、SwitchButtonGroup 和 TextInputArea 组件中,将原有的 button 元素替换为统一的 Button 组件,提升代码一致性和可维护性。
- 更新相关样式和属性以适应新组件,确保功能正常。
2026-06-19 10:08:27 +08:00
雨霖铃 d401744739 feat(TopBar): 添加搜索功能组件并优化结构
- 新增 SearchInput、SearchDropdown、SearchResultItem 和 TopBarActions 组件,增强 TopBar 的搜索功能。
- 更新 useTopBar 钩子以支持最近搜索功能,并优化搜索历史管理。
- 修改 TopBar 组件以整合新组件,提升可读性和可维护性。
- 更新 README.md,反映新增组件和功能。
2026-06-19 09:59:15 +08:00
雨霖铃 2c901ab4fd refactor: 重构 TopBar 组件并更新项目结构
- 删除原有的 TopBar 组件,重构为布局组件并移动至 layout/ 目录。
- 更新相关文档,反映 TopBar 组件的新位置。
- 引入新的 useTopBar 钩子以管理 TopBar 的状态和逻辑。
- 添加 TopBar 组件的单元测试,确保功能正常。
2026-06-19 09:53:29 +08:00
雨霖铃 a8a1e89f8a refactor: 更新功能键名称并移除 README 文件
- 将功能键 'jsonDiff' 更新为 'jsonTools',以更准确地反映其功能。
- 删除不再使用的 README.md 文件,清理项目结构。
- 更新相关测试用例以反映功能键名称的变化。
2026-06-19 09:47:08 +08:00
雨霖铃 9097c48b1f feat(Dashboard): 重构仪表板功能组件和逻辑
- 新增 dashboardFeatures.ts 和 useDashboard.ts 文件,封装仪表板功能的解析和状态管理逻辑。
- 更新 Dashboard 组件,使用 useDashboard 钩子简化可见工具和最近使用工具的处理。
- 添加仪表板功能的单元测试,确保功能正确性。
- 更新 README.md,移除不再使用的 ToolCard 组件说明。
2026-06-19 09:26:30 +08:00
雨霖铃 c69a26b469 refactor: 清理代码注释并优化组件结构
- 在 useQrCode.ts 中移除多余的注释,简化代码逻辑。
- 在 DataPreview.tsx 和 FieldEditor.tsx 中删除不必要的注释,提升可读性。
- 在 format.ts 和 jsonToToml.ts 中更新注释内容,确保准确性。
2026-06-19 01:04:15 +08:00
雨霖铃 cd21d11fa1 refactor: 将 UI 文案改为中文并优化相关逻辑
- 在 background.ts 中将错误信息翻译为中文。
- 在 uiPopover.ts 中移除注释并更新弹出框标题。
- 在 Base64ConverterSection.tsx 中动态显示最大文件大小。
- 在 DecodeResultPaper.tsx 中简化标题文案注释。
- 在 GenerateButton.tsx 中动态显示生成进度和预计剩余时间。
- 在 base64Converter.ts 中将错误信息翻译为中文。
2026-06-19 00:53:56 +08:00
雨霖铃 f0292acbb7 fix(ThemeModeProvider): 修复主题快照同步与首屏闪烁问题
抽取 themeSnapshot 工具模块,异步恢复与跨窗同步时写回 snapshot;HTML 内联脚本与 useLayoutEffect 消除 dark 类首帧闪烁;避免 storage 恢复覆盖用户操作及 setState 嵌套更新。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-19 00:45:52 +08:00
雨霖铃 8f9ff27034 fix(RouterProvider): 将 goBack 重命名为 goHome,修复跨窗同步 merge 不一致
API 命名与实际行为(返回 dashboard)对齐;storage.onChanged 同步 visiblePages/pageOrder 时应用 mergeWithDefaults,与初始化逻辑保持一致。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-19 00:38:01 +08:00
雨霖铃 aa63723b80 refactor(AppRoot): 移除 RouterProvider 组件,直接渲染子组件
- 在 AppRoot 组件中移除 RouterProvider,直接将 children 作为子元素渲染,简化组件结构。
2026-06-19 00:36:40 +08:00
雨霖铃 ec92afa294 refactor(i18n): 移除 chrome.i18n 国际化,统一使用中文硬编码
移除 chromeI18n 工具、_locales 翻译文件和 manifest default_locale 配置,
将所有 UI 文案改为直接硬编码中文,并更新相关测试与文档。
2026-06-19 00:26:56 +08:00
雨霖铃 618f3c36f2 refactor: 更新测试用例和组件结构
- 在背景测试中更新菜单项标题,移除图标
- 在 StorageCleaner 页面中更新初始化加载状态的中文文案
- 在 QrCode 组件中修正导入路径
- 在 DecodeResultPaper 中使用 Label 组件替换原生 label 元素
- 移除不再使用的 formatFileSize 函数
2026-06-14 09:40:05 +08:00
雨霖铃 24c83de56d refactor(Timestamp): 规范化页面组件目录结构
- 将子组件移至 components/ 子目录(LiveClock、ResultView)
- 创建 __tests__/ 目录结构
- 更新所有相关导入路径

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 21:16:12 +08:00
雨霖铃 9c5f5c8cfe refactor(StorageCleaner): 规范化页面组件目录结构
- 将子组件移至 components/ 子目录(OptionItem、AutoRefreshToggle、CleaningResult、StorageCleanerConfirm、StorageOptionsGrid、ErrorDisplay)
- 创建 __tests__/ 目录结构
- 更新所有相关导入路径

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 20:56:38 +08:00
雨霖铃 016173a9c5 refactor(JsonTools): 规范化页面组件目录结构
- 将子组件移至 components/ 子目录(JsonDiffInput、DiffResult、JsonFormatSection、JsonConvertSection、DiffNavigator、JsonTree)
- 将 diffEngine.ts 工具函数移至 src/utils/
- 提取 ConvertFunction/ConvertResult 类型到 types.ts,解决循环依赖
- 创建 __tests__/ 目录结构
- 更新所有相关导入路径

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 20:39:40 +08:00
雨霖铃 30d3561726 refactor: 规范化页面组件目录结构
- 将 QrCodePreview、ImageUploader 移动到 QrCode/components/
- 将 DecodeResultPaper、Base64ConverterSection、TextMode 移动到 Base64Converter/components/
- 提取 useTextMode Hook,遵循 UI + Hook 分离模式
- 简化 SwitchButtonGroup 组件,提取常量到外部

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 20:11:35 +08:00
雨霖铃 1f9288f780 refactor: 使用 Label 组件替换原生 label 元素,并在 DecodeResultPaper 中引入 Input 组件 2026-06-10 08:15:09 +08:00
雨霖铃 355a94d116 refactor: 将 formatFileSize 重命名为 formatBytes 并移至通用工具模块 2026-06-10 00:32:29 +08:00
雨霖铃 3f73adc015 docs: 更新文档和配置,移除未使用的权限和过时描述 2026-06-07 09:34:20 +08:00
LingandRX a163297de4 develop (#68)
* docs(AGENTS.md): 添加 Git Commit 必须使用中文描述的规范
* docs: 更新测试数据生成器功能设计文档
* refactor: 删除死代码
* docs: 统一测试数据生成器文档,移除 SQL/TypeScript 导出,修复接口命名不一致
* feat: 实现测试数据生成器功能
* docs: 更新 README 和 AGENTS 文档,补充测试数据生成器模块说明
2026-06-07 00:04:53 +08:00
雨霖铃 86870864a9 docs: 更新 README 和 AGENTS 文档,补充测试数据生成器模块说明 2026-06-06 23:56:05 +08:00
雨霖铃 1a285fc4c1 Merge branch 'feature/test-data-generator' into develop 2026-06-06 23:42:14 +08:00
雨霖铃 9a0c8ba850 fix: 更新 features 测试断言以匹配新增的 testDataGenerator
fix: 修复测试数据生成器多项代码问题
- useCallback 闭包问题:改用函数式 setFields 更新,避免依赖过时引用
- Worker 复用:复用已有 Worker 而非每次 generate 都销毁重建
- 导出复制反馈:复制操作完成后添加 toast 成功/失败提示
- 字段名校验:添加空值、非法字符、重复名称校验
- i18n 硬编码:GeneratorConfig 和 ruleStorage 统一使用 t() 函数
2026-06-06 23:40:36 +08:00
雨霖铃 6c7b7294b4 feat: 优化规则管理功能
- 移动保存按钮到字段配置区域
- 添加规则编辑功能,支持更新和另存为
- 编辑模式下显示编辑状态标识
- 保存时检查规则名称重复,支持覆盖保存
- 优化弹窗样式,统一风格
- 更换加载图标为 FolderOpen
- 添加搜索防抖和错误处理
- 添加国际化文本
2026-06-06 23:08:05 +08:00
雨霖铃 a2e07a9115 refactor: 删除未使用的 defaultNullRate 功能
- 移除 GenerateOptions 中的 defaultNullRate 相关 props 和 UI
- 移除 messages.json 中的 testDataGenerator_defaultNullRate 翻译 key
2026-06-06 20:08:06 +08:00
雨霖铃 2464a5fea6 feat: 字段编辑弹窗添加「完成」按钮 去除关闭按钮,添加取消按钮
- 弹窗底部添加「完成」按钮,点击关闭弹窗
- DialogContent 支持 showCloseButton prop 控制关闭按钮显示
- 底部添加取消和完成两个按钮
2026-06-06 20:06:48 +08:00
雨霖铃 78f5292b00 fix: 切换为非必填时默认空值率 100%
- 必填 → 非必填:默认空值率 100%(始终生成 null)
- 非必填 → 必填:空值率清零
2026-06-06 19:42:55 +08:00
雨霖铃 5a2589cc57 fix: 修复空值率与必填状态的逻辑
- 空值率 0% 时自动设为必填(始终生成数据)
- 空值率 > 0% 时设为非必填(按概率生成 null)
- 移除空值率 100% 自动变为必填的错误逻辑
2026-06-06 19:41:42 +08:00
雨霖铃 64db60d75e fix: 非必填字段预览显示 null
- 预览示例数据中,非必填字段显示 null 值
- 保持与实际生成逻辑一致
2026-06-06 19:36:41 +08:00
雨霖铃 7090ba3251 refactor: 数据预览改为显示示例数据,简化组件逻辑
- DataPreview 改为接收字段配置,显示一条示例数据
- 移除虚拟列表、格式切换等复杂功能
- 添加 JSON 语法高亮(键名蓝色、字符串绿色、数字琥珀色、布尔紫色)
- 优化空状态样式,垂直居中显示
- 缩短预览区域高度 (400px → 280px)
- 添加 testDataGenerator_sampleData 翻译 key
2026-06-06 19:29:27 +08:00
雨霖铃 31b6edf06b feat: 生成成功改为 toast 提示
- 使用 sonner toast 显示成功提示
- 简化提示:生成完成 + 数据条数
- 仅在失败或有警告时显示结果面板
- 使用 useRef 记录已触发的 result
- 仅在新 result 时触发 toast
2026-06-06 11:05:42 +08:00
雨霖铃 a374bf456d feat: 数据格式改为按钮组选择
- 移除 Select 下拉框
- 改为 JSON/CSV 按钮组
- 选中状态与生成数量按钮样式统一

feat: 优化空值率设置

- 增加 75% 预设选项
- 设置 100% 时自动设为必填
- 限制输入范围 0-100

fix: 空值率预设改为纯数字显示

- 5%/20%/50%/75% 统一样式

fix: 移除空值率低/中/高 i18n 翻译键

fix: 精简生成数量预设选项

- 移除 10、500
- 保留 50、100、1000、5000、10000
2026-06-06 10:52:56 +08:00
雨霖铃 ea10b27c14 feat: 简化字段卡片交互 - 点击直接打开编辑弹窗
- 移除字段卡片选中样式
- 点击字段直接打开编辑弹窗
- 简化 FieldItem 组件,移除 isSelected 属性
- 简化 FieldList 接口,onSelect 改为 onEdit

fix: 优化字段编辑弹窗

- 去除弹窗描述文字
- 字段名称增加字符计数 (x/20)
- 字段描述增加字符计数 (x/50)
- 保持 placeholder 提示

fix: 修复弹窗顶部间距问题

- DialogContent 添加 pt-6 避开关闭按钮
- 内容区域移除顶部 padding,改为 pb-6

fix: 增加弹窗顶部间距 (pt-6 → pt-8)

fix: 优化弹窗尺寸和按钮间距

- 弹窗最大宽度 360→420px 适配标签页
- 增加按钮 padding (px-2→px-3, py-1→py-1.5)
- 添加按钮增加 padding (h-8→h-9, px-3)
2026-06-06 10:43:35 +08:00
雨霖铃 40443c8fc5 fix: 优化字段编辑弹窗样式
- 去除弹窗标题
- 描述文字显示选择字段提示
- 关闭按钮置顶,不随内容滚动
- 内容区域独立滚动
2026-06-06 10:32:19 +08:00
雨霖铃 ee4a45630e fix: 限制弹窗尺寸适配 popup 显示
- 最大宽度 360px
- 最大高度 calc(100vh - 4rem)
- 内容超出时支持滚动
- 宽度自适应 viewport
2026-06-06 10:31:07 +08:00
雨霖铃 4a423889d9 feat: 字段编辑改为弹窗显示
- 添加 Dialog 组件包裹 FieldEditor
- 点击字段卡片打开编辑弹窗
- 简化布局,移除右侧字段编辑面板
- 支持点击遮罩或关闭按钮关闭弹窗
2026-06-06 10:29:44 +08:00
雨霖铃 ee78e314d3 fix: 修复拖拽时滚轮滚动不生效问题
- 改为在 document 级别监听 wheel 事件
- 使用 capture 阶段优先处理事件
- 确保在 PointerSensor 拦截前捕获滚轮事件
2026-06-06 10:20:59 +08:00
雨霖铃 ee77c97b56 feat: 拖拽时支持鼠标滚轮滚动列表
- 拖拽状态下监听 wheel 事件
- 使用 preventDefault 阻止默认滚动
- 降低滚动速度 (0.5x) 提供更精确的控制
- 自动更新虚拟列表渲染范围
2026-06-06 10:18:13 +08:00
雨霖铃 aa8b84af50 fix: 修复虚拟列表拖拽时显示空白问题
- 添加 DragOverlay 独立渲染拖拽中的项目
- 拖拽时扩展预加载范围 (OVERSCAN 2→10)
- 添加 activeId 状态跟踪拖拽项目
- 拖拽中的卡片以半透明效果独立显示
2026-06-06 10:17:16 +08:00
雨霖铃 707f17ac84 feat: 字段列表优化 - 输入限制、选中样式、取消选中
- FieldEditor: 字段名称maxLength=20,描述maxLength=50
- FieldItem: 统一有无描述时的垂直居中布局,选中左侧竖线+背景高亮
- FieldList: 点击已选中字段可取消选中,选中态加shadow和ring效果
- index: 支持selectedIndex=-1取消选中
2026-06-06 10:09:48 +08:00
雨霖铃 af0b2d8ed1 fix: 统一字段列表卡片高度,最大字段数改为40
- MAX_FIELDS: 20 → 40
- 固定卡片高度52px,无论虚拟/非虚拟模式视觉一致
- 5条以上容器高度固定,启用虚拟滚动
- 主页面添加字段时检查MAX_FIELDS限制
2026-06-06 09:58:57 +08:00
雨霖铃 49e7f1ea55 feat: 实现虚拟列表、拖拽排序、实时预览防抖,更新文档
- DataPreview: 大数据量(>100条)自动启用虚拟滚动,仅渲染可见区域
- FieldList: 基于 @dnd-kit/sortable 实现拖拽排序
- index.tsx: 配置变更时300ms防抖生成预览数据
- ui-design.md: 去除平板/移动端响应式、快捷键,更新交互说明
- technical-implementation.md: 去除downloadAsZip,更新虚拟列表说明
2026-06-06 09:42:58 +08:00
雨霖铃 1a92e88934 feat: 实现测试数据生成器完整功能
- 添加类型定义 (FieldConfig, DataRule, GeneratorDefinition 等)
- 实现 21 个数据生成器 (个人信息/业务数据/技术数据/基础类型)
- 实现 Web Worker 后台数据生成引擎
- 实现规则存储与管理功能
- 实现 JSON/CSV 数据导出功能
- 开发完整 UI 组件 (字段管理/生成器选择/数据预览/导出面板等)
- 集成到应用功能配置,添加国际化支持

功能特性:
- 实时预览生成的数据
- 字段配置 (名称/描述/必填/空值率/唯一性约束)
- 规则管理 (保存/加载/搜索/导入导出)
- Web Worker 后台生成,支持进度显示和取消
- 响应式布局,支持多种设备

docs: 更新 TASKS.md 所有子任务复选框为已完成状态
2026-06-06 09:29:12 +08:00
雨霖铃 2b84eda24f docs: 统一测试数据生成器文档,移除 SQL/TypeScript 导出,修复接口命名不一致
- 移除所有文档中的 SQL 和 TypeScript 导出格式引用
- 统一数据导出格式为 JSON 和 CSV
- 移除 GeneratorDefinition 中的 icon 字段
- 更新 generatorCategories 配置,包含所有 21 个生成器
- 统一 rule-management.md 和 technical-implementation.md 中的接口名称为 DataRule
- 更新 ui-design.md 中的界面设计,移除 SQL/TypeScript 相关元素
2026-06-06 09:04:53 +08:00
LingandRX a5d86a92c2 develop (#67)
feat: 升级项目 Logo、完善 toast 提示及优化工程化配置

### Features
- **logo**: 替换全套项目图标,更新 wxt.config.ts 浏览器扩展图标配置。

### Bug Fixes
- **ui/toast**: 引入 <Toaster> 组件修复 toast 无法显示问题,并调整位置为正下方。
- **i18n**: 统一将 t() 函数中的 `.` 和 `:` 转换为 `_`,修复国际化文案读取失败问题。
- **context-menu**: 限制 srcUrl 检查仅在二维码图片菜单项生效,避免误判。
- **hooks**: 修复 pre-push 钩子,仅对变更文件进行 eslint 检查,并支持无 upstream 时的回退对比。
- **components**: 修复 CopyButton 动画与组件导入方式,并补齐无障碍属性与测试。
- **async**: 延迟生成二维码以正确渲染 loading 状态,并适配相关异步测试。

### Refactor & Cleans
- **clean**: 清理未使用的组件(ToolCard)、Hook(useDebounce)及多个未使用的工具函数导出。
- **structure**: 统一移动测试文件至 __tests__ 目录,移除不必要的类型导出。

### Documentation
- **docs**: 在 AGENTS.md 中明确规范 Git Commit 必须使用中文描述。
2026-06-06 01:07:27 +08:00
雨霖铃 7f22b95aa2 refactor: 移除不必要的 export 关键字
中优先级任务:将仅内部使用的函数和类型移除 export 关键字

移除 export 的函数:
- uiPopover: showPopover
- storageCleaner: injectClearLocalStorage, injectClearSessionStorage,
  injectClearIndexedDB, injectClearCacheStorage, injectUnregisterServiceWorkers

移除 export 的类型和常量:
- base64Converter: SUPPORTED_IMAGE_TYPES, SUPPORTED_IMAGE_EXTENSIONS,
  TextToBase64Result, Base64ToBlobResult
- contextMenu: ContextMenuItemConfig, ContextMenuClickedInfo, ParseResult
- jsonFormatter: JsonMinifyResult
- jsonToToml: JsonToTomlResult
- jsonToYaml: JsonToYamlResult
- jwt: JwtHeader, JwtPayload
- qrCodeParser: QrCodeParseResult

所有 473 个测试通过,类型检查通过
2026-06-06 01:00:10 +08:00
雨霖铃 9990d86270 refactor: 删除高优先级死代码
删除未使用的代码以提高代码质量和可维护性:

删除的文件:
- src/pages/Dashboard/ToolCard.tsx:未被使用的组件
- src/utils/useDebounce.ts:未被使用的 hook
- src/components/__tests__/ToolCard.test.tsx:对应组件的测试文件

删除的导出函数:
- chromeTabs: getActiveTab, getActiveTabDomain, ensureContentScriptInjected
- clipboard: copyImageToClipboard
- storageCleaner: isEmptyResult
- chromeI18n: preloadNamespaces
- useContextMenuData: clearContextMenuData

更新了对应的测试文件以保持一致性

总计删除 525 行代码,所有 473 个测试通过
2026-06-06 01:00:10 +08:00
雨霖铃 ea0552e8aa feat: 替换项目图标为自定义 logo
- 替换 public/icon/ 目录下所有 PNG 图标文件为新 logo
- 删除 public/wxt.svg(不再需要)
- 在 wxt.config.ts 中添加 icons 和 default_icon 配置
- 更新 public/README.md 文档
2026-06-06 01:00:10 +08:00
雨霖铃 97098c3abe refactor: 移除不必要的 export 关键字
中优先级任务:将仅内部使用的函数和类型移除 export 关键字

移除 export 的函数:
- uiPopover: showPopover
- storageCleaner: injectClearLocalStorage, injectClearSessionStorage,
  injectClearIndexedDB, injectClearCacheStorage, injectUnregisterServiceWorkers

移除 export 的类型和常量:
- base64Converter: SUPPORTED_IMAGE_TYPES, SUPPORTED_IMAGE_EXTENSIONS,
  TextToBase64Result, Base64ToBlobResult
- contextMenu: ContextMenuItemConfig, ContextMenuClickedInfo, ParseResult
- jsonFormatter: JsonMinifyResult
- jsonToToml: JsonToTomlResult
- jsonToYaml: JsonToYamlResult
- jwt: JwtHeader, JwtPayload
- qrCodeParser: QrCodeParseResult

所有 473 个测试通过,类型检查通过
2026-06-06 00:55:05 +08:00
雨霖铃 7afce94d93 refactor: 删除高优先级死代码
删除未使用的代码以提高代码质量和可维护性:

删除的文件:
- src/pages/Dashboard/ToolCard.tsx:未被使用的组件
- src/utils/useDebounce.ts:未被使用的 hook
- src/components/__tests__/ToolCard.test.tsx:对应组件的测试文件

删除的导出函数:
- chromeTabs: getActiveTab, getActiveTabDomain, ensureContentScriptInjected
- clipboard: copyImageToClipboard
- storageCleaner: isEmptyResult
- chromeI18n: preloadNamespaces
- useContextMenuData: clearContextMenuData

更新了对应的测试文件以保持一致性

总计删除 525 行代码,所有 473 个测试通过
2026-06-06 00:49:57 +08:00
雨霖铃 e2b76a5f86 feat: 替换项目图标为自定义 logo
- 替换 public/icon/ 目录下所有 PNG 图标文件为新 logo
- 删除 public/wxt.svg(不再需要)
- 在 wxt.config.ts 中添加 icons 和 default_icon 配置
- 更新 public/README.md 文档
2026-06-05 23:57:04 +08:00
雨霖铃 1ab79785b6 docs(AGENTS.md): 添加 Git Commit 必须使用中文描述的规范 2026-06-05 23:41:04 +08:00
雨霖铃 c40b66b703 docs: 更新测试数据生成器功能设计文档
主要更新:
- 添加字段必填/选填设置和空值率配置
- 移除标签管理系统(20条规则无需标签)
- 添加组合策略保证唯一性生成
- 添加价格生成器(真实分布/心理定价)
- 添加场景化生成器(年龄/数量/评分/折扣/库存)
- 更新所有文档移除emoji,使用纯文字
- 更新类型定义和生成逻辑
2026-06-05 23:40:36 +08:00
雨霖铃 f55fd4338d docs(AGENTS.md): 添加 Git Commit 必须使用中文描述的规范 2026-06-05 22:54:29 +08:00
雨霖铃 27c1cf37e3 chore: update .gitignore with common ignore patterns
- Add environment variable files (.env, .env.local, .env.*.local)
- Add build output directories (dist/, build/)
- Add test coverage directory (coverage/)
- Add IDE workspace files (*.code-workspace)
- Add TypeScript build info files (*.tsbuildinfo, *.tsxbuildinfo)
- Remove docs/* to track documentation files
2026-06-05 22:53:39 +08:00
雨霖铃 2520d72eb1 refactor: 移动 CopyButton 测试文件到统一 __tests__ 目录 2026-06-05 21:57:19 +08:00
LingandRX 4d038203a7 fix(CopyButton): 修复 review 反馈并补充测试 (#66)
* fix: remove non-functional animation classes from CopyButton
* test(CopyButton): 补充单元测试覆盖边界场景
* fix: unify CopyButton imports to named export across all consumers
* fix(CopyButton): add aria-label for screen reader accessibility
2026-06-05 21:16:12 +08:00
LingandRX c992986789 fix(CopyButton): 修复 review 反馈并补充测试 (#66)
* fix: remove non-functional animation classes from CopyButton
* test(CopyButton): 补充单元测试覆盖边界场景
* fix: unify CopyButton imports to named export across all consumers
* fix(CopyButton): add aria-label for screen reader accessibility
2026-06-05 21:15:48 +08:00
LingandRX 2c45ddada9 fix: add missing Toaster component to enable toast display (#65)
The project was calling toast.success/error/warning from sonner in 7 files,
but the <Toaster> component was never rendered in the React tree. This meant
sonner stored toast data internally but no UI was ever displayed to users.

Changes:
- Create src/components/ui/sonner.tsx with shadcn/ui-style Toaster wrapper
  that integrates with ThemeModeProvider for light/dark theme support
- Add <Toaster /> to AppRoot.tsx so it works across all entry points
  (popup, sidepanel, browser-tab)

fix: always convert dots to underscores in i18n t() key lookup

The t() function only converted dots to underscores when the key contained
a colon (namespace:key format). Keys using dot notation like
'messages.copySuccess' were passed directly to chrome.i18n.getMessage()
without dot-to-underscore conversion, causing lookups to fail silently and
return empty strings - resulting in toast notifications with no text.

Now all separators (both ':' and '.') are consistently converted to '_'
regardless of format, matching the messages.json key convention.

fix: position toast at bottom-center instead of bottom-right
2026-06-05 19:34:09 +08:00
LingandRX 70b799aa2d fix: add missing Toaster component to enable toast display (#65)
The project was calling toast.success/error/warning from sonner in 7 files,
but the <Toaster> component was never rendered in the React tree. This meant
sonner stored toast data internally but no UI was ever displayed to users.

Changes:
- Create src/components/ui/sonner.tsx with shadcn/ui-style Toaster wrapper
  that integrates with ThemeModeProvider for light/dark theme support
- Add <Toaster /> to AppRoot.tsx so it works across all entry points
  (popup, sidepanel, browser-tab)

fix: always convert dots to underscores in i18n t() key lookup

The t() function only converted dots to underscores when the key contained
a colon (namespace:key format). Keys using dot notation like
'messages.copySuccess' were passed directly to chrome.i18n.getMessage()
without dot-to-underscore conversion, causing lookups to fail silently and
return empty strings - resulting in toast notifications with no text.

Now all separators (both ':' and '.') are consistently converted to '_'
regardless of format, matching the messages.json key convention.

fix: position toast at bottom-center instead of bottom-right
2026-06-05 19:29:21 +08:00
LingandRX 58af2af37b fix: 右键菜单 srcUrl 误判、pre-push 变更文件检测、generating 状态不可见 (#64)
* fix: 修复右键菜单 srcUrl 误判、pre-push 全量扫描、generating 状态不可见问题

- contextMenu: srcUrl 检查限定为 qrCode-image 菜单项,避免其他菜单误返回图片 URL
- pre-push: eslint 改为只检查变更文件(git diff 过滤),tsc 保留全项目检查
- useQrCode: confirmGenerate 用 setTimeout 延迟生成,让 loading 状态先被 React 渲染
- tests: 生成相关测试改为 async + waitFor 适配异步 confirmGenerate

* fix(hooks): pre-push 无 upstream 时回退到 origin/main 对比

* fix(hooks): 用 merge-base 替代 rev-parse 修复 pre-push 变更文件检测
2026-06-05 13:10:44 +08:00
LingandRX a0b1ade662 Merge branch 'main' into develop 2026-06-05 13:10:34 +08:00
雨霖铃 d964e27c9d fix(hooks): 用 merge-base 替代 rev-parse 修复 pre-push 变更文件检测 2026-06-05 13:02:30 +08:00
LingandRX 359326c6da Hotfix/contextmenu srcurl generating fix (#63)
* fix: 修复右键菜单 srcUrl 误判、pre-push 全量扫描、generating 状态不可见问题

- contextMenu: srcUrl 检查限定为 qrCode-image 菜单项,避免其他菜单误返回图片 URL
- pre-push: eslint 改为只检查变更文件(git diff 过滤),tsc 保留全项目检查
- useQrCode: confirmGenerate 用 setTimeout 延迟生成,让 loading 状态先被 React 渲染
- tests: 生成相关测试改为 async + waitFor 适配异步 confirmGenerate

* fix(hooks): pre-push 无 upstream 时回退到 origin/main 对比
2026-06-05 12:53:19 +08:00
雨霖铃 10644fae95 fix(hooks): pre-push 无 upstream 时回退到 origin/main 对比 2026-06-05 12:50:55 +08:00
雨霖铃 77f722b575 fix: 修复右键菜单 srcUrl 误判、pre-push 全量扫描、generating 状态不可见问题
- contextMenu: srcUrl 检查限定为 qrCode-image 菜单项,避免其他菜单误返回图片 URL
- pre-push: eslint 改为只检查变更文件(git diff 过滤),tsc 保留全项目检查
- useQrCode: confirmGenerate 用 setTimeout 延迟生成,让 loading 状态先被 React 渲染
- tests: 生成相关测试改为 async + waitFor 适配异步 confirmGenerate
2026-06-05 12:49:43 +08:00
LingandRX 0624b609be Merge pull request #62 from LingandRX/hotfix/remove-unused-alarms
hotfix: remove unused alarms permission
2026-06-02 19:10:26 +08:00
Ubuntu f551428142 hotfix: remove unused alarms permission 2026-06-02 19:09:39 +08:00
LingandRX 1ae057ccfa Merge pull request #61 from LingandRX/hotfix/revert-version
hotfix: revert package.json version to 1.0.0
2026-06-02 14:59:34 +08:00
Ubuntu 400a1afb0c hotfix: revert package.json version to 1.0.0 2026-06-02 14:58:54 +08:00
LingandRX c6d0914d33 Merge pull request #60 from LingandRX/develop
fix: update _locales path in vitest.setup.ts
2026-06-02 14:38:05 +08:00
Ubuntu 7617e317f2 fix: update _locales path from zh to zh_CN in vitest.setup.ts 2026-06-02 14:35:56 +08:00
LingandRX 396b3d7013 Merge pull request #59 from LingandRX/develop
fix: correct default_locale to zh_CN
2026-06-02 14:18:20 +08:00
雨霖铃 92fb853329 fix: correct default_locale to zh_CN to match _locales directory 2026-06-02 14:16:33 +08:00
雨霖铃 e4e5464be0 chore(hooks): 优化 lint-staged 和 git hooks 配置
- lint-staged: 本地开发仅自动修复不阻塞,CI 环境严格检查
- pre-commit: 后台运行 tsc 类型检查,前台运行 lint-staged
- pre-push: 新增严格检查闸门(tsc + eslint --max-warnings=0)
2026-05-30 21:31:12 +08:00
LingandRX f4b6ae89b5 Merge pull request #58 from LingandRX/develop
chore: merge develop into main (v1.x.x)
2026-05-30 21:20:52 +08:00
雨霖铃 37bdeb5f3a fix(qrcode): 去除已上传图片区域重复的文本预览
- 移除图片预览区域中的解析结果文本预览
- 解析结果只在下方的 TextInputArea 中显示
- 避免重复显示相同内容
2026-05-30 21:03:35 +08:00
雨霖铃 e919af6857 fix(qrcode): 修复图片右键菜单功能描述和检测逻辑
- 修改菜单标题为「解析图片二维码」更准确
- 改进 isImageUrl 函数,支持更多图片URL格式
- 支持 data:image/ 格式
- 支持URL路径中包含图片相关关键词
- 更新国际化文案和测试用例
2026-05-30 21:01:33 +08:00
雨霖铃 5be15301b5 feat(qrcode): 实现图片转二维码右键菜单功能
- 添加图片类型的右键菜单配置
- 修改 parseContextMenuClick 支持 srcUrl 参数
- 实现图片URL自动解析功能
- 检测图片URL自动切换到解析模式
- 更新测试用例覆盖新功能
- 新增国际化文案
2026-05-30 20:58:28 +08:00
雨霖铃 306600a35c refactor(qrcode): 优化二维码转文本布局,与文本转二维码风格统一
- 未上传图片时只显示上传区域
- 上传图片后显示小图预览 + 解析结果
- 添加重新上传按钮
- 解析结果文本限制显示长度
- 新增国际化文案(已上传图片、重新上传、解析中等)
- 适配popup窗口紧凑布局
2026-05-30 20:51:48 +08:00
雨霖铃 a0345b384d style(qrcode): 优化预览态布局适配popup窗口
- 调整文本预览区域高度,单行显示截断文本
- 缩小二维码图片尺寸(56x56 -> 48x48)
- 减少内边距和间距,更紧凑的布局
- 确保完整显示在popup窗口中无需滚动
2026-05-30 20:46:47 +08:00
雨霖铃 9458d62a99 feat(qrcode): 右键菜单传入URL时自动生成二维码
- 右键菜单传入URL时直接生成二维码,跳转到预览态
- 保留手动输入文本并点击生成按钮的功能
- 优化用户体验,简化操作流程
2026-05-30 20:38:23 +08:00
雨霖铃 5b26287cff refactor(qrcode): 优化布局逻辑,输入态隐藏预览区域
- 输入态:只显示输入框和生成按钮,隐藏二维码预览
- 预览态:上方显示文本预览(超长文本截断),下方显示二维码
- 点击编辑返回输入态,隐藏二维码预览
- 更新测试用例适配新布局
2026-05-30 20:29:12 +08:00
雨霖铃 6dca673a5a refactor(qrcode): 优化文本转二维码为两步操作模式
- 将实时预览改为显式触发生成,用户体验更清晰
- 新增输入态(input)和预览态(preview)两种状态
- 输入态:显示输入框和生成按钮,右侧显示占位提示
- 预览态:显示只读文本预览和编辑按钮,右侧显示二维码
- 保存上次输入内容,返回编辑态时自动回填
- 添加空输入校验和错误提示
- 更新国际化文案
- 完善单元测试覆盖新功能
2026-05-30 20:23:05 +08:00
雨霖铃 afd57bb5b2 Merge branch 'main' into develop 2026-05-30 19:47:16 +08:00
LingandRX 3f447708b7 fix: replace undefined formatByteSize with formatBytes (#57)
JsonConvertSection.tsx and JsonFormatSection.tsx were calling
formatByteSize which doesn't exist. The correct function name
imported from @/utils/format is formatBytes.

Co-authored-by: Ubuntu <ubuntu@localhost.localdomain>
2026-05-30 12:55:04 +08:00
LingandRX f9821553b3 chore: merge develop into main (#56)
* fix(ci): fix release workflow artifact upload issue

- Split artifact upload into 3 separate steps (Chrome, Firefox, Source)
- Update download steps to match new artifact names
- Add if-no-files-found: error for better error handling
- Fix Node.js 20 compatibility issue with upload-artifact@v4 glob pattern

* docs: 更新 AGENTS.md,添加测试工具和翻译文件结构说明

* docs: 更新 AGENTS.md,完善 CI 步骤和存储键名格式说明

* fix: remove unnecessary animations from StorageCleaner page

- Remove entrance animations (animate-in, fade-in, zoom-in) from all components
- Remove scale effect (active:scale-[0.99]) from clean button
- Remove transition effects (transition-all, transition-colors) from OptionItem, StorageOptionsGrid, AutoRefreshToggle
- Remove bouncing animation from error icon
- Remove pulsing animation from warning banner
- Keep animate-spin on button loading spinner as functional indicator

* fix: remove unnecessary animations from RightClickRestorer page

* fix: remove all unnecessary animations from all pages

- Remove dead code animations (animate-in, fade-in, slide-in, zoom-in, shake) from tailwindcss-animate plugin (not installed)
- Remove decorative transition effects (transition-all, transition-colors) from all page components
- Remove scale effects (active:scale-95) from buttons
- Remove bounce animations (animate-bounce) from icons
- Keep functional animate-spin on loading spinners as they provide essential loading feedback

Affected pages: Dashboard, Timestamp, Jwt, JsonTools, Base64Converter, HtmlToMarkdown, MarkdownToHtml, QrCode, TextStatistics

* docs: 添加项目文档(copilot-instructions、编码规范、各目录 README)

- 新增 .github/copilot-instructions.md:Copilot 指令文件,涵盖构建命令、CI 流水线、项目架构、关键规范
- 新增 .github/CODING_STANDARDS.md:完整的代码编写规范文档(TypeScript、React、样式、错误处理、命名、测试、Hook、i18n、存储等 12 个章节)
- 新增 11 个目录的 README.md:components、components/ui、config、entrypoints、i18n、lib、pages、providers、public、src、types、utils
- 更新 AGENTS.md:补充页面组件模式说明和 components/ui 目录

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(CopyButton): use shadcn buttonVariants instead of duplicated variant classes

Remove hand-written variantClasses/sizeClasses that duplicated shadcn's
buttonVariants. Now extends ButtonProps and imports buttonVariants from
components/ui/button for consistent styling.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(ErrorBoundary): use shadcn destructive tokens instead of hardcoded red colors

Replace hardcoded red-200/red-50/red-600 with border-destructive,
bg-destructive/5, text-destructive. Use variant='destructive' on
Button. Align error log styling with PageErrorBoundary.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(TextInputArea): replace hardcoded Chinese strings with i18n t() calls

Use existing translation keys for clear success, copy success, and
copy error messages instead of hardcoded Chinese text.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(QrCodePreview): use shadcn Button instead of manual button styles

Replace hand-written button class strings with shadcn Button component
using outline and default variants. Removes ~15 lines of duplicated
Tailwind classes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs(GlobalSnackbar): clarify relationship with sonner toast

Add note explaining when to use GlobalSnackbar (provider context,
severity levels, custom positioning) vs sonner toast (simple one-off
messages).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(ImageUploader): wrap handleClearFile with useCallback

Prevents unnecessary re-renders by memoizing the callback, consistent
with handleFileChange which already uses useCallback.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(SwitchButtonGroup): remove redundant containerPadding conditional

Both branches of the ternary returned 'p-1'. Inline the constant
directly into the className string.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(TopBar): add missing openExtensionPage import

The function was called in handleOpenInTab but never imported from
utils/chromeTabs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(TopBar): add selectedIndex boundary protection in keyboard nav

Guard against out-of-bounds access when search results change during
keyboard navigation. Check selectedIndex < totalItems before accessing
arrays.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(RouterContainer): remove unnecessary empty-dep useMemo

getEntryPointType() is a simple config getter. Replace useMemo(() => ..., [])
with a direct call — the empty dep array made the memo pointless.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(i18n): add missing translation keys for error boundaries and copy messages

Add keys for errorBoundary, pageErrorBoundary, router, and
messages.copyEmpty in both zh and en locales.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(CopyButton): replace hardcoded Chinese strings with i18n t() calls

Use useTranslation('common') for tooltip, copy empty/success/error
toast messages.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(ErrorBoundary): replace hardcoded Chinese with i18n via withTranslation HOC

Use react-i18next withTranslation HOC for class component to translate
title, description, and refresh button text.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(PageErrorBoundary): replace hardcoded Chinese with i18n via withTranslation HOC

Use react-i18next withTranslation HOC for class component to translate
title, description, and retry button text.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(RouterContainer): replace hardcoded Chinese with i18n t() calls

Use useTranslation('common') for 404 page title and description with
entryPointType interpolation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(pages): 修复 TypeScript 错误、硬编码字符串,移除死代码

- 修复 LiveClock.tsx CopyButton size 属性类型错误 (small → sm)
- 修复 ToolCard.tsx 紫色 RGB 值拼写错误 (147,51,2 purple → 147,51,232)
- 替换 4 处硬编码中文为 i18n 调用 (StorageCleaner, JsonTools)
- MarkdownToHtml 预览链接色改为 CSS 变量以支持暗黑模式
- 移除 Base64Converter 中已被 Base64ConverterSection 替代的死代码
  (FileMode.tsx, ImageMode.tsx 及其测试文件)
- 更新 README.md 移除对已删除文件的引用

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(CopyButton, LiveClock): 修改按钮大小为 'icon',移除硬编码颜色配置

* refactor(TextInputArea): 复用 CopyButton 组件替换内联复制逻辑

* fix(test): 修复 CopyButton mock 未捕获 clipboard writeText 异常

* refactor: 迁移 i18n 系统从 react-i18next 到 chrome.i18n

- 移除 react-i18next、i18next 及相关依赖
- 删除旧的 i18n/ 目录和 useLazyTranslation 工具
- 新增 utils/chromeI18n.ts 类型安全 wrapper(useI18n Hook + getMessage)
- 生成 public/_locales/{zh,en}/messages.json(298 个翻译 key)
- 批量更新 39+ 组件文件的导入和翻译调用
- 转换翻译键格式:namespace:key → namespace_key
- 修复 ErrorBoundary/PageErrorBoundary 从 withTranslation HOC 改为直接调用 getMessage
- 更新 vitest.setup.ts mock 加载实际翻译文本
- 修复 11 个测试文件的断言以匹配中文翻译
- TypeScript、ESLint、547 项测试全部通过

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* refactor: 移除语言切换按钮

chrome.i18n 语言由浏览器设置决定,不支持运行时切换,移除 TopBar 中的语言切换按钮及相关逻辑。

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* refactor: 移除英文翻译,仅保留中文

删除 public/_locales/en/ 目录,清理 chromeI18n.ts 中未使用的语言切换相关导出。

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chore: 清理未使用的 i18n key(322 → 245)

移除 77 个未被源码引用的翻译 key,包括:
- jsonFormat_* (17): 未接入的子模式标题和按钮
- buttons_* (15): 代码使用 common: 命名空间格式,直接 key 未引用
- qrCode_* (14): 未使用的页面标题和状态提示
- timestamp/common/storageCleaner/jsonDiff 等零散未用 key

保留了通过模板字面量动态引用的 key(storageCleaner_options_*、common_buttons_themeMode_*、jsonFormat_*InputPlaceholder 等)。

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* refactor: reorganize directory structure into src/

Move all source code directories into src/ for cleaner project structure:
- pages/, components/, utils/, config/, providers/, types/, lib/, assets/, entrypoints/ → src/
- Use WXT srcDir config to resolve @/ alias to src/
- Update tsconfig, vitest, eslint, tailwind configs
- Remove scattered README.md files from subdirectories
- Update documentation (AGENTS.md, CODING_STANDARDS.md, README.md)
- Fix pre-existing lint error in RouterProvider.tsx

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* refactor: remove meaningless comments

Remove 50+ noise comments across the codebase:
- AI-generated verbose prose (💡 emoji, '超进化', '大闸', etc.)
- Comments that restate what the code obviously does
- Comments about deleted code
- Import-level noise comments
- Overly verbose Chinese section markers

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* refactor: clean up unused code and simplify imports

- Remove unused imports and variables across 35 files
- Simplify component logic and remove dead code
- Clean up test files by removing unnecessary setup
- Streamline CI workflow configuration

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: remove HTML-Markdown conversion features (htmlToMarkdown & markdownToHtml)

Removed both conversion tools and all related code:
- Deleted page components: HtmlToMarkdown, MarkdownToHtml
- Deleted utility functions: htmlToMarkdown.ts, markdownToHtml.ts
- Deleted unit tests for both utilities
- Removed feature configs and PageType entries from storage.d.ts
- Removed preview mode types and StorageSchema keys
- Removed all i18n translation keys from messages.json
- Removed vendor-markdown chunk from wxt.config.ts
- Removed marked dependency from package.json
- Updated feature count in tests (11 -> 9, page order 10 -> 8)
- Updated README, AGENTS.md, and copilot-instructions.md

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: redesign Dashboard layout with search and recently used tools

- Add search input at top to filter tools by name/description
- Add 'recently used' section showing last 3 used tools as compact chips
- Add 'all tools' section header for the tool card grid
- Track tool usage via new 'app/recentlyUsedTools' storage key
- Update navigateTo to record recently used tools (max 3, LRU order)
- Add i18n keys: dashboard_searchPlaceholder, dashboard_recentlyUsed, dashboard_allTools

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: replace ToolCard grid with compact icon+name grid on Dashboard

- Replace large ToolCard layout with dense grid (3-6 columns responsive)
- Each item shows icon + tool name in a compact tile
- Hover highlights icon and text with smooth transitions
- Remove ToolCard import from Dashboard (component retained for reuse)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: remove redundant search input from Dashboard (header already has one)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: optimize TopBar search with Cmd+K shortcut and improved styling

- Add global Cmd/Ctrl+K keyboard shortcut to focus search input
- Show keyboard shortcut hint (⌘K) in search input when empty
- Improve input styling: rounded-lg, softer borders, focus transitions
- Improve dropdown: rounded-lg, larger shadow, better spacing
- Larger result icons (h-8 w-8) with rounded-lg containers
- Refined history section with uppercase label and muted icon
- Search icon fades in on focus via group-focus-within

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: improve search history to show recently used tools with direct navigation

- Store feature keys (PageType) in history instead of translated labels
- Show icon + name + description in history items (same as search results)
- Click or Enter on history item navigates directly to the tool page
- Remove History icon import (no longer needed)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: useI18n fallback for empty string translations from chrome.i18n

* refactor: 移除设置页面

- 删除 options 入口点目录及相关文件
- 移除 wxt.config.ts 中 options_ui 配置
- 移除 TopBar 组件中的设置按钮和 onOpenOptions prop
- 移除 popup/sidepanel App.tsx 中的 handleOpenOptions
- 移除相关测试中的设置页面引用
- 移除 messages.json 中设置相关翻译键
- 通过 lint、typecheck、test 验证

* docs: 更新文档,移除设置页面引用

- README.md: 移除 entrypoints/options/ 目录说明
- AGENTS.md: 更新项目结构描述,移除 options/ 入口点

* docs: add comprehensive page development standards (CODING_STANDARDS.md §11)

- 新增 §11 页面开发规范(10 个子节),覆盖目录结构分4级、入口组件命名统一为 Index、业务 Hook 内部结构顺序、常量/子组件/存储键/模式切换/布局约定等规范
- 新增 §11.9 页面开发检查清单(10 步逐项确认)
- 更新 AGENTS.md 和 copilot-instructions.md 同步引用新规范章节
- 修正 useLazyTranslation → useI18n 与实际代码保持一致

* refactor(pages): apply UI+Hook separation and unify component naming

- Dashboard, RightClickRestorer: rename page component to Index
- Jwt: extract useJwt.ts hook + JwtSection.tsx sub-component (127→76 lines)
- TextStatistics: extract useTextStatistics.ts hook (63→48 lines)
- JsonTools: extract useJsonTools.ts hook + constants.ts (199→109 lines)
- All pages now follow CODING_STANDARDS.md §11 pattern

* refactor(StorageCleaner): improve style consistency and checkbox alignment

- Group storage options, auto-refresh toggle, and clean button into unified card
- Remove card wrappers from StorageOptionsGrid and AutoRefreshToggle (now sections within parent card)
- Align OptionItem and Select All checkboxes with consistent px-3.5 padding
- Distinguish indeterminate vs checked checkbox states visually
- Standardize spacing (space-y-4), add select-none to page root
- Fix import path for StorageCleanerConfirm to relative ./

* fix(StorageCleaner, Base64Converter): fix layout padding and checkbox alignment

- StorageCleaner: add px-3.5 to grid container to prevent content sticking to border
- StorageCleaner: adjust select-all bar to pl-3.5 pr-7 for checkbox alignment with grid items
- Base64Converter: add px-2 to TextMode and Base64ConverterSection for consistent spacing

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: add initial CLAUDE.md file with reference to @AGENT.md

* docs: add comprehensive visual style guide

Add VISUAL_STYLE_GUIDE.md to standardize UI development:
- Design principles and keywords
- Complete color system (CSS variables, semantic usage)
- Typography hierarchy and patterns
- Spacing, layout, and container specs
- Border radius and shadow system
- Component specs (Button, Input, Card, Dialog, etc.)
- Interaction and animation guidelines
- Dark mode implementation rules
- Tool color palette and assignment
- Code organization standards
- Anti-patterns checklist
- Quick reference appendix

Update .gitignore to allow tracking the style guide.

* docs(AGENTS): update i18n section to reflect chrome.i18n migration

- Replace react-i18next docs with chrome.i18n API documentation
- Update translation file paths from i18n/locales/ to public/_locales/
- Update test mock description for chromeI18n
- Remove references to useLazyTranslation, vendor-i18n chunk, and snackbar

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(content): simplify content script structure

- Remove messageHandler.ts (6-line pass-through facade)
- Import contextMenuHandler directly from content.ts
- Replace local getI18nText() with shared getMessage() from utils/chromeI18n

Reduces content script files from 3 to 2 and eliminates i18n logic duplication.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(RightClickRestorer): simplify setDomain logic

Replace nested if/try/catch with a single ternary expression.
isUnsupportedPage() already handles invalid URLs, so new URL() is safe here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(background): extract main world injection script

Move the 62-line inline main world injection function from background.ts
to a dedicated utils/rightClickInjection.ts file.

This separates the RightClickRestorer's DOM patching logic from the
service worker's message routing concerns.

- background.ts: -62 lines, now delegates to imported function
- utils/rightClickInjection.ts: new file with mainWorldInjectionScript()

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(background): simplify RELOAD_TAB with setTimeout

Replace the 30-line chrome.alarms API implementation with a simple
setTimeout for delayed tab reloads.

Rationale: The only usage is a 1-second delay after storage cleaning.
Service workers don't go idle within 1 second, so alarms' persistence
benefit is unnecessary here.

- Removed: alarm naming, create/clear, listener register/unregister
- Added: 4-line setTimeout conditional

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(storageCleaner): extract executeScript helpers

Extract two generic helpers to eliminate repetitive executeScript boilerplate:

- runScript<T>(): for size/query operations (5 functions)
- runCleanScript(): for cleanup operations (5 functions)

Also add CLEAN_OPTION_KEYS constant shared by formatCleaningResult
and isEmptyResult.

- 428 lines → 379 lines (-49 lines)
- Eliminates ~100 lines of duplicated try/catch/result parsing

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(base64Converter): simplify utility functions

- Mark formatFileSize() as @deprecated, delegate to formatBytes
- Replace manual for-loop in base64ToBytes() with Uint8Array.from()
- Replace nested loops in sniffMimeFromBytes() with Array.every()

5 insertions(+), 23 deletions(-)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor: replace GlobalSnackbar with sonner toast

Remove the custom 373-line GlobalSnackbar component and its Provider/Hook
in favor of the already-installed sonner library.

Changes:
- Delete src/components/GlobalSnackbar.tsx (+ tests)
- Remove SnackbarProvider from popup/App.tsx and sidepanel/App.tsx
- Replace useSnackbar() calls with toast.success()/toast.error() in:
  - ImageUploader.tsx
  - useQrCode.ts
  - ParsePanel.tsx
  - LiveClock.tsx
- Update test mocks to use sonner instead of GlobalSnackbar

-554 lines, +43 lines (net -511 lines)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(RouterContainer): remove unnecessary useMemo and hoist constant

- Replace useMemo with direct expression for animationClass
- Move entryPointType to module scope (value is fixed after mount)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor: remove unnecessary useMemo calls across components

Replace over-cached simple computations with direct expressions:

- popup/App.tsx: getEntryPointType() and routerConfig (values are stable)
- Dashboard/index.tsx: visibleSet, visibleFeatures, recentFeatures
  (Set creation + array filter/map are cheaper than useMemo overhead)
- useTextStatistics.ts: getTextStats(text) (simple string processing)
- ThemeModeProvider.tsx: contextValue object (setMode is stable via useCallback)

-39 lines, +28 lines (net -11 lines)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor: unify formatBytes usage and simplify useContextMenuData

1. Remove formatSize/formatByteSize delegation functions:
   - storageCleaner.ts:formatSize (was just formatBytes wrapper)
   - textStatistics.ts:formatByteSize (was just formatBytes wrapper)
   - Update all callers to import formatBytes directly from @/utils/format

2. Simplify useContextMenuData hook:
   - Remove unnecessary useCallback wrapping
   - Inline checkAndConsumeData logic directly in useEffect
   - Eliminate callback->effect dependency cycle

-69 lines, +67 lines (net -2 lines, but removes 2 indirection layers)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(storageCleaner): simplify architecture and improve type semantics

- Remove RELOAD_TAB message chain, use chrome.tabs.reload() directly
- Rename IndexedDB label to '站点存储' for accuracy
- Introduce StorageSizeInfo type to distinguish bytes vs count
- Rename totalSize to totalBytes for clarity
- Merge runCleanScript into runScript to reduce duplication
- Rename CleaningResult.success to overallSuccess to avoid confusion
- Remove unused domain state and setDomain call

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor: replace require with import for JSON module in vitest.setup.ts

* refactor: simplify Timestamp and RightClickRestorer

Timestamp:
- Extract shared utilities (msToUnit, dayjsFromTimestamp, ModeType)
- Remove unnecessary useCallback/useMemo/React.memo
- Remove extra info section (relative time, ISO 8601, UTC)
- Fix missing translation key timestamp_unitS

RightClickRestorer:
- Remove website badge (右键已解锁 overlay)
- Remove mouse penetration logic for media elements
- Remove unnecessary useCallback
- Remove redundant setIsLoading call

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(StorageCleanerConfirm.test.tsx): update IndexedDB label to Chinese translation

---------

Co-authored-by: Ubuntu <ubuntu@localhost.localdomain>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 23:00:21 +08:00
雨霖铃 355ab0cf86 Merge remote-tracking branch 'origin/main' into develop 2026-05-29 22:59:10 +08:00
雨霖铃 13113f0ea2 refactor(StorageCleanerConfirm.test.tsx): update IndexedDB label to Chinese translation 2026-05-29 22:42:59 +08:00
雨霖铃 0d69695dbb refactor: simplify Timestamp and RightClickRestorer
Timestamp:
- Extract shared utilities (msToUnit, dayjsFromTimestamp, ModeType)
- Remove unnecessary useCallback/useMemo/React.memo
- Remove extra info section (relative time, ISO 8601, UTC)
- Fix missing translation key timestamp_unitS

RightClickRestorer:
- Remove website badge (右键已解锁 overlay)
- Remove mouse penetration logic for media elements
- Remove unnecessary useCallback
- Remove redundant setIsLoading call

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 22:40:11 +08:00
雨霖铃 093bf278da refactor: replace require with import for JSON module in vitest.setup.ts 2026-05-29 22:08:04 +08:00
雨霖铃 f9fbc79612 refactor(storageCleaner): simplify architecture and improve type semantics
- Remove RELOAD_TAB message chain, use chrome.tabs.reload() directly
- Rename IndexedDB label to '站点存储' for accuracy
- Introduce StorageSizeInfo type to distinguish bytes vs count
- Rename totalSize to totalBytes for clarity
- Merge runCleanScript into runScript to reduce duplication
- Rename CleaningResult.success to overallSuccess to avoid confusion
- Remove unused domain state and setDomain call

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 21:59:54 +08:00
雨霖铃 1b9f6d044d refactor: unify formatBytes usage and simplify useContextMenuData
1. Remove formatSize/formatByteSize delegation functions:
   - storageCleaner.ts:formatSize (was just formatBytes wrapper)
   - textStatistics.ts:formatByteSize (was just formatBytes wrapper)
   - Update all callers to import formatBytes directly from @/utils/format

2. Simplify useContextMenuData hook:
   - Remove unnecessary useCallback wrapping
   - Inline checkAndConsumeData logic directly in useEffect
   - Eliminate callback->effect dependency cycle

-69 lines, +67 lines (net -2 lines, but removes 2 indirection layers)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 21:08:27 +08:00
雨霖铃 b41cfb2a02 refactor: remove unnecessary useMemo calls across components
Replace over-cached simple computations with direct expressions:

- popup/App.tsx: getEntryPointType() and routerConfig (values are stable)
- Dashboard/index.tsx: visibleSet, visibleFeatures, recentFeatures
  (Set creation + array filter/map are cheaper than useMemo overhead)
- useTextStatistics.ts: getTextStats(text) (simple string processing)
- ThemeModeProvider.tsx: contextValue object (setMode is stable via useCallback)

-39 lines, +28 lines (net -11 lines)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 20:59:03 +08:00
雨霖铃 93019f56cf refactor(RouterContainer): remove unnecessary useMemo and hoist constant
- Replace useMemo with direct expression for animationClass
- Move entryPointType to module scope (value is fixed after mount)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 20:55:05 +08:00
雨霖铃 2727e60025 refactor: replace GlobalSnackbar with sonner toast
Remove the custom 373-line GlobalSnackbar component and its Provider/Hook
in favor of the already-installed sonner library.

Changes:
- Delete src/components/GlobalSnackbar.tsx (+ tests)
- Remove SnackbarProvider from popup/App.tsx and sidepanel/App.tsx
- Replace useSnackbar() calls with toast.success()/toast.error() in:
  - ImageUploader.tsx
  - useQrCode.ts
  - ParsePanel.tsx
  - LiveClock.tsx
- Update test mocks to use sonner instead of GlobalSnackbar

-554 lines, +43 lines (net -511 lines)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 20:47:16 +08:00
雨霖铃 c2e54b34cb refactor(base64Converter): simplify utility functions
- Mark formatFileSize() as @deprecated, delegate to formatBytes
- Replace manual for-loop in base64ToBytes() with Uint8Array.from()
- Replace nested loops in sniffMimeFromBytes() with Array.every()

5 insertions(+), 23 deletions(-)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 20:38:06 +08:00
雨霖铃 dbdec710ef refactor(storageCleaner): extract executeScript helpers
Extract two generic helpers to eliminate repetitive executeScript boilerplate:

- runScript<T>(): for size/query operations (5 functions)
- runCleanScript(): for cleanup operations (5 functions)

Also add CLEAN_OPTION_KEYS constant shared by formatCleaningResult
and isEmptyResult.

- 428 lines → 379 lines (-49 lines)
- Eliminates ~100 lines of duplicated try/catch/result parsing

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 20:32:58 +08:00
雨霖铃 7bd52bfc2b refactor(background): simplify RELOAD_TAB with setTimeout
Replace the 30-line chrome.alarms API implementation with a simple
setTimeout for delayed tab reloads.

Rationale: The only usage is a 1-second delay after storage cleaning.
Service workers don't go idle within 1 second, so alarms' persistence
benefit is unnecessary here.

- Removed: alarm naming, create/clear, listener register/unregister
- Added: 4-line setTimeout conditional

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 20:22:44 +08:00
雨霖铃 c9fe27a26b refactor(background): extract main world injection script
Move the 62-line inline main world injection function from background.ts
to a dedicated utils/rightClickInjection.ts file.

This separates the RightClickRestorer's DOM patching logic from the
service worker's message routing concerns.

- background.ts: -62 lines, now delegates to imported function
- utils/rightClickInjection.ts: new file with mainWorldInjectionScript()

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 20:20:11 +08:00
雨霖铃 e97f4cae9a refactor(RightClickRestorer): simplify setDomain logic
Replace nested if/try/catch with a single ternary expression.
isUnsupportedPage() already handles invalid URLs, so new URL() is safe here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 20:15:54 +08:00
雨霖铃 21a6430f0d refactor(content): simplify content script structure
- Remove messageHandler.ts (6-line pass-through facade)
- Import contextMenuHandler directly from content.ts
- Replace local getI18nText() with shared getMessage() from utils/chromeI18n

Reduces content script files from 3 to 2 and eliminates i18n logic duplication.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 20:10:45 +08:00
雨霖铃 dff2705ef8 docs(AGENTS): update i18n section to reflect chrome.i18n migration
- Replace react-i18next docs with chrome.i18n API documentation
- Update translation file paths from i18n/locales/ to public/_locales/
- Update test mock description for chromeI18n
- Remove references to useLazyTranslation, vendor-i18n chunk, and snackbar

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 19:56:52 +08:00
雨霖铃 f8c48a3047 docs: add comprehensive visual style guide
Add VISUAL_STYLE_GUIDE.md to standardize UI development:
- Design principles and keywords
- Complete color system (CSS variables, semantic usage)
- Typography hierarchy and patterns
- Spacing, layout, and container specs
- Border radius and shadow system
- Component specs (Button, Input, Card, Dialog, etc.)
- Interaction and animation guidelines
- Dark mode implementation rules
- Tool color palette and assignment
- Code organization standards
- Anti-patterns checklist
- Quick reference appendix

Update .gitignore to allow tracking the style guide.
2026-05-29 19:51:44 +08:00
雨霖铃 488e2e8e49 chore: add initial CLAUDE.md file with reference to @AGENT.md 2026-05-29 19:51:16 +08:00
雨霖铃 98abe59731 fix(StorageCleaner, Base64Converter): fix layout padding and checkbox alignment
- StorageCleaner: add px-3.5 to grid container to prevent content sticking to border
- StorageCleaner: adjust select-all bar to pl-3.5 pr-7 for checkbox alignment with grid items
- Base64Converter: add px-2 to TextMode and Base64ConverterSection for consistent spacing

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 19:38:28 +08:00
雨霖铃 77168e2652 refactor(StorageCleaner): improve style consistency and checkbox alignment
- Group storage options, auto-refresh toggle, and clean button into unified card
- Remove card wrappers from StorageOptionsGrid and AutoRefreshToggle (now sections within parent card)
- Align OptionItem and Select All checkboxes with consistent px-3.5 padding
- Distinguish indeterminate vs checked checkbox states visually
- Standardize spacing (space-y-4), add select-none to page root
- Fix import path for StorageCleanerConfirm to relative ./
2026-05-28 23:43:02 +08:00
雨霖铃 4f7e402dba refactor(pages): apply UI+Hook separation and unify component naming
- Dashboard, RightClickRestorer: rename page component to Index
- Jwt: extract useJwt.ts hook + JwtSection.tsx sub-component (127→76 lines)
- TextStatistics: extract useTextStatistics.ts hook (63→48 lines)
- JsonTools: extract useJsonTools.ts hook + constants.ts (199→109 lines)
- All pages now follow CODING_STANDARDS.md §11 pattern
2026-05-28 23:18:58 +08:00
雨霖铃 e33761381d docs: add comprehensive page development standards (CODING_STANDARDS.md §11)
- 新增 §11 页面开发规范(10 个子节),覆盖目录结构分4级、入口组件命名统一为 Index、业务 Hook 内部结构顺序、常量/子组件/存储键/模式切换/布局约定等规范
- 新增 §11.9 页面开发检查清单(10 步逐项确认)
- 更新 AGENTS.md 和 copilot-instructions.md 同步引用新规范章节
- 修正 useLazyTranslation → useI18n 与实际代码保持一致
2026-05-28 23:09:14 +08:00
LingandRX 945780def8 Develop (#55)
feat: optimize dashboard/search UX and simplify extension architecture

Redesign Dashboard with compact tool grid and recently used tools
Improve TopBar search UX with Cmd/Ctrl+K shortcut and better history navigation
Reorganize project structure into src/
Migrate i18n from react-i18next to chrome.i18n
Remove runtime language switch and settings page
Remove HTML/Markdown conversion tools
Clean up unused code, dead animations, redundant comments, and imports
Improve component consistency with shadcn/ui patterns
Replace hardcoded strings/colors with i18n tokens and theme tokens
Add comprehensive project documentation and coding standards
Fix CI artifact upload workflow and multiple TypeScript/test issues

Includes various refactors, UI polish, i18n cleanup, CI improvements,
and maintenance updates across the codebase.
2026-05-28 22:39:25 +08:00
雨霖铃 6bddc4fcfc Merge remote-tracking branch 'origin/main' into develop 2026-05-28 22:35:39 +08:00
雨霖铃 3df45f491c docs: 更新文档,移除设置页面引用
- README.md: 移除 entrypoints/options/ 目录说明
- AGENTS.md: 更新项目结构描述,移除 options/ 入口点
2026-05-28 22:20:16 +08:00
雨霖铃 0054e97a0a refactor: 移除设置页面
- 删除 options 入口点目录及相关文件
- 移除 wxt.config.ts 中 options_ui 配置
- 移除 TopBar 组件中的设置按钮和 onOpenOptions prop
- 移除 popup/sidepanel App.tsx 中的 handleOpenOptions
- 移除相关测试中的设置页面引用
- 移除 messages.json 中设置相关翻译键
- 通过 lint、typecheck、test 验证
2026-05-28 22:15:56 +08:00
雨霖铃 97159cb799 fix: useI18n fallback for empty string translations from chrome.i18n 2026-05-28 21:53:35 +08:00
雨霖铃 5175677b24 feat: improve search history to show recently used tools with direct navigation
- Store feature keys (PageType) in history instead of translated labels
- Show icon + name + description in history items (same as search results)
- Click or Enter on history item navigates directly to the tool page
- Remove History icon import (no longer needed)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-28 21:38:18 +08:00
雨霖铃 3400ffc9dc feat: optimize TopBar search with Cmd+K shortcut and improved styling
- Add global Cmd/Ctrl+K keyboard shortcut to focus search input
- Show keyboard shortcut hint (⌘K) in search input when empty
- Improve input styling: rounded-lg, softer borders, focus transitions
- Improve dropdown: rounded-lg, larger shadow, better spacing
- Larger result icons (h-8 w-8) with rounded-lg containers
- Refined history section with uppercase label and muted icon
- Search icon fades in on focus via group-focus-within

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-28 21:34:16 +08:00
雨霖铃 4955f617ee fix: remove redundant search input from Dashboard (header already has one)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-28 21:28:52 +08:00
雨霖铃 2cb5b0c1e7 feat: replace ToolCard grid with compact icon+name grid on Dashboard
- Replace large ToolCard layout with dense grid (3-6 columns responsive)
- Each item shows icon + tool name in a compact tile
- Hover highlights icon and text with smooth transitions
- Remove ToolCard import from Dashboard (component retained for reuse)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-28 21:25:47 +08:00
雨霖铃 c6cadda95b feat: redesign Dashboard layout with search and recently used tools
- Add search input at top to filter tools by name/description
- Add 'recently used' section showing last 3 used tools as compact chips
- Add 'all tools' section header for the tool card grid
- Track tool usage via new 'app/recentlyUsedTools' storage key
- Update navigateTo to record recently used tools (max 3, LRU order)
- Add i18n keys: dashboard_searchPlaceholder, dashboard_recentlyUsed, dashboard_allTools

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-28 21:22:30 +08:00
雨霖铃 067a20fb0a feat: remove HTML-Markdown conversion features (htmlToMarkdown & markdownToHtml)
Removed both conversion tools and all related code:
- Deleted page components: HtmlToMarkdown, MarkdownToHtml
- Deleted utility functions: htmlToMarkdown.ts, markdownToHtml.ts
- Deleted unit tests for both utilities
- Removed feature configs and PageType entries from storage.d.ts
- Removed preview mode types and StorageSchema keys
- Removed all i18n translation keys from messages.json
- Removed vendor-markdown chunk from wxt.config.ts
- Removed marked dependency from package.json
- Updated feature count in tests (11 -> 9, page order 10 -> 8)
- Updated README, AGENTS.md, and copilot-instructions.md

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-28 21:08:15 +08:00
雨霖铃 ca4bfc33fd refactor: clean up unused code and simplify imports
- Remove unused imports and variables across 35 files
- Simplify component logic and remove dead code
- Clean up test files by removing unnecessary setup
- Streamline CI workflow configuration

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-28 20:57:08 +08:00
雨霖铃 e140af7698 refactor: remove meaningless comments
Remove 50+ noise comments across the codebase:
- AI-generated verbose prose (💡 emoji, '超进化', '大闸', etc.)
- Comments that restate what the code obviously does
- Comments about deleted code
- Import-level noise comments
- Overly verbose Chinese section markers

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-28 20:37:52 +08:00
雨霖铃 b79fe7dd49 refactor: reorganize directory structure into src/
Move all source code directories into src/ for cleaner project structure:
- pages/, components/, utils/, config/, providers/, types/, lib/, assets/, entrypoints/ → src/
- Use WXT srcDir config to resolve @/ alias to src/
- Update tsconfig, vitest, eslint, tailwind configs
- Remove scattered README.md files from subdirectories
- Update documentation (AGENTS.md, CODING_STANDARDS.md, README.md)
- Fix pre-existing lint error in RouterProvider.tsx

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-28 20:20:45 +08:00
雨霖铃 9903483f42 Merge branch 'feature/chrome-i18n' into develop
迁移 i18n 系统从 react-i18next 到 chrome.i18n:
- 移除 react-i18next 依赖,使用 chrome.i18n API
- 生成 _locales/zh/messages.json(245 个翻译 key)
- 清理未使用的翻译 key
- 移除语言切换按钮和英文翻译
2026-05-28 19:50:51 +08:00
雨霖铃 08911d4cba chore: 清理未使用的 i18n key(322 → 245)
移除 77 个未被源码引用的翻译 key,包括:
- jsonFormat_* (17): 未接入的子模式标题和按钮
- buttons_* (15): 代码使用 common: 命名空间格式,直接 key 未引用
- qrCode_* (14): 未使用的页面标题和状态提示
- timestamp/common/storageCleaner/jsonDiff 等零散未用 key

保留了通过模板字面量动态引用的 key(storageCleaner_options_*、common_buttons_themeMode_*、jsonFormat_*InputPlaceholder 等)。

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-28 19:48:38 +08:00
雨霖铃 1daf049737 refactor: 移除英文翻译,仅保留中文
删除 public/_locales/en/ 目录,清理 chromeI18n.ts 中未使用的语言切换相关导出。

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-28 19:27:15 +08:00
雨霖铃 8b6bac9f2c refactor: 移除语言切换按钮
chrome.i18n 语言由浏览器设置决定,不支持运行时切换,移除 TopBar 中的语言切换按钮及相关逻辑。

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-28 19:23:57 +08:00
雨霖铃 aa23a263fe refactor: 迁移 i18n 系统从 react-i18next 到 chrome.i18n
- 移除 react-i18next、i18next 及相关依赖
- 删除旧的 i18n/ 目录和 useLazyTranslation 工具
- 新增 utils/chromeI18n.ts 类型安全 wrapper(useI18n Hook + getMessage)
- 生成 public/_locales/{zh,en}/messages.json(298 个翻译 key)
- 批量更新 39+ 组件文件的导入和翻译调用
- 转换翻译键格式:namespace:key → namespace_key
- 修复 ErrorBoundary/PageErrorBoundary 从 withTranslation HOC 改为直接调用 getMessage
- 更新 vitest.setup.ts mock 加载实际翻译文本
- 修复 11 个测试文件的断言以匹配中文翻译
- TypeScript、ESLint、547 项测试全部通过

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-28 19:21:54 +08:00
LingandRX d4c29a1bf4 Develop (#54)
* fix(ci): fix release workflow artifact upload issue

- Split artifact upload into 3 separate steps (Chrome, Firefox, Source)
- Update download steps to match new artifact names
- Add if-no-files-found: error for better error handling
- Fix Node.js 20 compatibility issue with upload-artifact@v4 glob pattern

* docs: 更新 AGENTS.md,添加测试工具和翻译文件结构说明

* docs: 更新 AGENTS.md,完善 CI 步骤和存储键名格式说明

* fix: remove unnecessary animations from StorageCleaner page

- Remove entrance animations (animate-in, fade-in, zoom-in) from all components
- Remove scale effect (active:scale-[0.99]) from clean button
- Remove transition effects (transition-all, transition-colors) from OptionItem, StorageOptionsGrid, AutoRefreshToggle
- Remove bouncing animation from error icon
- Remove pulsing animation from warning banner
- Keep animate-spin on button loading spinner as functional indicator

* fix: remove unnecessary animations from RightClickRestorer page

* fix: remove all unnecessary animations from all pages

- Remove dead code animations (animate-in, fade-in, slide-in, zoom-in, shake) from tailwindcss-animate plugin (not installed)
- Remove decorative transition effects (transition-all, transition-colors) from all page components
- Remove scale effects (active:scale-95) from buttons
- Remove bounce animations (animate-bounce) from icons
- Keep functional animate-spin on loading spinners as they provide essential loading feedback

Affected pages: Dashboard, Timestamp, Jwt, JsonTools, Base64Converter, HtmlToMarkdown, MarkdownToHtml, QrCode, TextStatistics

* docs: 添加项目文档(copilot-instructions、编码规范、各目录 README)

- 新增 .github/copilot-instructions.md:Copilot 指令文件,涵盖构建命令、CI 流水线、项目架构、关键规范
- 新增 .github/CODING_STANDARDS.md:完整的代码编写规范文档(TypeScript、React、样式、错误处理、命名、测试、Hook、i18n、存储等 12 个章节)
- 新增 11 个目录的 README.md:components、components/ui、config、entrypoints、i18n、lib、pages、providers、public、src、types、utils
- 更新 AGENTS.md:补充页面组件模式说明和 components/ui 目录

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(CopyButton): use shadcn buttonVariants instead of duplicated variant classes

Remove hand-written variantClasses/sizeClasses that duplicated shadcn's
buttonVariants. Now extends ButtonProps and imports buttonVariants from
components/ui/button for consistent styling.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(ErrorBoundary): use shadcn destructive tokens instead of hardcoded red colors

Replace hardcoded red-200/red-50/red-600 with border-destructive,
bg-destructive/5, text-destructive. Use variant='destructive' on
Button. Align error log styling with PageErrorBoundary.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(TextInputArea): replace hardcoded Chinese strings with i18n t() calls

Use existing translation keys for clear success, copy success, and
copy error messages instead of hardcoded Chinese text.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(QrCodePreview): use shadcn Button instead of manual button styles

Replace hand-written button class strings with shadcn Button component
using outline and default variants. Removes ~15 lines of duplicated
Tailwind classes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs(GlobalSnackbar): clarify relationship with sonner toast

Add note explaining when to use GlobalSnackbar (provider context,
severity levels, custom positioning) vs sonner toast (simple one-off
messages).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(ImageUploader): wrap handleClearFile with useCallback

Prevents unnecessary re-renders by memoizing the callback, consistent
with handleFileChange which already uses useCallback.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(SwitchButtonGroup): remove redundant containerPadding conditional

Both branches of the ternary returned 'p-1'. Inline the constant
directly into the className string.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(TopBar): add missing openExtensionPage import

The function was called in handleOpenInTab but never imported from
utils/chromeTabs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(TopBar): add selectedIndex boundary protection in keyboard nav

Guard against out-of-bounds access when search results change during
keyboard navigation. Check selectedIndex < totalItems before accessing
arrays.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(RouterContainer): remove unnecessary empty-dep useMemo

getEntryPointType() is a simple config getter. Replace useMemo(() => ..., [])
with a direct call — the empty dep array made the memo pointless.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(i18n): add missing translation keys for error boundaries and copy messages

Add keys for errorBoundary, pageErrorBoundary, router, and
messages.copyEmpty in both zh and en locales.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(CopyButton): replace hardcoded Chinese strings with i18n t() calls

Use useTranslation('common') for tooltip, copy empty/success/error
toast messages.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(ErrorBoundary): replace hardcoded Chinese with i18n via withTranslation HOC

Use react-i18next withTranslation HOC for class component to translate
title, description, and refresh button text.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(PageErrorBoundary): replace hardcoded Chinese with i18n via withTranslation HOC

Use react-i18next withTranslation HOC for class component to translate
title, description, and retry button text.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(RouterContainer): replace hardcoded Chinese with i18n t() calls

Use useTranslation('common') for 404 page title and description with
entryPointType interpolation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(pages): 修复 TypeScript 错误、硬编码字符串,移除死代码

- 修复 LiveClock.tsx CopyButton size 属性类型错误 (small → sm)
- 修复 ToolCard.tsx 紫色 RGB 值拼写错误 (147,51,2 purple → 147,51,232)
- 替换 4 处硬编码中文为 i18n 调用 (StorageCleaner, JsonTools)
- MarkdownToHtml 预览链接色改为 CSS 变量以支持暗黑模式
- 移除 Base64Converter 中已被 Base64ConverterSection 替代的死代码
  (FileMode.tsx, ImageMode.tsx 及其测试文件)
- 更新 README.md 移除对已删除文件的引用

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(CopyButton, LiveClock): 修改按钮大小为 'icon',移除硬编码颜色配置

* refactor(TextInputArea): 复用 CopyButton 组件替换内联复制逻辑

* fix(test): 修复 CopyButton mock 未捕获 clipboard writeText 异常

---------

Co-authored-by: Ubuntu <ubuntu@localhost.localdomain>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-28 00:40:54 +08:00
雨霖铃 64e23994a6 fix(test): 修复 CopyButton mock 未捕获 clipboard writeText 异常 2026-05-28 00:39:47 +08:00
雨霖铃 528621f16e fix(tests): 修复 CI 失败的测试 - 补充 withTranslation 和 CopyButton mock
- vitest.setup.ts: react-i18next mock 添加 withTranslation HOC(读取 zh/common.json 翻译)
- vitest.setup.ts: 添加 @/components/CopyButton 全局 mock
- App 测试: 补充 withTranslation 导出
- TextMode 测试: 修复多个 CopyButton 实例导致的 getByTestId 冲突,改用 getByRole
2026-05-28 00:13:09 +08:00
雨霖铃 c735a0aa1a refactor(TextInputArea): 复用 CopyButton 组件替换内联复制逻辑 2026-05-27 22:55:28 +08:00
雨霖铃 62c880b0a8 fix(CopyButton, LiveClock): 修改按钮大小为 'icon',移除硬编码颜色配置 2026-05-27 17:07:39 +08:00
雨霖铃 e263b63ced fix(pages): 修复 TypeScript 错误、硬编码字符串,移除死代码
- 修复 LiveClock.tsx CopyButton size 属性类型错误 (small → sm)
- 修复 ToolCard.tsx 紫色 RGB 值拼写错误 (147,51,2 purple → 147,51,232)
- 替换 4 处硬编码中文为 i18n 调用 (StorageCleaner, JsonTools)
- MarkdownToHtml 预览链接色改为 CSS 变量以支持暗黑模式
- 移除 Base64Converter 中已被 Base64ConverterSection 替代的死代码
  (FileMode.tsx, ImageMode.tsx 及其测试文件)
- 更新 README.md 移除对已删除文件的引用

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-27 16:56:27 +08:00
雨霖铃 1e4b6eddc7 fix(RouterContainer): replace hardcoded Chinese with i18n t() calls
Use useTranslation('common') for 404 page title and description with
entryPointType interpolation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-27 14:14:19 +08:00
雨霖铃 4316bb57d2 fix(PageErrorBoundary): replace hardcoded Chinese with i18n via withTranslation HOC
Use react-i18next withTranslation HOC for class component to translate
title, description, and retry button text.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-27 14:11:44 +08:00
雨霖铃 f40485e2dc fix(ErrorBoundary): replace hardcoded Chinese with i18n via withTranslation HOC
Use react-i18next withTranslation HOC for class component to translate
title, description, and refresh button text.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-27 14:10:11 +08:00
雨霖铃 0908358f97 fix(CopyButton): replace hardcoded Chinese strings with i18n t() calls
Use useTranslation('common') for tooltip, copy empty/success/error
toast messages.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-27 14:08:10 +08:00
雨霖铃 a3f89d07ea feat(i18n): add missing translation keys for error boundaries and copy messages
Add keys for errorBoundary, pageErrorBoundary, router, and
messages.copyEmpty in both zh and en locales.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-27 14:06:19 +08:00
雨霖铃 003dad5d8b fix(RouterContainer): remove unnecessary empty-dep useMemo
getEntryPointType() is a simple config getter. Replace useMemo(() => ..., [])
with a direct call — the empty dep array made the memo pointless.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-27 13:55:54 +08:00
雨霖铃 27d3360f9d fix(TopBar): add selectedIndex boundary protection in keyboard nav
Guard against out-of-bounds access when search results change during
keyboard navigation. Check selectedIndex < totalItems before accessing
arrays.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-27 13:54:38 +08:00
雨霖铃 67629263f9 fix(TopBar): add missing openExtensionPage import
The function was called in handleOpenInTab but never imported from
utils/chromeTabs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-27 13:53:48 +08:00
雨霖铃 8b58dcf02e fix(SwitchButtonGroup): remove redundant containerPadding conditional
Both branches of the ternary returned 'p-1'. Inline the constant
directly into the className string.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-27 13:53:11 +08:00
雨霖铃 2e2f6e9835 fix(ImageUploader): wrap handleClearFile with useCallback
Prevents unnecessary re-renders by memoizing the callback, consistent
with handleFileChange which already uses useCallback.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-27 13:52:11 +08:00
雨霖铃 cdad9bda68 docs(GlobalSnackbar): clarify relationship with sonner toast
Add note explaining when to use GlobalSnackbar (provider context,
severity levels, custom positioning) vs sonner toast (simple one-off
messages).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-27 13:51:30 +08:00
雨霖铃 fae8dea971 fix(QrCodePreview): use shadcn Button instead of manual button styles
Replace hand-written button class strings with shadcn Button component
using outline and default variants. Removes ~15 lines of duplicated
Tailwind classes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-27 13:50:36 +08:00
雨霖铃 e0de152645 fix(TextInputArea): replace hardcoded Chinese strings with i18n t() calls
Use existing translation keys for clear success, copy success, and
copy error messages instead of hardcoded Chinese text.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-27 13:49:31 +08:00
雨霖铃 e005d4d95d fix(ErrorBoundary): use shadcn destructive tokens instead of hardcoded red colors
Replace hardcoded red-200/red-50/red-600 with border-destructive,
bg-destructive/5, text-destructive. Use variant='destructive' on
Button. Align error log styling with PageErrorBoundary.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-27 13:48:30 +08:00
雨霖铃 601260797e fix(CopyButton): use shadcn buttonVariants instead of duplicated variant classes
Remove hand-written variantClasses/sizeClasses that duplicated shadcn's
buttonVariants. Now extends ButtonProps and imports buttonVariants from
components/ui/button for consistent styling.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-27 13:48:02 +08:00
雨霖铃 c61b599287 docs: 添加项目文档(copilot-instructions、编码规范、各目录 README)
- 新增 .github/copilot-instructions.md:Copilot 指令文件,涵盖构建命令、CI 流水线、项目架构、关键规范
- 新增 .github/CODING_STANDARDS.md:完整的代码编写规范文档(TypeScript、React、样式、错误处理、命名、测试、Hook、i18n、存储等 12 个章节)
- 新增 11 个目录的 README.md:components、components/ui、config、entrypoints、i18n、lib、pages、providers、public、src、types、utils
- 更新 AGENTS.md:补充页面组件模式说明和 components/ui 目录

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-27 11:54:21 +08:00
LingandRX 42d3fedbaa develop → main: 合并开发分支 (#51)
* fix(ci): fix release workflow artifact upload issue

- Split artifact upload into 3 separate steps (Chrome, Firefox, Source)
- Update download steps to match new artifact names
- Add if-no-files-found: error for better error handling
- Fix Node.js 20 compatibility issue with upload-artifact@v4 glob pattern

* docs: 更新 AGENTS.md,添加测试工具和翻译文件结构说明

* docs: 更新 AGENTS.md,完善 CI 步骤和存储键名格式说明

* fix: remove unnecessary animations from StorageCleaner page

- Remove entrance animations (animate-in, fade-in, zoom-in) from all components
- Remove scale effect (active:scale-[0.99]) from clean button
- Remove transition effects (transition-all, transition-colors) from OptionItem, StorageOptionsGrid, AutoRefreshToggle
- Remove bouncing animation from error icon
- Remove pulsing animation from warning banner
- Keep animate-spin on button loading spinner as functional indicator

* fix: remove unnecessary animations from RightClickRestorer page

* fix: remove all unnecessary animations from all pages

- Remove dead code animations (animate-in, fade-in, slide-in, zoom-in, shake) from tailwindcss-animate plugin (not installed)
- Remove decorative transition effects (transition-all, transition-colors) from all page components
- Remove scale effects (active:scale-95) from buttons
- Remove bounce animations (animate-bounce) from icons
- Keep functional animate-spin on loading spinners as they provide essential loading feedback

Affected pages: Dashboard, Timestamp, Jwt, JsonTools, Base64Converter, HtmlToMarkdown, MarkdownToHtml, QrCode, TextStatistics

---------

Co-authored-by: Ubuntu <ubuntu@localhost.localdomain>
2026-05-26 15:08:53 +08:00
雨霖铃 0f3b6c4cd8 fix: remove all unnecessary animations from all pages
- Remove dead code animations (animate-in, fade-in, slide-in, zoom-in, shake) from tailwindcss-animate plugin (not installed)
- Remove decorative transition effects (transition-all, transition-colors) from all page components
- Remove scale effects (active:scale-95) from buttons
- Remove bounce animations (animate-bounce) from icons
- Keep functional animate-spin on loading spinners as they provide essential loading feedback

Affected pages: Dashboard, Timestamp, Jwt, JsonTools, Base64Converter, HtmlToMarkdown, MarkdownToHtml, QrCode, TextStatistics
2026-05-25 20:29:12 +08:00
雨霖铃 af2262d2c9 fix: remove unnecessary animations from RightClickRestorer page 2026-05-25 20:15:23 +08:00
雨霖铃 e5ae802ab2 fix: remove unnecessary animations from StorageCleaner page
- Remove entrance animations (animate-in, fade-in, zoom-in) from all components
- Remove scale effect (active:scale-[0.99]) from clean button
- Remove transition effects (transition-all, transition-colors) from OptionItem, StorageOptionsGrid, AutoRefreshToggle
- Remove bouncing animation from error icon
- Remove pulsing animation from warning banner
- Keep animate-spin on button loading spinner as functional indicator
2026-05-25 20:11:39 +08:00
雨霖铃 dec43f89da docs: 更新 AGENTS.md,完善 CI 步骤和存储键名格式说明 2026-05-25 19:21:22 +08:00
雨霖铃 61e741f198 docs: 更新 AGENTS.md,添加测试工具和翻译文件结构说明 2026-05-25 19:15:31 +08:00
雨霖铃 c98f767809 Merge branch 'main' into develop 2026-05-24 08:44:32 +08:00
LingandRX 92d524d832 fix(tsconfig): remove unused baseUrl and paths configuration (#50) 2026-05-24 08:44:04 +08:00
Ubuntu 63f47fd2b1 Merge branch 'main' into develop 2026-05-23 18:58:15 +08:00
LingandRX 6abdf4add4 fix(ci): sync manifest version from tag before build (#49)
The release workflow was always building with the hardcoded version
from package.json (1.0.0), causing Chrome Web Store to reject uploads
because 1.0.0 equals the already-published 1.0 (trailing zeros ignored).

Added a step to extract the version from the git tag and update
package.json before the build, so the zip contains the correct version.

Co-authored-by: Ubuntu <ubuntu@localhost.localdomain>
2026-05-23 18:58:08 +08:00
Ubuntu e95ca1297c merge: resolve conflict by taking main version of release.yml 2026-05-23 14:46:44 +08:00
LingandRX a0d6e76cad Merge pull request #48 from LingandRX/hotfix/fix-release-workflow
fix(ci): fix release workflow artifact upload
2026-05-23 14:46:05 +08:00
Ubuntu cc6d414d10 fix(ci): fix release workflow artifact upload
- Copy zip files to fixed directory before upload to avoid glob pattern issues
- Add debug steps to verify output and downloaded artifacts
- Fix Node.js 20 compatibility issue with upload-artifact@v4
2026-05-23 14:43:36 +08:00
Ubuntu c8ad2a2283 fix(ci): fix release workflow artifact upload issue
- Split artifact upload into 3 separate steps (Chrome, Firefox, Source)
- Update download steps to match new artifact names
- Add if-no-files-found: error for better error handling
- Fix Node.js 20 compatibility issue with upload-artifact@v4 glob pattern
2026-05-23 14:37:39 +08:00
LingandRX 5daa151b5e Refactor GitHub Actions workflow for clarity and efficiency (#46) 2026-05-23 14:27:57 +08:00
LingandRX e93b5c817f Merge pull request #45 from LingandRX/develop
chore: release v1.0.4
2026-05-23 14:15:07 +08:00
雨霖铃 d678f14549 ci(release): fix typecheck command and add missing WXT prepare steps
- Replace non-existent
pm run compile with correct
pm run typecheck
- Add
px wxt prepare before typecheck and test steps (aligned with ci.yml)
- Update AGENTS.md to remove fixed issue note
2026-05-23 14:13:27 +08:00
LingandRX c95e028482 Merge pull request #44 from LingandRX/develop
chore: release v1.0.4
2026-05-23 14:07:13 +08:00
雨霖铃 8ec36ac3b3 test: fix i18n namespace assertions in Base64Converter and QrCode tests
Align test selectors with useLazyTranslation mock which prefixes keys
with namespace (e.g. base64Converter:encode). Also remove obsolete
page title assertions for components that no longer render titles.
2026-05-23 14:04:10 +08:00
雨霖铃 262c5e9255 Merge remote-tracking branch 'origin/main' into develop 2026-05-23 13:39:28 +08:00
雨霖铃 e4993cb0ce docs: update README with current project state
- Add Right Click Restorer feature description
- Remove non-existent pageTheme.ts from project structure
- Update UI components to mention shadcn/ui
- Add missing permissions: contextMenus, alarms, unlimitedStorage
- Clarify permission descriptions
2026-05-23 13:23:53 +08:00
雨霖铃 0d60961a82 docs: update AGENTS.md outdated info
- Remove non-existent config/pageTheme.ts and config/theme.ts
- Update style guide from MUI sx to Tailwind CSS + shadcn/ui
- Replace MUI with Tailwind CSS and shadcn/ui in tech stack
2026-05-23 13:21:50 +08:00
雨霖铃 df3eee8749 refactor: 移除所有页面的 PageHeader 组件
删除 PageHeader 组件及其测试文件,清理相关不再使用的导入和变量。
2026-05-23 13:15:25 +08:00
雨霖铃 cf05df7022 fix(i18n): replace useTranslation with useLazyTranslation in Base64Converter
Fix missing translations by ensuring base64Converter namespace is dynamically loaded.
2026-05-23 13:07:31 +08:00
雨霖铃 e0888c7fb3 refactor: remove remaining config/pageTheme dependencies
- Replace base64ConverterPageStyles.primaryColor with inline text-indigo-800
- Replace qrCodePageStyles.primaryColor with inline text-green-700
- Remove obsolete page-error-boundary design doc
2026-05-23 12:58:35 +08:00
雨霖铃 8b2bca9eef refactor(timestamp): migrate pageTheme constants to Timestamp feature
Remove config/pageTheme.ts and move DATE_FORMAT, ZONES, UnitType, ZoneType
to pages/Timestamp/constants.ts for better feature cohesion.
2026-05-23 12:57:51 +08:00
雨霖铃 6bac88fbc4 feat(right-click-restorer): add unsupported page detection
- Detect and block chrome://, chrome-extension://, about://, edge://, brave:// pages
- Show 'Unsupported' badge with warning icon and disabled button
- Add 'unsupported' and 'unsupportedDesc' i18n keys (zh/en)
- Update useRightClickRestorer hook with isUnsupported state
- Add unit tests for unsupported page behavior
- Update page UI to render conditional unsupported layout
2026-05-23 12:46:20 +08:00
雨霖铃 4c08e17b19 fix(right-click-restorer): prevent domain overflow in domain card
- Add truncate/min-w-0/flex-1 to domain code block for ellipsis
- Add shrink-0 to Badge to prevent compression
- Add gap-2 for consistent spacing
2026-05-23 12:41:42 +08:00
雨霖铃 fa0f1bdcad feat: add right-click restorer for current site
- Add RightClickRestorer page with active unlock trigger
- Content script with dual-world strategy (isolated + main world)
- Monkey patch preventDefault/stopPropagation via background executeScript
- Overlay penetration for pointer-events blocking layers
- Delegated main-world injection to avoid CSP restrictions
- Async message handling for reliable state updates
- i18n translations (zh/en) and unit tests
2026-05-23 12:39:14 +08:00
雨霖铃 2d7bb8fc2c refactor: update background.ts comments for clarity and enhance alarm handling in reload logic; add alarms permission in wxt.config.ts 2026-05-23 10:08:12 +08:00
雨霖铃 a0e6871b16 refactor: rename autoRefresh to reloadAfterClean for clarity and consistency across components 2026-05-23 10:02:02 +08:00
雨霖铃 3adfe95234 refactor: update docs with new features, revised tech stack, and CI changes 2026-05-23 00:17:24 +08:00
LingandRX efbed7cf53 feat: Tailwind CSS/shadcn/ui 迁移 & 右键菜单功能 (#42)
* feat: 添加 contextMenus 权限和消息类型定义

* feat: 实现右键菜单注册与点击分流逻辑

- 新增 utils/contextMenu.ts 封装菜单配置和解析函数
- 在 background.ts 监听 onInstalled 初始化菜单
- 实现 contextMenus.onClicked 点击事件处理
- 分流逻辑:优先发送到侧边栏,否则打开 options 页面
- 添加 contextMenu 单元测试(15个测试用例)
- 更新 vitest.setup.ts 添加 contextMenus mock

* feat: 实现 Content Script 原位轻量提示 UI

- 新增 uiPopover.ts 实现原位弹窗组件(深色主题、自动定位、自动隐藏)
- 新增 contextMenuHandler.ts 处理右键菜单消息
- 时间戳转换:在点击位置显示转换结果
- 文本统计:显示字符/单词/行数/字节统计
- 更新 messageHandler.ts 挂载右键菜单消息监听

* feat: 实现 React 页面层右键菜单数据联动

- 新增 useContextMenuData Hook 处理右键菜单数据传递
- 重构 Jwt/Base64Converter/TextStatistics/QrCode 页面接收右键数据
- RouterProvider 支持从 URL 参数解析右键菜单数据
- 添加 contextMenu/pendingData 存储键到 StorageSchema
- 添加 useContextMenuData 单元测试(12个测试用例)

* test: 添加 background 单元测试与边界情况处理

- 新增 entrypoints/__tests__/background.test.ts (12个测试用例)
- 超长文本截断限制 (>10000字符)
- Base64 内联图片拦截并返回错误提示
- 更新 parseContextMenuClick 返回 ParseResult 格式
- 更新测试匹配新的返回格式
- lint/typecheck/test 全部通过 (579个测试)

* fix: 修复右键菜单跳转到错误页面的问题

- 将 options 页面路径从 '/entrypoints/options/index.html' 修正为 '/options.html'
- WXT 构建后 entrypoints/options/index.html 会输出为根目录的 options.html

* fix: 修复右键菜单跳转目标为 popup 页面

- 将 fallback 页面从 options.html 改为 popup.html
- popup 页面使用 RouterProvider,可以正确处理 URL 参数并跳转到对应功能页面
- options 页面是独立设置页,不支持 URL 参数路由

* fix: 使用 openPopup() 替代 tabs.create() 打开弹窗

- background.ts: 使用 browser.action.openPopup() 打开 popup 弹窗
- 先保存数据到 storage,然后打开 popup
- RouterProvider.tsx: 初始化时检查 storage 中的 pendingData 并跳转到对应页面
- 移除之前错误的 tabs.create() 方式

* fix: 修复右键菜单数据未传递到目标页面的问题

- RouterProvider 不再提前清除 pendingData
- 让目标页面的 useContextMenuData 来消费和清除数据
- 这样确保 TextStatistics 等页面能正确接收选中的文本

* fix: 修复 popup 已打开时右键菜单不生效的问题

- 在 RouterProvider 中添加对 contextMenu/pendingData 的 storage 变化监听
- 当 pendingData 变化时,自动跳转到对应功能页面
- 解决了 openPopup() 只聚焦已有窗口而不触发重新挂载的问题

* feat: 支持右键菜单图片二维码识别

- 在 qrCodeParser.ts 中添加 parseQrCodeFromUrl 函数,支持从图片 URL 解析二维码
- 在 QrCodeToUrlSection 中使用 useContextMenuData 接收右键菜单传递的图片 URL
- 自动下载图片并解析二维码,显示解析结果

* refactor: 暂时移除图片二维码识别功能

- 删除 qrCode-image 右键菜单配置
- 删除 parseQrCodeFromUrl 函数
- 删除 QrCodeToUrlSection 中的 useContextMenuData 相关代码
- 更新相关测试用例

* feat: 支持右键菜单时间戳转换

- 在 useTimestampConverter 中添加 useContextMenuData hook
- 智能识别输入是时间戳还是日期时间字符串
- 自动切换到对应模式并执行转换
- 时间戳自动识别秒/毫秒单位

* fix: 修复右键菜单功能的多个逻辑漏洞

1. 修复时区硬编码问题 - 使用用户选择的时区而非固定 Asia/Shanghai
2. 修复 openPopup() 失败后数据残留 - 失败时清除 storage 中的待处理数据
3. 统一数据过期时间常量 - 导出 CONTEXT_MENU_DATA_EXPIRY_MS 并统一使用
4. 修复 featureKey 类型断言不安全 - 使用映射表处理非常规菜单 ID

* refactor(qrcode): 提取独立组件并添加单元测试

- 创建 pages/QrCode/types.ts 定义状态类型接口
- 提取 QrCodePreview 组件用于二维码预览和操作
- 提取 ImageUploader 组件封装图片上传、拖拽、粘贴逻辑
- 重构 UrlToQrCodeSection 状态提升到父组件
- 重构 QrCodeToUrlSection 通过回调传递解析结果
- 更新主页面 index.tsx 集中管理所有状态
- 为 QrCodePreview 和 ImageUploader 添加单元测试 (23 个用例)

* refactor(qrcode): 引入通用组件并重构双栏布局

- 引入 SwitchButtonGroup 用于模式切换
- 引入 TextInputArea 用于文本输入和结果展示
- 使用 MUI Grid 构建响应式双栏布局
- 实现 generate/parse 模式下的左右面板内容切换
- 修复 iconColor 颜色格式错误,使用 qrCodePageStyles.primaryColor

* refactor(qrcode): 国际化补充、质量保障与错误处理优化

- 补充 i18n 翻译键(generateMode, parseMode, pasteHint 等)
- 创建 useDebounce Hook 实现输入防抖 (200ms)
- 使用 useRef 解决 useEffect 无限循环问题
- 编写 pages/QrCode 单元测试 (7 个用例)
- 优化右键菜单错误处理,改进用户提示
- 运行 603 个测试全部通过

* refactor(qrcode): 组件化重构,采用 Context + Hook 模式

- 创建 QrCodeContext 和 QrCodeProvider 管理共享状态
- 提取 useQrCode Hook 封装所有状态和业务逻辑
- 创建 GeneratePanel 组件处理二维码生成模式
- 创建 ParsePanel 组件处理二维码解析模式
- 简化 index.tsx 为容器组件 (340行 → 47行)
- 删除未使用的旧组件文件 (QrCodeToUrlSection, UrlToQrCodeSection)
- 清理 types.ts 中未使用的类型定义
- 603 个测试全部通过

* fix(qrcode): 固定二维码预览图片尺寸,避免布局抖动

- 设置 QR_PREVIEW_IMAGE 固定尺寸 250x250
- 与 QRious 生成的二维码大小保持一致
- 解决输入内容变化时预览窗口大小跳动问题

* fix(qrcode): 修复切换 tab 后二维码图片失效问题

- 移除 ImageUploader 组件卸载时的预览 URL 释放逻辑
- 在 useQrCode hook 中统一管理预览 URL 生命周期
- 创建新预览 URL 前释放旧的,避免内存泄漏

* fix(qrcode): 固定图片上传区域高度,避免布局抖动

- 将 DROPZONE 的 minHeight: 200 改为 height: 250
- 与二维码预览图片高度保持一致
- 解决上传图片时 div 高度变化问题

* refactor(qrCode): 优化二维码解析功能

- 将'二维码转 URL'改为'二维码转文本'
- 将解析结果的文本预览框的 placeholder 置为空
- 去除手动解析二维码功能,只保留自动解析

* fix(pageHeader): 使用 MUI 主题色替代硬编码颜色,支持暗色模式

* feat(pageHeader): popup 模式下隐藏 PageHeader 组件

* fix(i18n): 统一使用 useLazyTranslation 避免翻译 key 闪烁

* perf: 配置 manualChunks 拆分 vendor chunk,优化打包体积

- 使用 Vite 插件 manualChunksForHtmlOnly 仅对 HTML 多入口构建生效
- 跳过 background/content-script 的 IIFE 构建(不支持 manualChunks)
- 拆分 vendor-react (193KB)、vendor-mui (326KB)、vendor-i18n (55KB)
- 按需加载 vendor-qr (78KB)、vendor-dnd (45KB)、vendor-markdown (41KB)
- PageErrorBoundary chunk 从 454KB 降至 36KB

* chore: upgrade wxt to v0.20.26 and @wxt-dev/module-react to v1.2.2

* chore: upgrade low-risk dependencies

- react: 19.2.3 → 19.2.6
- react-dom: 19.2.3 → 19.2.6
- dayjs: 1.11.19 → 1.11.20
- marked: 18.0.3 → 18.0.4
- prettier: 3.8.1 → 3.8.3
- terser: 5.46.0 → 5.47.1
- i18next: 26.0.8 → 26.2.0
- react-i18next: 17.0.6 → 17.0.8
- globals: 17.2.0 → 17.6.0
- eslint-plugin-react-hooks: 7.0.1 → 7.1.1
- @typescript-eslint/*: 8.54.0 → 8.59.4
- typescript-eslint: 8.54.0 → 8.59.4
- @testing-library/jest-dom: 6.6.0 → 6.9.1
- @testing-library/react: 16.0.0 → 16.3.2
- @testing-library/user-event: 14.5.2 → 14.6.1
- @testing-library/dom: added as peer dependency
- @types/react: 19.2.7 → 19.2.15
- @types/chrome: 0.1.36 → 0.1.42
- @types/webextension-polyfill: 0.12.4 → 0.12.5

* chore: upgrade medium-risk dependencies

- lint-staged: 16.2.7 → 17.0.5
- jsdom: 25.0.0 → 29.1.1
- @vitejs/plugin-react: 4.3.4 → 6.0.2
- vitest: 2.0.0 → 4.1.7
- @vitest/coverage-v8: upgraded to match vitest
- @webext-core/messaging: 2.3.0 → 3.0.1
- eslint: 9.39.2 → 10.4.0
- @eslint/js: added as new dependency
- @types/marked: removed (marked now provides its own types)

Fixes:
- Fix ref update during render in useQrCode.ts
- Fix ref update during render in useStorageCleaner.ts
- Add error cause in jwt.ts decode function
- Add type assertion for mock functions in htmlToMarkdown.test.ts

* chore(deps): 降级 eslint 版本并移除 @eslint/js

* docs: 精简 AGENTS.md,聚焦高信号信息

* test: 修复测试中的 act() 警告和 clipboard mock 问题

- TextInputArea: 使用 userEvent 替代 fireEvent,用 vi.spyOn 替代 Object.assign mock clipboard
- ImageMode/FileMode: 添加 waitForStorageReady() 等待 useStorageState 异步初始化

* perf: 优化首屏加载,快照有效时跳过骨架屏

- isLoaded 初始值根据 localStorage 快照是否存在决定
- 后续访问直接渲染真实 DOM,无需等待 chrome.storage 异步加载
- loadInitialData 仍在后台静默执行确保数据同步

* 引入 Tailwind CSS 和 shadcn/ui

* 替换 Timestamp 页面图标为 lucide-react 的 Clock 图标

* 用 Tailwind CSS 重写 Timestamp 页面的 MUI 基础排版组件

* 用 Tailwind CSS 重构 ResultView 和 LiveClock 组件

* 用 Tailwind CSS 重写 Dashboard/index.tsx 和 ToolCard.tsx

* 用 Tailwind CSS 重写 TextStatistics 页面

* 用 Tailwind CSS 重写 Base64Converter 页面

* 用 Tailwind CSS 重写 QrCode 页面及相关组件

* 用 Tailwind CSS 重写 Jwt 页面

* 用 Tailwind CSS 重写 StorageCleaner 页面,创建 shadcn/ui Dialog 组件

* 用 Tailwind CSS 重写 JsonTools 页面

* 用 Tailwind CSS 重构所有共享组件

* 修复 TypeScript 类型错误:将 MUI sx 语法改成标准 CSS 属性

* 重写基础骨架组件:用纯 Tailwind CSS 替换 MUI Box 组件

* 重写所有页面和组件:用纯 Tailwind CSS 替换 MUI 组件和图标

* 移除 MUI 依赖,简化配置文件,重写 ThemeModeProvider

* 移除 wxt.config.ts 中残留的 MUI chunk 配置

* 为 Dialog 组件添加暗黑模式背景色支持

* fix: 修复暗黑模式样式兼容性

- 在 tailwind.config.js 中启用 darkMode: 'class' 并扩展语义化颜色变量
- 将全项目硬编码颜色(bg-white、text-gray-*、border-gray-* 等)替换为语义化类名
- 修复 popup/index.html 硬编码浅色背景色
- 同步更新相关单元测试中的类名断言

* fix: 修复输入框和按钮在暗色模式下的样式问题

- 为 :root 和 .dark 添加 color-scheme,使浏览器表单控件默认样式适配暗色模式
- 将 bg-primary 搭配 text-white 的按钮改为 text-primary-foreground
- 替换残留的 hover:bg-blue-700、text-blue-500、bg-gray-200 等硬编码颜色
- 同步更新 TextInputArea 单元测试

* fix: 增强 SwitchButtonGroup 和立即转换按钮的视觉层级

- SwitchButtonGroup 选中按钮阴影从 shadow-sm 升级到 shadow-md
- 同步更新单元测试中断言
- Timestamp 立即转换按钮添加 shadow-md 增强边界感

* style: 隐藏所有窗口的滚动条

- 在全局 CSS 中为 html 添加 scrollbar-width: none(Firefox)
- 添加 ::-webkit-scrollbar { display: none }(Chrome/Safari/Opera)

* feat: add sonner dependency for toast notifications

* refactor: remove DomainHeader component and its usage

* refactor: 使用 className 替换 sx/buttonSx 并优化 SwitchButtonGroup 选中动画

* refactor: 简化 TextInputArea,移除 showMessage 和 MUI 样式,改用 shadcn 样式和 sonner toast

* refactor: 简化 TextInputArea,移除 showMessage 和 MUI 样式,改用 shadcn 样式和 sonner toast

* refactor: 重构 QrCodePreview 组件,继承 HTMLDiv 属性并统一 shadcn 样式

- 组件改为继承 `React.HTMLAttributes<HTMLDivElement>`,支持外部传入 className 和事件
- 使用 `cn()` 函数合并外部类名
- 空白状态改用 `border border-dashed` 和更浅的 `bg-muted/40` 背景
- 正常状态改为纯白背景柔边圆角容器包裹二维码,确保暗黑模式下黑白对比度
- 下载/复制按钮完全对齐 shadcn 的 Outline 与 Default 样式规范
- 更新测试用例中的 alt 文本为 "QR Code Preview"

* refactor: 将 PageHeader 组件样式从 MUI sx 迁移至 Tailwind 类名,并更新测试

* refactor: 更新PageErrorBoundary的文案和样式以对齐shadcn主题

- 错误提示从“该页面加载失败”改为“该功能运行异常”
- 重试按钮文字从“重试”改为“重新尝试”
- 使用border-destructive, bg-destructive等语义化类名替换硬编码颜色
- 更新对应测试断言

* refactor: 重构 CopyButton 与 RouterContainer 组件,移除 GlobalSnackbar 并统一使用 sonner toast 和 cn 工具函数

* refactor: 移除 MUI sx 样式和未使用的 GlobalSnackbar 引用

* feat: add @radix-ui/react-select dependency

* feat: add Input and Select shadcn UI components

* refactor: 重构 Timestamp 页面使用响应式 Hook 统一状态并全面迁移至 shadcn 样式

* refactor: 将 TextStatistics 页面和 TextInputArea 组件全面迁移至 shadcn 样式并优化布局

* feat: add Badge, Checkbox, Label, and Switch shadcn UI components

* refactor: 全面迁移 StorageCleaner 页面至 shadcn 样式并移除 GlobalSnackbar 依赖

* refactor: 全面重构 JsonTools 页面,采用声明式响应架构并统一 shadcn 样式

* refactor: 全面重构 Jwt 页面,采用防抖输入并统一 shadcn 样式

* refactor: 重构 Dashboard 页面和 ToolCard 组件,全面迁移至 shadcn 样式并优化布局

* refactor: 重构 HtmlToMarkdown 和 MarkdownToHtml 页面,统一 shadcn 样式并优化打印与暗色模式

* refactor: 抽离 useBase64Converter hook 并新增 Base64ConverterSection 组件,统一 Base64 转换页面的 shadcn 样式

* refactor: 重构 QrCode 页面为声明式响应架构并统一 shadcn 样式

* refactor: 优化 Dashboard 布局及 ToolCard 描述文本截断处理

* refactor: 优化 MarkdownToHtml 预览样式,剥离 CSS 变量回退值并增强 iframe 暗色模式自适应

* refactor: 优化 Popover 安全性和交互体验,修复 Context Menu 数据持久化问题并添加 i18n 支持

* refactor: strengthen i18n type safety and fix async Promise handling in language detection

* refactor: extend vitest setup with full chrome/browser mocks and i18n utility mock

* refactor: migrate storage listeners to wxt/browser and refactor provider state management

* refactor: restructure CI/CD pipelines with cached dependencies, separate setup job, and multi-browser builds

* refactor: remove tsc check from lint-staged and reorder lint commands

* refactor: migrate eslint config to tseslint.config with projectService and upgrade React rules

* refactor: use __MSG_ placeholders for i18n and remove postcss config

* refactor: replace __MSG_ placeholders with static strings for extension name and description

* refactor: add React version detection and disable prop-types in eslint; refactor lazy translation tests with proper mock isolation and Chrome/browser stubs

* refactor: use regex matchers in StorageCleanerConfirm tests and add chrome/browser mocks

* refactor: use regex matchers for i18n text and add uniform mocks in test files

* ci: add wxt prepare step to generate .wxt types for typecheck and tests

---------

Co-authored-by: Ubuntu <ubuntu@localhost.localdomain>
2026-05-23 00:02:43 +08:00
Ubuntu 364672918d ci: add wxt prepare step to generate .wxt types for typecheck and tests 2026-05-22 23:59:56 +08:00
雨霖铃 5e23b29a25 refactor: use regex matchers for i18n text and add uniform mocks in test files 2026-05-22 23:51:16 +08:00
雨霖铃 cc9162f601 refactor: use regex matchers in StorageCleanerConfirm tests and add chrome/browser mocks 2026-05-22 23:45:41 +08:00
雨霖铃 67bafe53d5 refactor: add React version detection and disable prop-types in eslint; refactor lazy translation tests with proper mock isolation and Chrome/browser stubs 2026-05-22 23:43:08 +08:00
雨霖铃 ba57c070f9 refactor: replace __MSG_ placeholders with static strings for extension name and description 2026-05-22 23:22:28 +08:00
雨霖铃 34030e5f6a refactor: use __MSG_ placeholders for i18n and remove postcss config 2026-05-22 23:18:42 +08:00
雨霖铃 8b11dcd0ce refactor: migrate eslint config to tseslint.config with projectService and upgrade React rules 2026-05-22 23:13:47 +08:00
雨霖铃 67c0c30a7a refactor: remove tsc check from lint-staged and reorder lint commands 2026-05-22 23:12:32 +08:00
雨霖铃 a3b2bfb43a refactor: restructure CI/CD pipelines with cached dependencies, separate setup job, and multi-browser builds 2026-05-22 23:10:55 +08:00
雨霖铃 81a2e793ba refactor: migrate storage listeners to wxt/browser and refactor provider state management 2026-05-22 23:06:55 +08:00
雨霖铃 756ece31d3 refactor: extend vitest setup with full chrome/browser mocks and i18n utility mock 2026-05-22 23:02:41 +08:00
雨霖铃 7efed8673a refactor: strengthen i18n type safety and fix async Promise handling in language detection 2026-05-22 22:58:27 +08:00
雨霖铃 88bebfa1b8 refactor: 优化 Popover 安全性和交互体验,修复 Context Menu 数据持久化问题并添加 i18n 支持 2026-05-22 22:56:25 +08:00
雨霖铃 272bcae904 refactor: 优化 MarkdownToHtml 预览样式,剥离 CSS 变量回退值并增强 iframe 暗色模式自适应 2026-05-22 22:46:13 +08:00
雨霖铃 539f9648b1 refactor: 优化 Dashboard 布局及 ToolCard 描述文本截断处理 2026-05-22 22:37:45 +08:00
雨霖铃 7e58b5688f refactor: 重构 QrCode 页面为声明式响应架构并统一 shadcn 样式 2026-05-22 22:32:11 +08:00
雨霖铃 89e33f6273 refactor: 抽离 useBase64Converter hook 并新增 Base64ConverterSection 组件,统一 Base64 转换页面的 shadcn 样式 2026-05-22 22:26:35 +08:00
雨霖铃 9601afdd49 refactor: 重构 HtmlToMarkdown 和 MarkdownToHtml 页面,统一 shadcn 样式并优化打印与暗色模式 2026-05-22 22:06:34 +08:00
雨霖铃 ae0b923e11 refactor: 重构 Dashboard 页面和 ToolCard 组件,全面迁移至 shadcn 样式并优化布局 2026-05-22 21:56:09 +08:00
雨霖铃 917fca5947 refactor: 全面重构 Jwt 页面,采用防抖输入并统一 shadcn 样式 2026-05-22 21:46:45 +08:00
雨霖铃 d2e5e6b45d refactor: 全面重构 JsonTools 页面,采用声明式响应架构并统一 shadcn 样式 2026-05-22 21:45:13 +08:00
雨霖铃 336b62256d refactor: 全面迁移 StorageCleaner 页面至 shadcn 样式并移除 GlobalSnackbar 依赖 2026-05-22 21:26:02 +08:00
雨霖铃 7ddc761df4 feat: add Badge, Checkbox, Label, and Switch shadcn UI components 2026-05-22 21:25:29 +08:00
雨霖铃 67553afe9c refactor: 将 TextStatistics 页面和 TextInputArea 组件全面迁移至 shadcn 样式并优化布局 2026-05-22 21:10:52 +08:00
雨霖铃 02aef0c7b1 refactor: 重构 Timestamp 页面使用响应式 Hook 统一状态并全面迁移至 shadcn 样式 2026-05-22 21:03:48 +08:00
雨霖铃 94158f016b feat: add Input and Select shadcn UI components 2026-05-22 21:03:06 +08:00
雨霖铃 dd03d9391f feat: add @radix-ui/react-select dependency 2026-05-22 21:02:48 +08:00
雨霖铃 21e44cb889 refactor: 移除 MUI sx 样式和未使用的 GlobalSnackbar 引用 2026-05-22 20:51:57 +08:00
雨霖铃 b3dd7146ad refactor: 重构 CopyButton 与 RouterContainer 组件,移除 GlobalSnackbar 并统一使用 sonner toast 和 cn 工具函数 2026-05-22 20:51:13 +08:00
雨霖铃 2ced38769b refactor: 更新PageErrorBoundary的文案和样式以对齐shadcn主题
- 错误提示从“该页面加载失败”改为“该功能运行异常”
- 重试按钮文字从“重试”改为“重新尝试”
- 使用border-destructive, bg-destructive等语义化类名替换硬编码颜色
- 更新对应测试断言
2026-05-22 20:49:46 +08:00
雨霖铃 8406b03dd6 refactor: 将 PageHeader 组件样式从 MUI sx 迁移至 Tailwind 类名,并更新测试 2026-05-22 20:49:07 +08:00
雨霖铃 da667ccb13 refactor: 重构 QrCodePreview 组件,继承 HTMLDiv 属性并统一 shadcn 样式
- 组件改为继承 `React.HTMLAttributes<HTMLDivElement>`,支持外部传入 className 和事件
- 使用 `cn()` 函数合并外部类名
- 空白状态改用 `border border-dashed` 和更浅的 `bg-muted/40` 背景
- 正常状态改为纯白背景柔边圆角容器包裹二维码,确保暗黑模式下黑白对比度
- 下载/复制按钮完全对齐 shadcn 的 Outline 与 Default 样式规范
- 更新测试用例中的 alt 文本为 "QR Code Preview"
2026-05-22 20:48:14 +08:00
雨霖铃 1bfb7d4e6d refactor: 简化 TextInputArea,移除 showMessage 和 MUI 样式,改用 shadcn 样式和 sonner toast 2026-05-22 20:47:04 +08:00
雨霖铃 b3c2fbccf5 refactor: 简化 TextInputArea,移除 showMessage 和 MUI 样式,改用 shadcn 样式和 sonner toast 2026-05-22 20:46:43 +08:00
雨霖铃 b29e9417cc refactor: 使用 className 替换 sx/buttonSx 并优化 SwitchButtonGroup 选中动画 2026-05-22 20:45:31 +08:00
雨霖铃 cf0d19914c refactor: remove DomainHeader component and its usage 2026-05-22 20:44:59 +08:00
雨霖铃 6b2ac33b41 feat: add sonner dependency for toast notifications 2026-05-22 20:44:24 +08:00
雨霖铃 02a611e49b style: 隐藏所有窗口的滚动条
- 在全局 CSS 中为 html 添加 scrollbar-width: none(Firefox)
- 添加 ::-webkit-scrollbar { display: none }(Chrome/Safari/Opera)
2026-05-22 20:03:09 +08:00
雨霖铃 72883b5d1d fix: 增强 SwitchButtonGroup 和立即转换按钮的视觉层级
- SwitchButtonGroup 选中按钮阴影从 shadow-sm 升级到 shadow-md
- 同步更新单元测试中断言
- Timestamp 立即转换按钮添加 shadow-md 增强边界感
2026-05-22 18:08:44 +08:00
雨霖铃 c7beebf6e0 fix: 修复输入框和按钮在暗色模式下的样式问题
- 为 :root 和 .dark 添加 color-scheme,使浏览器表单控件默认样式适配暗色模式
- 将 bg-primary 搭配 text-white 的按钮改为 text-primary-foreground
- 替换残留的 hover:bg-blue-700、text-blue-500、bg-gray-200 等硬编码颜色
- 同步更新 TextInputArea 单元测试
2026-05-22 17:36:37 +08:00
雨霖铃 de7da9f1db fix: 修复暗黑模式样式兼容性
- 在 tailwind.config.js 中启用 darkMode: 'class' 并扩展语义化颜色变量
- 将全项目硬编码颜色(bg-white、text-gray-*、border-gray-* 等)替换为语义化类名
- 修复 popup/index.html 硬编码浅色背景色
- 同步更新相关单元测试中的类名断言
2026-05-22 16:04:50 +08:00
雨霖铃 5b9a0f59d3 为 Dialog 组件添加暗黑模式背景色支持 2026-05-22 08:55:23 +08:00
雨霖铃 3e1753da23 移除 wxt.config.ts 中残留的 MUI chunk 配置 2026-05-22 00:57:01 +08:00
雨霖铃 6763cd7862 移除 MUI 依赖,简化配置文件,重写 ThemeModeProvider 2026-05-22 00:53:12 +08:00
雨霖铃 e2dd5f5915 重写所有页面和组件:用纯 Tailwind CSS 替换 MUI 组件和图标 2026-05-22 00:51:11 +08:00
雨霖铃 1bc3ee59a0 重写基础骨架组件:用纯 Tailwind CSS 替换 MUI Box 组件 2026-05-22 00:44:32 +08:00
雨霖铃 6711783975 修复 TypeScript 类型错误:将 MUI sx 语法改成标准 CSS 属性 2026-05-22 00:38:48 +08:00
雨霖铃 427e668e7c 用 Tailwind CSS 重构所有共享组件 2026-05-22 00:31:52 +08:00
雨霖铃 37618eb71f 用 Tailwind CSS 重写 JsonTools 页面 2026-05-22 00:07:56 +08:00
雨霖铃 d2334c5e95 用 Tailwind CSS 重写 StorageCleaner 页面,创建 shadcn/ui Dialog 组件 2026-05-22 00:00:04 +08:00
雨霖铃 a4533be2c1 用 Tailwind CSS 重写 Jwt 页面 2026-05-21 23:52:20 +08:00
雨霖铃 83bf9dd342 用 Tailwind CSS 重写 QrCode 页面及相关组件 2026-05-21 23:46:44 +08:00
雨霖铃 8902bc0f10 用 Tailwind CSS 重写 Base64Converter 页面 2026-05-21 23:40:52 +08:00
雨霖铃 4753a7b54d 用 Tailwind CSS 重写 TextStatistics 页面 2026-05-21 23:35:25 +08:00
雨霖铃 7f9bf2334c 用 Tailwind CSS 重写 Dashboard/index.tsx 和 ToolCard.tsx 2026-05-21 23:32:19 +08:00
雨霖铃 6bd05aa6f1 用 Tailwind CSS 重构 ResultView 和 LiveClock 组件 2026-05-21 23:23:13 +08:00
雨霖铃 bdb40343b1 用 Tailwind CSS 重写 Timestamp 页面的 MUI 基础排版组件 2026-05-21 23:13:29 +08:00
雨霖铃 9a9336aea8 替换 Timestamp 页面图标为 lucide-react 的 Clock 图标 2026-05-21 23:00:44 +08:00
雨霖铃 3eb00d8a29 引入 Tailwind CSS 和 shadcn/ui 2026-05-21 22:57:23 +08:00
雨霖铃 bb27577278 perf: 优化首屏加载,快照有效时跳过骨架屏
- isLoaded 初始值根据 localStorage 快照是否存在决定
- 后续访问直接渲染真实 DOM,无需等待 chrome.storage 异步加载
- loadInitialData 仍在后台静默执行确保数据同步
2026-05-21 22:46:05 +08:00
雨霖铃 141e206b62 Merge branch 'main' into develop 2026-05-21 22:33:03 +08:00
LingandRX 689c3faf16 chore: upgrade dependencies (WXT v0.20.26 + low/medium risk deps) (#41)
* feat: 添加 contextMenus 权限和消息类型定义

* feat: 实现右键菜单注册与点击分流逻辑

- 新增 utils/contextMenu.ts 封装菜单配置和解析函数
- 在 background.ts 监听 onInstalled 初始化菜单
- 实现 contextMenus.onClicked 点击事件处理
- 分流逻辑:优先发送到侧边栏,否则打开 options 页面
- 添加 contextMenu 单元测试(15个测试用例)
- 更新 vitest.setup.ts 添加 contextMenus mock

* feat: 实现 Content Script 原位轻量提示 UI

- 新增 uiPopover.ts 实现原位弹窗组件(深色主题、自动定位、自动隐藏)
- 新增 contextMenuHandler.ts 处理右键菜单消息
- 时间戳转换:在点击位置显示转换结果
- 文本统计:显示字符/单词/行数/字节统计
- 更新 messageHandler.ts 挂载右键菜单消息监听

* feat: 实现 React 页面层右键菜单数据联动

- 新增 useContextMenuData Hook 处理右键菜单数据传递
- 重构 Jwt/Base64Converter/TextStatistics/QrCode 页面接收右键数据
- RouterProvider 支持从 URL 参数解析右键菜单数据
- 添加 contextMenu/pendingData 存储键到 StorageSchema
- 添加 useContextMenuData 单元测试(12个测试用例)

* test: 添加 background 单元测试与边界情况处理

- 新增 entrypoints/__tests__/background.test.ts (12个测试用例)
- 超长文本截断限制 (>10000字符)
- Base64 内联图片拦截并返回错误提示
- 更新 parseContextMenuClick 返回 ParseResult 格式
- 更新测试匹配新的返回格式
- lint/typecheck/test 全部通过 (579个测试)

* fix: 修复右键菜单跳转到错误页面的问题

- 将 options 页面路径从 '/entrypoints/options/index.html' 修正为 '/options.html'
- WXT 构建后 entrypoints/options/index.html 会输出为根目录的 options.html

* fix: 修复右键菜单跳转目标为 popup 页面

- 将 fallback 页面从 options.html 改为 popup.html
- popup 页面使用 RouterProvider,可以正确处理 URL 参数并跳转到对应功能页面
- options 页面是独立设置页,不支持 URL 参数路由

* fix: 使用 openPopup() 替代 tabs.create() 打开弹窗

- background.ts: 使用 browser.action.openPopup() 打开 popup 弹窗
- 先保存数据到 storage,然后打开 popup
- RouterProvider.tsx: 初始化时检查 storage 中的 pendingData 并跳转到对应页面
- 移除之前错误的 tabs.create() 方式

* fix: 修复右键菜单数据未传递到目标页面的问题

- RouterProvider 不再提前清除 pendingData
- 让目标页面的 useContextMenuData 来消费和清除数据
- 这样确保 TextStatistics 等页面能正确接收选中的文本

* fix: 修复 popup 已打开时右键菜单不生效的问题

- 在 RouterProvider 中添加对 contextMenu/pendingData 的 storage 变化监听
- 当 pendingData 变化时,自动跳转到对应功能页面
- 解决了 openPopup() 只聚焦已有窗口而不触发重新挂载的问题

* feat: 支持右键菜单图片二维码识别

- 在 qrCodeParser.ts 中添加 parseQrCodeFromUrl 函数,支持从图片 URL 解析二维码
- 在 QrCodeToUrlSection 中使用 useContextMenuData 接收右键菜单传递的图片 URL
- 自动下载图片并解析二维码,显示解析结果

* refactor: 暂时移除图片二维码识别功能

- 删除 qrCode-image 右键菜单配置
- 删除 parseQrCodeFromUrl 函数
- 删除 QrCodeToUrlSection 中的 useContextMenuData 相关代码
- 更新相关测试用例

* feat: 支持右键菜单时间戳转换

- 在 useTimestampConverter 中添加 useContextMenuData hook
- 智能识别输入是时间戳还是日期时间字符串
- 自动切换到对应模式并执行转换
- 时间戳自动识别秒/毫秒单位

* fix: 修复右键菜单功能的多个逻辑漏洞

1. 修复时区硬编码问题 - 使用用户选择的时区而非固定 Asia/Shanghai
2. 修复 openPopup() 失败后数据残留 - 失败时清除 storage 中的待处理数据
3. 统一数据过期时间常量 - 导出 CONTEXT_MENU_DATA_EXPIRY_MS 并统一使用
4. 修复 featureKey 类型断言不安全 - 使用映射表处理非常规菜单 ID

* refactor(qrcode): 提取独立组件并添加单元测试

- 创建 pages/QrCode/types.ts 定义状态类型接口
- 提取 QrCodePreview 组件用于二维码预览和操作
- 提取 ImageUploader 组件封装图片上传、拖拽、粘贴逻辑
- 重构 UrlToQrCodeSection 状态提升到父组件
- 重构 QrCodeToUrlSection 通过回调传递解析结果
- 更新主页面 index.tsx 集中管理所有状态
- 为 QrCodePreview 和 ImageUploader 添加单元测试 (23 个用例)

* refactor(qrcode): 引入通用组件并重构双栏布局

- 引入 SwitchButtonGroup 用于模式切换
- 引入 TextInputArea 用于文本输入和结果展示
- 使用 MUI Grid 构建响应式双栏布局
- 实现 generate/parse 模式下的左右面板内容切换
- 修复 iconColor 颜色格式错误,使用 qrCodePageStyles.primaryColor

* refactor(qrcode): 国际化补充、质量保障与错误处理优化

- 补充 i18n 翻译键(generateMode, parseMode, pasteHint 等)
- 创建 useDebounce Hook 实现输入防抖 (200ms)
- 使用 useRef 解决 useEffect 无限循环问题
- 编写 pages/QrCode 单元测试 (7 个用例)
- 优化右键菜单错误处理,改进用户提示
- 运行 603 个测试全部通过

* refactor(qrcode): 组件化重构,采用 Context + Hook 模式

- 创建 QrCodeContext 和 QrCodeProvider 管理共享状态
- 提取 useQrCode Hook 封装所有状态和业务逻辑
- 创建 GeneratePanel 组件处理二维码生成模式
- 创建 ParsePanel 组件处理二维码解析模式
- 简化 index.tsx 为容器组件 (340行 → 47行)
- 删除未使用的旧组件文件 (QrCodeToUrlSection, UrlToQrCodeSection)
- 清理 types.ts 中未使用的类型定义
- 603 个测试全部通过

* fix(qrcode): 固定二维码预览图片尺寸,避免布局抖动

- 设置 QR_PREVIEW_IMAGE 固定尺寸 250x250
- 与 QRious 生成的二维码大小保持一致
- 解决输入内容变化时预览窗口大小跳动问题

* fix(qrcode): 修复切换 tab 后二维码图片失效问题

- 移除 ImageUploader 组件卸载时的预览 URL 释放逻辑
- 在 useQrCode hook 中统一管理预览 URL 生命周期
- 创建新预览 URL 前释放旧的,避免内存泄漏

* fix(qrcode): 固定图片上传区域高度,避免布局抖动

- 将 DROPZONE 的 minHeight: 200 改为 height: 250
- 与二维码预览图片高度保持一致
- 解决上传图片时 div 高度变化问题

* refactor(qrCode): 优化二维码解析功能

- 将'二维码转 URL'改为'二维码转文本'
- 将解析结果的文本预览框的 placeholder 置为空
- 去除手动解析二维码功能,只保留自动解析

* fix(pageHeader): 使用 MUI 主题色替代硬编码颜色,支持暗色模式

* feat(pageHeader): popup 模式下隐藏 PageHeader 组件

* fix(i18n): 统一使用 useLazyTranslation 避免翻译 key 闪烁

* perf: 配置 manualChunks 拆分 vendor chunk,优化打包体积

- 使用 Vite 插件 manualChunksForHtmlOnly 仅对 HTML 多入口构建生效
- 跳过 background/content-script 的 IIFE 构建(不支持 manualChunks)
- 拆分 vendor-react (193KB)、vendor-mui (326KB)、vendor-i18n (55KB)
- 按需加载 vendor-qr (78KB)、vendor-dnd (45KB)、vendor-markdown (41KB)
- PageErrorBoundary chunk 从 454KB 降至 36KB

* chore: upgrade wxt to v0.20.26 and @wxt-dev/module-react to v1.2.2

* chore: upgrade low-risk dependencies

- react: 19.2.3 → 19.2.6
- react-dom: 19.2.3 → 19.2.6
- dayjs: 1.11.19 → 1.11.20
- marked: 18.0.3 → 18.0.4
- prettier: 3.8.1 → 3.8.3
- terser: 5.46.0 → 5.47.1
- i18next: 26.0.8 → 26.2.0
- react-i18next: 17.0.6 → 17.0.8
- globals: 17.2.0 → 17.6.0
- eslint-plugin-react-hooks: 7.0.1 → 7.1.1
- @typescript-eslint/*: 8.54.0 → 8.59.4
- typescript-eslint: 8.54.0 → 8.59.4
- @testing-library/jest-dom: 6.6.0 → 6.9.1
- @testing-library/react: 16.0.0 → 16.3.2
- @testing-library/user-event: 14.5.2 → 14.6.1
- @testing-library/dom: added as peer dependency
- @types/react: 19.2.7 → 19.2.15
- @types/chrome: 0.1.36 → 0.1.42
- @types/webextension-polyfill: 0.12.4 → 0.12.5

* chore: upgrade medium-risk dependencies

- lint-staged: 16.2.7 → 17.0.5
- jsdom: 25.0.0 → 29.1.1
- @vitejs/plugin-react: 4.3.4 → 6.0.2
- vitest: 2.0.0 → 4.1.7
- @vitest/coverage-v8: upgraded to match vitest
- @webext-core/messaging: 2.3.0 → 3.0.1
- eslint: 9.39.2 → 10.4.0
- @eslint/js: added as new dependency
- @types/marked: removed (marked now provides its own types)

Fixes:
- Fix ref update during render in useQrCode.ts
- Fix ref update during render in useStorageCleaner.ts
- Add error cause in jwt.ts decode function
- Add type assertion for mock functions in htmlToMarkdown.test.ts

* chore(deps): 降级 eslint 版本并移除 @eslint/js

* docs: 精简 AGENTS.md,聚焦高信号信息

* test: 修复测试中的 act() 警告和 clipboard mock 问题

- TextInputArea: 使用 userEvent 替代 fireEvent,用 vi.spyOn 替代 Object.assign mock clipboard
- ImageMode/FileMode: 添加 waitForStorageReady() 等待 useStorageState 异步初始化
2026-05-21 22:12:11 +08:00
LingandRX f5e8e96830 Merge branch 'main' into develop 2026-05-21 22:11:06 +08:00
雨霖铃 f1f0047928 test: 修复测试中的 act() 警告和 clipboard mock 问题
- TextInputArea: 使用 userEvent 替代 fireEvent,用 vi.spyOn 替代 Object.assign mock clipboard
- ImageMode/FileMode: 添加 waitForStorageReady() 等待 useStorageState 异步初始化
2026-05-21 22:06:09 +08:00
雨霖铃 f76fc3c140 docs: 精简 AGENTS.md,聚焦高信号信息 2026-05-21 21:54:07 +08:00
雨霖铃 9ed84382ea chore(deps): 降级 eslint 版本并移除 @eslint/js 2026-05-21 21:47:21 +08:00
Ubuntu 4a382144d1 chore: upgrade medium-risk dependencies
- lint-staged: 16.2.7 → 17.0.5
- jsdom: 25.0.0 → 29.1.1
- @vitejs/plugin-react: 4.3.4 → 6.0.2
- vitest: 2.0.0 → 4.1.7
- @vitest/coverage-v8: upgraded to match vitest
- @webext-core/messaging: 2.3.0 → 3.0.1
- eslint: 9.39.2 → 10.4.0
- @eslint/js: added as new dependency
- @types/marked: removed (marked now provides its own types)

Fixes:
- Fix ref update during render in useQrCode.ts
- Fix ref update during render in useStorageCleaner.ts
- Add error cause in jwt.ts decode function
- Add type assertion for mock functions in htmlToMarkdown.test.ts
2026-05-21 20:44:40 +08:00
Ubuntu a654e64fe0 chore: upgrade low-risk dependencies
- react: 19.2.3 → 19.2.6
- react-dom: 19.2.3 → 19.2.6
- dayjs: 1.11.19 → 1.11.20
- marked: 18.0.3 → 18.0.4
- prettier: 3.8.1 → 3.8.3
- terser: 5.46.0 → 5.47.1
- i18next: 26.0.8 → 26.2.0
- react-i18next: 17.0.6 → 17.0.8
- globals: 17.2.0 → 17.6.0
- eslint-plugin-react-hooks: 7.0.1 → 7.1.1
- @typescript-eslint/*: 8.54.0 → 8.59.4
- typescript-eslint: 8.54.0 → 8.59.4
- @testing-library/jest-dom: 6.6.0 → 6.9.1
- @testing-library/react: 16.0.0 → 16.3.2
- @testing-library/user-event: 14.5.2 → 14.6.1
- @testing-library/dom: added as peer dependency
- @types/react: 19.2.7 → 19.2.15
- @types/chrome: 0.1.36 → 0.1.42
- @types/webextension-polyfill: 0.12.4 → 0.12.5
2026-05-21 20:25:00 +08:00
Ubuntu ed8720e63e chore: upgrade wxt to v0.20.26 and @wxt-dev/module-react to v1.2.2 2026-05-21 20:13:34 +08:00
Ubuntu 179a3d31f4 Revert "fix(ci): 修复 CI/CD 工作流问题"
This reverts commit 1df53323d0.
2026-05-21 19:52:44 +08:00
Ubuntu 7cd99876d7 Revert "fix(ci): 修复 CI/CD 工作流问题"
This reverts commit 6b0de96b18.
2026-05-21 19:52:44 +08:00
Ubuntu 6b0de96b18 fix(ci): 修复 CI/CD 工作流问题
- fix: release.yml typecheck 脚本名 compile → typecheck(发版阻断 bug)
- perf: CI 测试改用 test:coverage 生成覆盖率报告
- refactor: Release 复用 CI 工作流(workflow_call),消除重复 Job
- feat: Release 新增 Firefox/Chrome zip 产物验证步骤
- fix: 补全 lint-staged 配置,修复 pre-commit hook 失效问题
- feat: 添加 Dependabot 自动依赖更新配置
- fix: 添加 @vitest/coverage-v8 依赖(覆盖率所需)
- fix: 改用 npx 直接调用命令,规避 npm PATH 问题
2026-05-21 18:33:22 +08:00
Ubuntu 1df53323d0 fix(ci): 修复 CI/CD 工作流问题
- fix: release.yml typecheck 脚本名 compile → typecheck(发版阻断 bug)
- perf: CI 测试改用 test:coverage 生成覆盖率报告
- refactor: Release 复用 CI 工作流(workflow_call),消除重复 Job
- feat: Release 新增 Firefox/Chrome zip 产物验证步骤
- fix: 补全 lint-staged 配置,修复 pre-commit hook 失效问题
- feat: 添加 Dependabot 自动依赖更新配置
2026-05-21 18:01:55 +08:00
LingandRX a5c34e5428 Merge develop into main (#27)
* feat: 添加 contextMenus 权限和消息类型定义

* feat: 实现右键菜单注册与点击分流逻辑

- 新增 utils/contextMenu.ts 封装菜单配置和解析函数
- 在 background.ts 监听 onInstalled 初始化菜单
- 实现 contextMenus.onClicked 点击事件处理
- 分流逻辑:优先发送到侧边栏,否则打开 options 页面
- 添加 contextMenu 单元测试(15个测试用例)
- 更新 vitest.setup.ts 添加 contextMenus mock

* feat: 实现 Content Script 原位轻量提示 UI

- 新增 uiPopover.ts 实现原位弹窗组件(深色主题、自动定位、自动隐藏)
- 新增 contextMenuHandler.ts 处理右键菜单消息
- 时间戳转换:在点击位置显示转换结果
- 文本统计:显示字符/单词/行数/字节统计
- 更新 messageHandler.ts 挂载右键菜单消息监听

* feat: 实现 React 页面层右键菜单数据联动

- 新增 useContextMenuData Hook 处理右键菜单数据传递
- 重构 Jwt/Base64Converter/TextStatistics/QrCode 页面接收右键数据
- RouterProvider 支持从 URL 参数解析右键菜单数据
- 添加 contextMenu/pendingData 存储键到 StorageSchema
- 添加 useContextMenuData 单元测试(12个测试用例)

* test: 添加 background 单元测试与边界情况处理

- 新增 entrypoints/__tests__/background.test.ts (12个测试用例)
- 超长文本截断限制 (>10000字符)
- Base64 内联图片拦截并返回错误提示
- 更新 parseContextMenuClick 返回 ParseResult 格式
- 更新测试匹配新的返回格式
- lint/typecheck/test 全部通过 (579个测试)

* fix: 修复右键菜单跳转到错误页面的问题

- 将 options 页面路径从 '/entrypoints/options/index.html' 修正为 '/options.html'
- WXT 构建后 entrypoints/options/index.html 会输出为根目录的 options.html

* fix: 修复右键菜单跳转目标为 popup 页面

- 将 fallback 页面从 options.html 改为 popup.html
- popup 页面使用 RouterProvider,可以正确处理 URL 参数并跳转到对应功能页面
- options 页面是独立设置页,不支持 URL 参数路由

* fix: 使用 openPopup() 替代 tabs.create() 打开弹窗

- background.ts: 使用 browser.action.openPopup() 打开 popup 弹窗
- 先保存数据到 storage,然后打开 popup
- RouterProvider.tsx: 初始化时检查 storage 中的 pendingData 并跳转到对应页面
- 移除之前错误的 tabs.create() 方式

* fix: 修复右键菜单数据未传递到目标页面的问题

- RouterProvider 不再提前清除 pendingData
- 让目标页面的 useContextMenuData 来消费和清除数据
- 这样确保 TextStatistics 等页面能正确接收选中的文本

* fix: 修复 popup 已打开时右键菜单不生效的问题

- 在 RouterProvider 中添加对 contextMenu/pendingData 的 storage 变化监听
- 当 pendingData 变化时,自动跳转到对应功能页面
- 解决了 openPopup() 只聚焦已有窗口而不触发重新挂载的问题

* feat: 支持右键菜单图片二维码识别

- 在 qrCodeParser.ts 中添加 parseQrCodeFromUrl 函数,支持从图片 URL 解析二维码
- 在 QrCodeToUrlSection 中使用 useContextMenuData 接收右键菜单传递的图片 URL
- 自动下载图片并解析二维码,显示解析结果

* refactor: 暂时移除图片二维码识别功能

- 删除 qrCode-image 右键菜单配置
- 删除 parseQrCodeFromUrl 函数
- 删除 QrCodeToUrlSection 中的 useContextMenuData 相关代码
- 更新相关测试用例

* feat: 支持右键菜单时间戳转换

- 在 useTimestampConverter 中添加 useContextMenuData hook
- 智能识别输入是时间戳还是日期时间字符串
- 自动切换到对应模式并执行转换
- 时间戳自动识别秒/毫秒单位

* fix: 修复右键菜单功能的多个逻辑漏洞

1. 修复时区硬编码问题 - 使用用户选择的时区而非固定 Asia/Shanghai
2. 修复 openPopup() 失败后数据残留 - 失败时清除 storage 中的待处理数据
3. 统一数据过期时间常量 - 导出 CONTEXT_MENU_DATA_EXPIRY_MS 并统一使用
4. 修复 featureKey 类型断言不安全 - 使用映射表处理非常规菜单 ID

* refactor(qrcode): 提取独立组件并添加单元测试

- 创建 pages/QrCode/types.ts 定义状态类型接口
- 提取 QrCodePreview 组件用于二维码预览和操作
- 提取 ImageUploader 组件封装图片上传、拖拽、粘贴逻辑
- 重构 UrlToQrCodeSection 状态提升到父组件
- 重构 QrCodeToUrlSection 通过回调传递解析结果
- 更新主页面 index.tsx 集中管理所有状态
- 为 QrCodePreview 和 ImageUploader 添加单元测试 (23 个用例)

* refactor(qrcode): 引入通用组件并重构双栏布局

- 引入 SwitchButtonGroup 用于模式切换
- 引入 TextInputArea 用于文本输入和结果展示
- 使用 MUI Grid 构建响应式双栏布局
- 实现 generate/parse 模式下的左右面板内容切换
- 修复 iconColor 颜色格式错误,使用 qrCodePageStyles.primaryColor

* refactor(qrcode): 国际化补充、质量保障与错误处理优化

- 补充 i18n 翻译键(generateMode, parseMode, pasteHint 等)
- 创建 useDebounce Hook 实现输入防抖 (200ms)
- 使用 useRef 解决 useEffect 无限循环问题
- 编写 pages/QrCode 单元测试 (7 个用例)
- 优化右键菜单错误处理,改进用户提示
- 运行 603 个测试全部通过

* refactor(qrcode): 组件化重构,采用 Context + Hook 模式

- 创建 QrCodeContext 和 QrCodeProvider 管理共享状态
- 提取 useQrCode Hook 封装所有状态和业务逻辑
- 创建 GeneratePanel 组件处理二维码生成模式
- 创建 ParsePanel 组件处理二维码解析模式
- 简化 index.tsx 为容器组件 (340行 → 47行)
- 删除未使用的旧组件文件 (QrCodeToUrlSection, UrlToQrCodeSection)
- 清理 types.ts 中未使用的类型定义
- 603 个测试全部通过

* fix(qrcode): 固定二维码预览图片尺寸,避免布局抖动

- 设置 QR_PREVIEW_IMAGE 固定尺寸 250x250
- 与 QRious 生成的二维码大小保持一致
- 解决输入内容变化时预览窗口大小跳动问题

* fix(qrcode): 修复切换 tab 后二维码图片失效问题

- 移除 ImageUploader 组件卸载时的预览 URL 释放逻辑
- 在 useQrCode hook 中统一管理预览 URL 生命周期
- 创建新预览 URL 前释放旧的,避免内存泄漏

* fix(qrcode): 固定图片上传区域高度,避免布局抖动

- 将 DROPZONE 的 minHeight: 200 改为 height: 250
- 与二维码预览图片高度保持一致
- 解决上传图片时 div 高度变化问题

* refactor(qrCode): 优化二维码解析功能

- 将'二维码转 URL'改为'二维码转文本'
- 将解析结果的文本预览框的 placeholder 置为空
- 去除手动解析二维码功能,只保留自动解析

* fix(pageHeader): 使用 MUI 主题色替代硬编码颜色,支持暗色模式

* feat(pageHeader): popup 模式下隐藏 PageHeader 组件

* fix(i18n): 统一使用 useLazyTranslation 避免翻译 key 闪烁

* perf: 配置 manualChunks 拆分 vendor chunk,优化打包体积

- 使用 Vite 插件 manualChunksForHtmlOnly 仅对 HTML 多入口构建生效
- 跳过 background/content-script 的 IIFE 构建(不支持 manualChunks)
- 拆分 vendor-react (193KB)、vendor-mui (326KB)、vendor-i18n (55KB)
- 按需加载 vendor-qr (78KB)、vendor-dnd (45KB)、vendor-markdown (41KB)
- PageErrorBoundary chunk 从 454KB 降至 36KB
2026-05-21 01:34:35 +08:00
雨霖铃 72f899477c perf: 配置 manualChunks 拆分 vendor chunk,优化打包体积
- 使用 Vite 插件 manualChunksForHtmlOnly 仅对 HTML 多入口构建生效
- 跳过 background/content-script 的 IIFE 构建(不支持 manualChunks)
- 拆分 vendor-react (193KB)、vendor-mui (326KB)、vendor-i18n (55KB)
- 按需加载 vendor-qr (78KB)、vendor-dnd (45KB)、vendor-markdown (41KB)
- PageErrorBoundary chunk 从 454KB 降至 36KB
2026-05-21 01:25:19 +08:00
雨霖铃 8e2ac5200d fix(i18n): 统一使用 useLazyTranslation 避免翻译 key 闪烁 2026-05-21 00:50:01 +08:00
雨霖铃 604fbb4d1f feat(pageHeader): popup 模式下隐藏 PageHeader 组件 2026-05-21 00:38:00 +08:00
雨霖铃 704ec7289d fix(pageHeader): 使用 MUI 主题色替代硬编码颜色,支持暗色模式 2026-05-21 00:34:02 +08:00
雨霖铃 e4fac241f1 refactor(qrCode): 优化二维码解析功能
- 将'二维码转 URL'改为'二维码转文本'
- 将解析结果的文本预览框的 placeholder 置为空
- 去除手动解析二维码功能,只保留自动解析
2026-05-21 00:22:47 +08:00
雨霖铃 4b339fc628 fix(qrcode): 固定图片上传区域高度,避免布局抖动
- 将 DROPZONE 的 minHeight: 200 改为 height: 250
- 与二维码预览图片高度保持一致
- 解决上传图片时 div 高度变化问题
2026-05-21 00:13:56 +08:00
雨霖铃 70580a9a63 fix(qrcode): 修复切换 tab 后二维码图片失效问题
- 移除 ImageUploader 组件卸载时的预览 URL 释放逻辑
- 在 useQrCode hook 中统一管理预览 URL 生命周期
- 创建新预览 URL 前释放旧的,避免内存泄漏
2026-05-21 00:12:06 +08:00
雨霖铃 99a0400db3 fix(qrcode): 固定二维码预览图片尺寸,避免布局抖动
- 设置 QR_PREVIEW_IMAGE 固定尺寸 250x250
- 与 QRious 生成的二维码大小保持一致
- 解决输入内容变化时预览窗口大小跳动问题
2026-05-21 00:09:17 +08:00
雨霖铃 df0b9440ca refactor(qrcode): 组件化重构,采用 Context + Hook 模式
- 创建 QrCodeContext 和 QrCodeProvider 管理共享状态
- 提取 useQrCode Hook 封装所有状态和业务逻辑
- 创建 GeneratePanel 组件处理二维码生成模式
- 创建 ParsePanel 组件处理二维码解析模式
- 简化 index.tsx 为容器组件 (340行 → 47行)
- 删除未使用的旧组件文件 (QrCodeToUrlSection, UrlToQrCodeSection)
- 清理 types.ts 中未使用的类型定义
- 603 个测试全部通过
2026-05-21 00:07:42 +08:00
雨霖铃 c5b51a9b33 refactor(qrcode): 国际化补充、质量保障与错误处理优化
- 补充 i18n 翻译键(generateMode, parseMode, pasteHint 等)
- 创建 useDebounce Hook 实现输入防抖 (200ms)
- 使用 useRef 解决 useEffect 无限循环问题
- 编写 pages/QrCode 单元测试 (7 个用例)
- 优化右键菜单错误处理,改进用户提示
- 运行 603 个测试全部通过
2026-05-20 22:42:33 +08:00
雨霖铃 bc560cc453 refactor(qrcode): 引入通用组件并重构双栏布局
- 引入 SwitchButtonGroup 用于模式切换
- 引入 TextInputArea 用于文本输入和结果展示
- 使用 MUI Grid 构建响应式双栏布局
- 实现 generate/parse 模式下的左右面板内容切换
- 修复 iconColor 颜色格式错误,使用 qrCodePageStyles.primaryColor
2026-05-20 22:12:48 +08:00
雨霖铃 2870e01610 refactor(qrcode): 提取独立组件并添加单元测试
- 创建 pages/QrCode/types.ts 定义状态类型接口
- 提取 QrCodePreview 组件用于二维码预览和操作
- 提取 ImageUploader 组件封装图片上传、拖拽、粘贴逻辑
- 重构 UrlToQrCodeSection 状态提升到父组件
- 重构 QrCodeToUrlSection 通过回调传递解析结果
- 更新主页面 index.tsx 集中管理所有状态
- 为 QrCodePreview 和 ImageUploader 添加单元测试 (23 个用例)
2026-05-20 21:59:57 +08:00
雨霖铃 9c03c585a8 merge: 合并 feature-context-menu 分支
功能特性:
- 右键菜单支持选中文本/图片触发对应工具
- 支持 JWT 解析、Base64 解码、文本统计、时间戳转换
- 网页链接转二维码
- popup/sidepanel 双入口支持
- 智能识别输入类型自动转换

修复:
- 时区硬编码问题
- openPopup 失败后数据残留
- 统一数据过期时间常量
- 类型断言安全性改进
2026-05-20 21:40:21 +08:00
雨霖铃 0427f16cc9 fix: 修复右键菜单功能的多个逻辑漏洞
1. 修复时区硬编码问题 - 使用用户选择的时区而非固定 Asia/Shanghai
2. 修复 openPopup() 失败后数据残留 - 失败时清除 storage 中的待处理数据
3. 统一数据过期时间常量 - 导出 CONTEXT_MENU_DATA_EXPIRY_MS 并统一使用
4. 修复 featureKey 类型断言不安全 - 使用映射表处理非常规菜单 ID
2026-05-20 21:39:33 +08:00
雨霖铃 e9a6fa3dd8 feat: 支持右键菜单时间戳转换
- 在 useTimestampConverter 中添加 useContextMenuData hook
- 智能识别输入是时间戳还是日期时间字符串
- 自动切换到对应模式并执行转换
- 时间戳自动识别秒/毫秒单位
2026-05-20 21:32:35 +08:00
雨霖铃 00fe3eddd9 refactor: 暂时移除图片二维码识别功能
- 删除 qrCode-image 右键菜单配置
- 删除 parseQrCodeFromUrl 函数
- 删除 QrCodeToUrlSection 中的 useContextMenuData 相关代码
- 更新相关测试用例
2026-05-20 21:27:36 +08:00
雨霖铃 8e920c2a94 feat: 支持右键菜单图片二维码识别
- 在 qrCodeParser.ts 中添加 parseQrCodeFromUrl 函数,支持从图片 URL 解析二维码
- 在 QrCodeToUrlSection 中使用 useContextMenuData 接收右键菜单传递的图片 URL
- 自动下载图片并解析二维码,显示解析结果
2026-05-20 21:19:39 +08:00
雨霖铃 387a95d6a6 fix: 修复 popup 已打开时右键菜单不生效的问题
- 在 RouterProvider 中添加对 contextMenu/pendingData 的 storage 变化监听
- 当 pendingData 变化时,自动跳转到对应功能页面
- 解决了 openPopup() 只聚焦已有窗口而不触发重新挂载的问题
2026-05-20 21:15:43 +08:00
雨霖铃 71995037dd fix: 修复右键菜单数据未传递到目标页面的问题
- RouterProvider 不再提前清除 pendingData
- 让目标页面的 useContextMenuData 来消费和清除数据
- 这样确保 TextStatistics 等页面能正确接收选中的文本
2026-05-20 21:09:47 +08:00
雨霖铃 8e56ebff6e fix: 使用 openPopup() 替代 tabs.create() 打开弹窗
- background.ts: 使用 browser.action.openPopup() 打开 popup 弹窗
- 先保存数据到 storage,然后打开 popup
- RouterProvider.tsx: 初始化时检查 storage 中的 pendingData 并跳转到对应页面
- 移除之前错误的 tabs.create() 方式
2026-05-20 21:07:06 +08:00
雨霖铃 5868316c3a fix: 修复右键菜单跳转目标为 popup 页面
- 将 fallback 页面从 options.html 改为 popup.html
- popup 页面使用 RouterProvider,可以正确处理 URL 参数并跳转到对应功能页面
- options 页面是独立设置页,不支持 URL 参数路由
2026-05-20 21:03:55 +08:00
雨霖铃 8ec584c85f fix: 修复右键菜单跳转到错误页面的问题
- 将 options 页面路径从 '/entrypoints/options/index.html' 修正为 '/options.html'
- WXT 构建后 entrypoints/options/index.html 会输出为根目录的 options.html
2026-05-20 21:00:11 +08:00
雨霖铃 c1890960ea test: 添加 background 单元测试与边界情况处理
- 新增 entrypoints/__tests__/background.test.ts (12个测试用例)
- 超长文本截断限制 (>10000字符)
- Base64 内联图片拦截并返回错误提示
- 更新 parseContextMenuClick 返回 ParseResult 格式
- 更新测试匹配新的返回格式
- lint/typecheck/test 全部通过 (579个测试)
2026-05-20 20:54:51 +08:00
雨霖铃 3b98c9438e feat: 实现 React 页面层右键菜单数据联动
- 新增 useContextMenuData Hook 处理右键菜单数据传递
- 重构 Jwt/Base64Converter/TextStatistics/QrCode 页面接收右键数据
- RouterProvider 支持从 URL 参数解析右键菜单数据
- 添加 contextMenu/pendingData 存储键到 StorageSchema
- 添加 useContextMenuData 单元测试(12个测试用例)
2026-05-20 20:51:11 +08:00
雨霖铃 471e17eedf feat: 实现 Content Script 原位轻量提示 UI
- 新增 uiPopover.ts 实现原位弹窗组件(深色主题、自动定位、自动隐藏)
- 新增 contextMenuHandler.ts 处理右键菜单消息
- 时间戳转换:在点击位置显示转换结果
- 文本统计:显示字符/单词/行数/字节统计
- 更新 messageHandler.ts 挂载右键菜单消息监听
2026-05-20 20:44:00 +08:00
雨霖铃 3fb99ea49f feat: 实现右键菜单注册与点击分流逻辑
- 新增 utils/contextMenu.ts 封装菜单配置和解析函数
- 在 background.ts 监听 onInstalled 初始化菜单
- 实现 contextMenus.onClicked 点击事件处理
- 分流逻辑:优先发送到侧边栏,否则打开 options 页面
- 添加 contextMenu 单元测试(15个测试用例)
- 更新 vitest.setup.ts 添加 contextMenus mock
2026-05-20 20:41:24 +08:00
雨霖铃 3d375f78f3 feat: 添加 contextMenus 权限和消息类型定义 2026-05-20 20:35:55 +08:00
LingandRX 373fc0496c Develop (#26)
统一简化多个页面里的CopyButton调用,删除不再需要的空回调参数

* feat: 添加clearCookies函数的单元测试并修复cookie域名处理逻辑

* feat: 增强 data URI 处理,支持带参数的前缀并更新相关测试

* fix: 修正 AGENTS.md 中 TypeScript 类型检查命令的描述

* feat: 添加 settings.local.json 文件以配置 Bash 权限

* perf: 预设背景色避免 Popup 弹窗白屏闪烁

* perf: 避免图标过早实例化,传递组件引用而非 JSX 节点

* feat: 添加 useLazyTranslation 和 preloadNamespaces 函数以支持动态加载 i18n 命名空间

* feat: 使用 useLazyTranslation 替换 useTranslation 以支持懒加载翻译

* feat: 添加 PageSkeleton 组件及其测试用例以支持页面加载骨架屏

* feat: 使用骨架屏替换加载状态指示器,优化用户体验

* feat: 优化 CopyButton 组件的复制功能,添加定时器管理复制状态

* feat: 调整 chunk 大小警告阈值以优化构建性能
2026-05-20 20:07:44 +08:00
LingandRX 7167a43763 Develop (#25)
核心功能新增
工具套件扩展
JSON 工具(差异比较、格式化、转 YAML/TOML、压缩)
Base64 转换器(文本/文件/图像编解码)
Markdown ↔ HTML 双向转换
JWT 解析器
文本统计工具
二维码生成/解析(替换库以减小体积)
表单工具(已移除)
表单映射、智能填充、高亮定位功能已移除
架构与工程改进
技术栈升级
引入 @webext-core/messaging 重构消息通信
添加 @dnd-kit 支持功能列表拖拽排序
引入 i18next 实现中英文国际化
使用 MUI 主题系统替代硬编码样式,支持暗色模式
代码质量
统一组件导出为默认导出
提取公共组件(TextInputArea、SwitchButtonGroup、PageHeader、DecodeResultPaper)
使用 useStorageState 钩子统一管理存储状态
添加 PageErrorBoundary 页面级错误边界
实现组件懒加载优化首屏性能
关键重构
路由系统重构 — 支持独立标签页模式,合并路由与功能配置
状态管理优化 — 存储状态增加快照机制、验证器和防抖处理
剪贴板与打印工具 — 统一为 async/await 形式,修复重复触发问题
存储清理器 — 使用 TextEncoder 准确计算 UTF-8 字节数,支持 TB 单位
测试与 CI
测试覆盖 — 为新增工具、公共组件、边界场景补充单元测试
CI 精简 — 移除 Firefox 测试,仅保留 Chrome;修正类型检查命令
2026-05-20 19:55:49 +08:00
LingandRX 0cf8bae9f1 Develop (#24)
* docs: 添加组件文档注释和类型导入

refactor: 统一使用 SnackbarOptions 类型
style: 优化导入语句顺序和格式

* refactor: 简化假数据生成器中的faker导入和使用

Co-authored-by: Copilot <copilot@github.com>

* feat(form-recognizer): 增强表单识别功能并优化UI交互

- 新增字段类型偏好设置功能,支持按域名保存字段类型
- 重构FieldList组件,改进字段选择和类型修改体验
- 添加字段定位闪烁功能,便于在页面上快速找到对应字段
- 优化表单填充逻辑,支持单个字段覆盖默认填充模式
- 移除独立的侧边栏页面,统一使用主页面组件
- 改进useStorageState钩子,增加加载状态管理和防抖处理

* refactor: 移除未使用的组件文件

* refactor(页面头部): 提取通用 PageHeader 组件并替换各页面头部实现

重构各页面头部为统一的 PageHeader 组件,提高代码复用性和维护性

* style(组件): 调整自动刷新开关和存储选项网格的样式

优化自动刷新开关的文本内边距,重构存储选项网格的布局结构,调整间距和边框样式

* style(ui): 调整时间戳页面和结果视图的样式

- 为时区选择器添加圆角
- 优化结果视图的布局和对齐方式
- 调整结果项的内边距和文本样式

* ci(workflow): 移除Firefox测试以简化CI流程

仅保留Chrome浏览器的构建步骤,减少CI运行时间和资源消耗

* refactor(qrcode): 重构二维码解析功能并提取为独立模块

* style(页面样式): 统一页面头部图标颜色并优化表单样式

更新各页面头部组件的图标颜色配置,移除冗余的表单标签属性,优化输入框样式和按钮悬停效果

* feat(formMapping): 添加表单映射功能

- 创建 FormMappingPage 页面组件
- 实现表单扫描器 scanner.ts
- 实现高亮器 highlighter.ts
- 更新路由配置 routes.ts
- 更新内容脚本 content.ts
- 更新类型定义 storage.d.ts
- 添加 formMappingPageStyles 样式配置

* feat(表单映射): 添加配置导出功能及状态提示

添加配置导出为JSON文件的功能,包含导出按钮和错误处理。新增Snackbar组件用于显示导出成功和错误状态。优化页面布局结构,将状态提示移至全局容器外。

* feat(表单填充): 新增智能表单填充功能

添加智能表单填充功能,包括:
1. 新增表单填充页面和路由配置
2. 实现模糊匹配引擎和智能注入引擎
3. 添加Mock数据生成器和视觉反馈渲染器
4. 扩展消息接口支持填充操作

* ci: 精简触发CI的工作流分支

移除对develop分支及其变体的触发,仅保留main分支的触发

* test: 更新路由测试以匹配新增的路由数量

* test: 将测试文件中的描述和断言翻译为中文

* test(PageHeader): 添加组件测试用例验证渲染逻辑和样式

* refactor(theme): 重构主题样式并优化仪表盘卡片组件

将页面样式配置集中管理,移除各页面中硬编码的背景色
新增 DashboardCard 组件封装通用卡片逻辑
添加 dashboardCards 配置文件统一管理卡片数据

* refactor(Button): 统一按钮样式并移除重复样式定义

将按钮样式统一封装到 Button 组件中,移除各页面重复的样式定义
更新组件文档说明,提供更清晰的用法示例

* docs: 更新 README 文件中的目录结构说明

添加新组件和配置文件的说明,保持文档与代码同步

* refactor: 优化代码类型声明和UI布局

修复类型声明从any改为never以提高类型安全性
调整TimestampPage页面布局间距
移除DashboardPage中不必要的实时时钟状态
更新ESLint配置以使用推荐配置
添加选项页面打开的错误处理

* refactor(消息通信): 使用 @webext-core/messaging 重构消息处理逻辑

将原有的 chrome.runtime.onMessage 和 chrome.tabs.sendMessage 替换为类型安全的 @webext-core/messaging 实现
添加 ProtocolMap 类型定义确保消息类型安全
更新相关页面和后台脚本使用新的消息通信方式

* fix: 修复依赖项缺失导致的潜在问题

修复 useStorageState 中缺少 defaultValue 依赖的问题
重构 RouterProvider 的初始化逻辑,使用 useCallback 优化性能

* refactor: 重构表单识别和消息处理逻辑

将表单映射UI逻辑提取到独立文件
将消息处理器提取到独立文件
将表单识别页面逻辑提取到自定义hook
优化代码结构和可维护性

* feat(侧边栏): 添加侧边栏状态变化通知功能

- 在消息类型中新增侧边栏状态变化枚举和字段
- 侧边栏打开和关闭时发送状态通知
- 替换轮询检查方式为消息监听机制

* refactor: 优化高亮组件渲染逻辑并添加防抖处理

重构高亮组件渲染逻辑,使用 requestAnimationFrame 进行节流优化
在 useStorageCleaner 中添加防抖处理以避免频繁加载

* refactor: 统一组件导出方式为默认导出

* refactor: 重构样式系统并迁移至 MUI 主题

删除冗余的 CSS 文件,统一使用 MUI 主题管理样式
新增 useActiveTabDomain 和 useSidePanelState 自定义 Hook
优化各入口点的主题集成和布局处理

* docs: 更新项目文档以反映新增功能和技术栈细节

更新 AGENTS.md 文档,详细描述新增的功能模块(存储管理、URL 管理、二维码生成、表单工具套件等)和更新的技术栈信息

* feat: 添加错误边界组件以捕获子组件错误

在 popup、sidepanel 和 options 入口点添加 ErrorBoundary 组件,用于捕获并处理子组件中的 JavaScript 错误。当错误发生时,显示友好的错误界面并提供刷新功能。

* refactor(ui): 使用全局 snackbar 替换本地通知组件

重构表单页面中的通知系统,移除本地 Snackbar 和 Alert 组件,改用全局 GlobalSnackbar 组件统一管理通知
标准化主题配置中的 CSS 属性命名,移除带前缀的样式属性

* refactor(snackbar): 重构全局消息提示为 SnackbarProvider 组件

将原本分散在各页面的 GlobalSnackbar 组件重构为集中管理的 SnackbarProvider,通过 Context 提供统一的消息提示功能。主要变更包括:

1. 创建新的 SnackbarProvider 组件作为全局消息提示容器
2. 提供 useSnackbar hook 供子组件调用
3. 移除各页面中独立的 GlobalSnackbar 实例
4. 在 App 根组件中统一集成 SnackbarProvider
5. 优化消息提示样式和交互行为

* refactor(Snackbar): 重构消息提示组件并优化样式

重构 Snackbar 组件,将状态管理逻辑提取到 GlobalSnackbar 中复用
优化消息提示样式,调整阴影效果和最小尺寸
统一各页面使用 Snackbar 的方式,移除冗余配置

* refactor: 重构路由和功能配置系统

- 将路由配置和仪表盘卡片配置合并为统一的 features 配置
- 重构 RouterProvider 增加数据校验和本地快照功能
- 优化表单字段识别和填充逻辑,提取公共方法
- 更新相关组件和文档以适配新的配置系统
- 删除废弃的 dashboardCards 和 routes 配置文件

* refactor(GlobalSnackbar): 合并SnackbarProvider到GlobalSnackbar组件

重构Snackbar相关代码,将SnackbarProvider合并到GlobalSnackbar组件中,简化代码结构
更新相关组件引用路径,从SnackbarProvider改为GlobalSnackbar
修复StorageCleanerPage禁用状态逻辑问题

* feat(表单映射): 增强高亮器功能并优化元素定位

- 新增高亮器闪烁功能,支持自定义持续时间
- 改进元素定位算法,支持 Shadow DOM 穿透
- 优化拾取模式,支持连续拾取和悬停预览
- 增强错误处理和信息反馈
- 添加测试用例确保核心功能稳定性

* refactor: 移除表单映射相关功能

删除表单映射相关的UI、逻辑和测试代码,包括表单识别、智能填充和高亮功能
简化消息处理逻辑和配置文件,保留核心功能

* feat: 实现页面组件懒加载并优化加载状态显示

refactor: 移除未使用的数据生成和验证工具类

chore: 更新应用图标文件

* refactor(qrcode): 替换二维码生成和解析库以减小体积

使用 qr-scanner 替代 jsqr 进行二维码解析,使用 qrious 替代 qrcode 进行二维码生成
新的库体积更小且性能更好,同时移除了不再使用的 @faker-js/faker 依赖

* feat: 添加在标签页打开功能并优化错误处理

修复测试中的加载状态检查
将控制台日志改为错误捕获
添加全局消息提示组件

* feat: 支持独立标签页模式并重构路由配置

重构路由系统以支持独立标签页模式,替换原有的独立窗口模式
- 新增标签页路由、可见页面列表和页面顺序的存储键
- 修改 RouterProvider 以支持不同窗口类型的独立配置
- 更新选项页面以支持为不同窗口类型单独配置
- 添加相关单元测试

* feat(ui): 优化响应式布局和样式设计

重构选项页面布局,增强移动端适配性
更新主题配置和组件样式,提升用户体验

* refactor: 移除URL工具相关代码

移除不再需要的URL工具功能,包括组件、页面、样式和类型定义

* refactor: 简化页面标题和存储选项网格的响应式布局

* refactor: 优化样式和错误处理

- 简化 chrome.runtime.openOptionsPage 的错误处理
- 拆分 html, body, #root 的样式定义,使用 minHeight 替代 height

* refactor: 重构页面组件结构
- 将页面组件从 entrypoints/popup/pages 移动到 pages 目录
- 更新文档中的路径引用

* style(DashboardPage): 优化仪表盘布局为响应式网格

refactor(ToolCard): 重构工具卡片组件并添加详细注释

* feat: 新增文本统计功能

添加文本统计工具页面及相关功能,包括:
- 文本统计工具页面组件
- 文本统计计算工具函数
- 相关类型定义和样式配置
- 单元测试覆盖核心功能

* feat(jwt): 添加 JWT 解析工具功能

新增 JWT 解析工具页面,包含以下内容:
- 添加 JWT 类型定义和页面样式配置
- 实现 JWT 解析核心功能及测试用例
- 创建 JWT 解析页面组件
- 在功能配置中注册 JWT 工具

* docs: 更新 README 以更清晰地描述项目功能和结构

- 重构功能描述部分,使用更简洁的列表形式展示
- 更新技术栈和项目结构说明
- 优化开发流程部分的描述
- 移除冗余的权限和依赖说明

* docs: 更新AGENTS.md文档以反映项目最新状态

- 重写项目概述和核心命令部分,使其更简洁准确
- 重构目录结构描述,采用更清晰的树状格式
- 移除过时的功能模块描述,聚焦当前核心功能
- 更新AI代理开发准则和权限管理说明
- 优化文档整体结构和可读性

* test: 更新特征计数测试以匹配新增 textStatistics 和 jwt 工具

* style: 统一导入语句顺序

* refactor(pages): 重构工具页面组件结构并优化功能实现

- 将各工具页面组件迁移至独立目录并重构为模块化结构
- 新增存储清理工具相关组件及状态管理逻辑
- 实现时间戳转换工具的自定义hook和UI组件
- 优化JWT解析工具的代码结构和样式
- 更新文本统计工具的性能计算逻辑
- 调整配置文件中的页面引用路径
- 删除无用文件和过时文档

* feat(顶部栏): 添加搜索功能和历史记录

实现顶部栏搜索框功能,支持模糊搜索工具并显示搜索结果列表
添加搜索历史记录功能,自动保存最近搜索并显示
支持键盘导航和选择搜索结果

* feat: 添加多语言支持(i18n)

实现应用的多语言功能,支持中英文切换
添加i18next和相关依赖
创建语言资源文件
更新组件以使用翻译功能
重构文本统计工具页面
完善测试和构建配置

* feat(顶部栏): 添加语言切换功能并优化国际化支持
feat(i18n): 增加语言归一化和同步功能
fix(剪贴板): 使用国际化文本替换剪贴板消息
test(顶部栏): 更新测试以匹配国际化标题

* refactor: 重构组件目录结构,将组件移动到对应功能页面目录

将多个组件从通用组件目录移动到各自功能页面目录下,包括 ToolCard、StorageCleanerConfirm、UrlToQrCodeSection 和 QrCodeToUrlSection。同时新增 Dashboard 页面相关组件 DashboardCard 和 ToolCard,并优化了组件导入路径。

重构旨在提高代码组织性,使组件与其所属功能页面更紧密关联,便于维护和理解代码结构。所有组件功能保持不变,仅调整文件位置和导入路径。

* refactor(Dashboard): 优化卡片组件结构并移除无用属性

重构 DashboardCard 组件接口,直接传递属性而非配置对象
移除 ToolCard 中无用的 hasAI 属性
使用 useMemo 优化可见性判断性能
统一使用 alpha 函数处理颜色透明度

* refactor(timestamp): 合并输入处理函数并优化样式结构

重构时间戳转换器的输入处理逻辑,将 setTsInput 和 setDtInput 合并为统一的 setInput 函数
将组件样式提取到 pageTheme 配置中统一管理
使用 ToggleButtonGroup 替换自定义模式切换器实现
移除不必要的 showMessage 属性传递,改用 useSnackbar 钩子

* refactor(dashboard): 重构仪表盘卡片组件并优化布局

- 删除冗余的 DashboardCard 组件,直接使用 ToolCard
- 将网格布局样式移至主题配置
- 使用 CardActionArea 增强可访问性
- 简化点击事件处理逻辑
- 更新相关测试用例

* refactor(storageCleaner): 重构样式代码并优化国际化处理

将分散的样式代码集中到pageTheme.ts中统一管理
优化组件中的国际化文本引用
修复useStorageCleaner中的状态同步问题
更新测试用例以匹配国际化文本

* refactor(StorageCleaner): 优化条件判断和清理未使用的代码

简化禁用按钮的条件判断逻辑
移除未使用的 resultTimeoutRef 及相关清理代码

* refactor(QrCode): 重构二维码页面样式和组件结构

将内联样式提取到主题配置文件中
移除未使用的全局消息提示依赖
优化组件结构和代码复用
删除未使用的QrCodeUploader组件

* refactor(ErrorBoundary): 移除冗余状态更新并调整错误背景色

移除 componentDidCatch 中的冗余 setState 调用
将错误背景色从 'error.shortest' 改为 rgba 格式

* feat(TopBar): 优化顶部导航栏功能并添加样式配置

- 新增 openExtensionPage 工具函数用于打开扩展页面
- 将顶部导航栏样式配置提取到 pageTheme 中统一管理
- 改进搜索历史功能,增加错误处理和配置化限制
- 优化键盘导航和搜索结果的交互逻辑

* chore: 在.gitignore中添加docs目录

避免将文档生成目录提交到版本控制

* feat(JsonDiff): add type definitions for diff engine

* feat: 新增 JSON 差异比较工具功能

添加 JSON 差异比较工具页面,支持并排和统一视图模式,包含以下功能:
- 左右两侧 JSON 输入框及校验
- 差异高亮显示及导航
- 支持对象和数组的深度比较
- 国际化支持
- 单元测试覆盖核心差异算法

* feat(主题): 添加暗色模式支持并重构主题系统

- 新增 ThemeModeProvider 提供亮色/暗色/系统模式切换功能
- 重构所有组件使用 MUI 主题变量替代硬编码颜色
- 更新测试用例以适配新的主题系统
- 添加 i18n 翻译支持主题切换相关文本
- 统一入口点使用 AppRoot 组件封装主题和路由提供者

* feat(jsonDiff): 新增 JSON 格式化功能模块

- 新增 jsonFormat 语言资源,支持中英文文本
- 新增 JsonFormatSection 组件,提供 JSON 格式化输入与结果展示
- 在 JsonDiff 页面加入模式切换,支持差异比较与格式化双模式
- 实现 formatJson 和 validateJson 工具方法,支持缩进大小和键名排序选项
- 增加格式化结果显示大小统计和一键复制功能
- 写入大量单元测试,覆盖格式化和校验功能边界
- 优化页面布局与交互,提升用户体验和功能集成度

* feat(jsonDiff): 新增 JSON 转 YAML/TOML 及压缩功能

- 在语言文件(en/zh)中添加 YAML、TOML 和 JSON 压缩模式相关文案
- 扩展 JsonDiff 页面,新增 yaml、toml、minify 三种页面模式
- 新增 JsonConvertSection 组件,提供输入校验、转换操作及结果展示
- 实现 jsonToYaml 工具函数,将 JSON 转换为符合规范的 YAML 格式
- 实现 jsonToToml 工具函数,将 JSON 转换为符合规范且支持嵌套的 TOML 格式
- 实现 minifyJson 工具函数,实现 JSON 字符串压缩为单行格式
- 为所有新功能增加单元测试,覆盖正常及异常场景,保证转换正确性和稳定性
- 在页面中添加对应的切换按钮和图标,提升用户体验和操作便捷性

* refactor(jsonTools): 重命名 JSON 差异比较为 JSON 工具

- 将 JsonDiff 页面组件重命名为 JsonTools 并更新相关引用
- 修改功能配置中对应组件的名称以匹配新命名
- 更新英文和中文语言包中该功能的标题及描述
- 调整测试文件中导入路径以使用新的 JsonTools 文件夹名

* feat(storage): 优化本地存储状态管理和 JSON 工具页模式持久化

- 添加 useStorageState 钩子,支持同步快照和异步加载存储状态
- 实现快照机制,防止页面首屏闪烁现象
- 增加状态值验证器功能,保证恢复数据合法性
- JSON 工具页面模式改用受控存储状态持久化
- 新增 JsonToolsPageMode 类型定义,统一页面模式类型
- 为 useStorageState 编写完整单元测试,覆盖快照、校验及存储同步场景

* feat(base64Converter): 新增 Base64 转换器功能模块

- 添加 Base64 转换器页面及相关样式配置
- 支持文本、文件及图像三种转换模式,含编码和解码功能
- 实现拖拽、文件选择及图像预览功能
- 增加中文与英文多语言资源文件支持
- 补充相关工具函数,支持编码解码及格式校验
- 扩展路由提供者逻辑,合并已保存列表与默认功能集合
- 更新测试用例,覆盖合并新功能的场景验证
- 在特性配置中加入 Base64 转换器信息,支持图标和默认显示配置

* feat(markdown): 新增 Markdown 转 HTML 功能模块

- 添加 marked 依赖及类型定义支持 Markdown 解析
- 新增 MarkdownToHtml 页面,支持实时编辑、预览及 HTML 输出三种模式切换
- 实现 markdownToHtml 工具函数进行 Markdown 转 HTML 操作,含错误处理
- 包装完整 HTML 文档功能,支持打印和下载导出
- 配置全局主题样式,优化 Markdown 渲染视觉效果
- 完善 i18n 国际化资源,支持多语言页面展示
- 统一类型定义,新增 MarkdownToHtml 相关存储状态类型
- 添加相关单元测试,覆盖转换逻辑及功能组件
- 在功能配置中注册 Markdown 转 HTML 工具,支持界面调用和路由访问

* feat(htmlToMarkdown): 添加 HTML 转 Markdown 功能页面及转换工具

- 新增 HtmlToMarkdownPage 组件,支持实时转换 HTML 为 Markdown
- 增加 htmlToMarkdown 工具函数,实现基于 DOM 解析的转换逻辑
- 新增下载 Markdown 文件功能,支持一键导出转换结果
- 配置文件增加 htmlToMarkdown 相关功能项和页面样式
- 国际化资源添加对应中英文文案
- 更新类型定义,新增 HTML 转 Markdown 相关类型和存储键
- 补充对应的单元测试,覆盖转换逻辑和导出功能
- 修正路由提供器测试,确保新增页面被正确识别和排序

* refactor(jwt): 替换 JSON 格式化函数名称及优化代码表达

- 将 jwt 解析工具中的 formatJson 函数重命名为 stringifyJson
- 更新 Jwt 页面中相关的格式化函数调用,保持命名一致
- 统一注释风格,增强代码可读性
- 保持功能不变,改进代码语义表达

* refactor(timestamp): 优化时间戳页面布局与样式,简化组件结构

- 调整模式切换器和单位切换器样式,增加统一转换工作卡片和转换按钮样式
- 精简LiveClock组件,移除单位切换功能,瘦身为单行参考条
- 优化Timestamp页面布局,输入区、单位选择、时区选择及结果区并入统一卡片中
- 移除ResultView中多余的额外时间格式展示,仅保留转换结果显示
- 精简useTimestampConverter,删除自动转换的防抖useEffect逻辑
- 更新多处样式细节以提升界面紧凑度和一致性
- 删除国际化文件中未使用的时间相关文案键值

* feat(layout): 优化二维码与时间戳页面响应式布局

- 新增桌面端左右分栏布局样式,实现 md 断点后等宽分栏二卡片等高
- 调整二维码页面,移动端折叠,桌面端两个面板强制展开且隐藏折叠交互
- 时间戳页面新增左右分栏样式,左侧为转换工作台,右侧为结果展示卡片
- 结果展示新增空状态占位提示,支持显示额外时间格式信息及复制按钮
- 国际化资源补充时间戳转换相关文本,包括相对时间、ISO8601、UTC时间等标签
- 统一各布局区容器高度及内联样式,强化桌面端用户体验与视觉一致性

* fix(style): 优化页面布局与文字溢出显示

- 为页面主题配置添加 gridAutoRows 属性,统一自动行高
- 修改 ToolCard 中描述文本样式,实现单行溢出省略号显示
- 解决文本换行和截断,避免内容超出指定范围
- 改善视觉效果和布局一致性,提高用户体验

* feat(options): 支持功能列表拖拽排序

- 新增 @dnd-kit 相关依赖包,实现拖拽排序功能
- 将原先的上下移动按钮替换为拖拽交互
- 封装 SortableFeatureRow 组件,支持拖拽排序与开关切换
- 使用 DndContext 和 SortableContext 管理拖拽逻辑与排序状态
- 拖拽时高亮显示拖动项,优化用户体验
- 调整恢复默认按钮位置,与 Tab 同行展示
- 优化 UI 样式及相关交互提示信息
- 保存排序结果至存储,保证配置持久化

* fix(router): 防止组件卸载后更新状态导致竞态条件

- 修改 loadInitialData 调用逻辑,添加 cancelled 标志防止卸载后更新状态
- 确保组件卸载时取消状态更新,避免 React 警告和无效操作
- 新增测试覆盖组件卸载时不应更新 isLoaded 状态的竞态条件防护
- 修复加载初始路由数据失败时的错误处理代码逻辑

* refactor(router): 移除不再使用的本地导航相关代码

- 删除 RouterProvider 中的 navigateLocal 和 syncNavigation 方法及相关注释
- 清理对应的类型定义,移除不再使用的接口成员
- 更新测试文件,移除对 navigateLocal 的模拟函数调用
- 精简导航逻辑,保持代码整洁和一致性

* refactor(clipboard): 优化剪贴板复制函数并添加测试用例

- 将复制文本和复制图片的函数改为 async/await 形式,简化代码结构
- 删除 Promise 构造函数中多余的 then/catch,改用 try/catch 返回布尔值表示成功与否
- 修改 CopyButton 组件使用新的异步复制函数写法,提升代码可读性
- 添加剪贴板复制功能的单元测试,覆盖成功与失败两种场景
- 在测试环境中模拟 ClipboardItem 以支持图片复制测试

* fix(options): 验证配置数据有效性并恢复默认值

- 添加 isValidPage 和 isValidPageList 函数验证 visiblePages 和 pageOrder 的数据有效性
- 当读取到非法数据时,自动恢复为默认配置,避免异常状态
- 更新异步加载配置逻辑,确保界面状态稳定
- 新增单元测试,覆盖合法与非法数据的加载场景
- 修正 .gitignore 文件

* refactor(storage): 替换 IndexedDB 大小计算为通用存储估算

- 将获取 IndexedDB 大小的方法更名为获取源存储估算
- 修改调用处使用新的通用存储估算函数
- 更新错误日志信息以反映新的统计方法
- 统一存储空间估算接口,提升扩展能力

* fix(utils): 解决打印重复触发问题

- 添加 printed 标志防止多次执行打印
- 在 onload 和 setTimeout 中判断 printed 状态
- 防止部分浏览器 onload 不触发导致重复打印
- 增强打印逻辑的稳定性和兼容性

* fix(ErrorBoundary): 修复错误状态未随子组件变化重置的问题

- 增加 componentDidUpdate 钩子,子组件变化时重置错误状态
- 添加完整的单元测试覆盖正常渲染、错误捕获、状态重置和刷新按钮功能
- 修改 background.ts,调用 reload 时添加异常捕获并打印错误
- 修改 sidepanel/App.tsx,调用 openOptionsPage 时添加异常捕获并打印错误
- tsconfig.json 开启 noImplicitAny 检查,提高代码类型安全
- 添加 qrious 模块类型声明文件,完善类型定义

* fix(utils): 修复存储状态和主题模式加载的竞态条件问题

- 在 ThemeModeProvider 中取消异步操作防止内存泄漏
- 在 useStorageState 钩子中添加取消标志,避免状态更新竞态
- 修正 TopBar 搜索历史持久化,添加 useEffect 副作用同步存储
- 扩展 storageCleaner 支持 TB 单位,防止格式化错误
- GlobalSnackbar 组件支持自定义 anchorOrigin 属性,增强弹窗位置灵活性
- 优化 jsonToToml 和 jsonToYaml 中多余的字符判断逻辑,提升序列化准确性

* fix(Base64Converter): 修复切换模式时文件状态未及时重置问题

- 添加取消标志避免文件选择异步状态错乱
- 切换页面模式时清空文件和加载状态
- 切换文本编码方向时清空输出和错误状态
- 阻止重复选择同一模式导致不必要状态更新
- 确保异步转换过程中若取消选择不更新状态

* refactor(Base64Converter): 拆分文本、文件和图像转换模式为独立组件

- 将文本编码/解码逻辑提取至 TextMode 组件,简化主页面代码
- 将文件转Base64相关处理封装到 FileMode 组件,提升代码模块化
- 将图像转Base64相关处理封装到 ImageMode 组件,增强功能分离
- 主页面根据当前模式动态渲染对应组件,移除大量重复状态及逻辑
- 优化模式切换逻辑,取消不必要的状态重置,简化事件处理
- 清理未使用的导入和废弃代码,减少页面体积与复杂度

* test(toolcard): 调整键盘触发事件模拟

- 替换点击事件为键盘按下和释放事件模拟
- 修正测试用例以更真实地模拟键盘交互
- 确保事件处理函数被正确调用
- 移除eslint配置文件中对测试文件的忽略设置

* test(ToolCard): 优化键盘事件测试逻辑

- 引入 userEvent 以模拟更真实的用户键盘输入
- 使用 act 包裹异步操作确保测试稳定性
- 修改按 Enter 键测试为异步函数以支持 await
- 替换 fireEvent 键盘事件为 userEvent.keyboard 模拟Enter键
- 在触发键盘事件前手动聚焦目标按钮,确保事件正确触发

* docs: 添加页面级 ErrorBoundary 设计文档

为懒加载页面组件添加独立 ErrorBoundary 保护的设计方案。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(PageErrorBoundary): 添加页面级错误边界组件

轻量内嵌错误卡片,支持 resetKey 自动重置和重试按钮。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(PageErrorBoundary): 添加页面级错误边界单元测试

覆盖正常渲染、错误捕获、重试恢复、resetKey 自动重置等场景。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(RouterContainer): 在 Suspense 内层添加 PageErrorBoundary

以 currentPage 为 resetKey,页面切换时自动重置错误状态。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(options): 使用 PageErrorBoundary 替换全局 ErrorBoundary

options 页面统一使用页面级错误卡片,与 RouterContainer 体验一致。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(utils): add unified formatBytes function

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(utils): add formatBytes unit tests

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(utils): delegate formatByteSize to formatBytes

- Add unified formatBytes to utils/format.ts
- Update textStatistics.ts to re-export
- Update test expectations for consistent decimal formatting

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(utils): delegate formatFileSize and formatSize to formatBytes

- Update base64Converter.ts to re-export formatBytes as formatFileSize
- Update storageCleaner.ts to re-export formatBytes as formatSize

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(base64Converter): 支持 data URI 解码并优化错误提示

- base64ToText 支持自动剥离 data:<mime>;base64, 前缀进行正确解码
- 解码非 UTF-8 数据(如图片)时抛出友好错误,提示切换至图像模式
- TextMode 组件增加基于输入内容智能提示,解码图片类型时显示切换图像模式按钮
- 添加切换图像模式的回调支持及对应 UI 提示
- 增加对应单元测试覆盖新增功能和错误处理
- 更新中英文国际化文案,增加新的错误和提示信息
- eslint 配置调整,允许测试文件使用 any 类型警告关闭

* feat(base64Converter): 添加文件与图像模式的Base64解码及下载功能

- 增加文件模式的Base64解码功能,支持手动输入Base64内容进行解码
- 添加解码后的文件名编辑与下载按钮,提升用户操作便捷性
- 实现文件模式中编码与解码的切换按钮,并保留用户选择状态
- 图像模式同步新增解码功能,支持Base64字符串或数据URI的转换
- 图像解码结果显示预览图像、推断MIME类型、解码大小信息
- 增设统一错误处理机制,提示无效Base64字符串错误
- 更新i18n翻译词条,新增解码相关UI文字描述
- 扩展单元测试覆盖编码/解码切换及解码异常处理场景

* feat(eslint): 在测试文件规则中添加对未使用变量的警告配置

* fix(storageCleaner): 更新错误消息以反映清理操作的上下文

* feat(RouterProvider): 使用 ref 持有最新 currentPage,优化 chrome.storage 监听器注册

* fix(StorageSchema): 更新 RouterProvider 相关路由键的注释以反映默认行为

* feat(base64Converter): 添加对 BMP 和 WebP 文件类型的支持,更新魔数签名匹配规则

* fix(LiveClock): 修复定时器变量命名以提高代码可读性

* fix(storageCleaner): 使用 TextEncoder 准确计算 UTF-8 字节数

将 getCookieSize、getLocalStorageSize 和 getSessionStorageSize
中的字符串 .length 替换为 TextEncoder.encode().length,
避免 Unicode 字符导致的大小低估问题。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor: 移除不必要的样式定义,简化模式切换按钮的样式

* feat(SwitchButtonGroup): 添加 SwitchButtonGroup 组件及其测试用例,替换现有的 ToggleButtonGroup

* feat(SwitchButtonGroup): 添加 size 和 buttonSx 属性支持,并更新测试用例

* feat(MarkdownToHtml): 替换 ToggleButtonGroup 为 SwitchButtonGroup,简化模式切换逻辑

* feat: 替换 ToggleButtonGroup 为 SwitchButtonGroup,简化模式切换逻辑

* chore: 移除不必要的 compile 脚本

* feat: 更新 SwitchButtonGroup 组件,支持 number 类型和自定义样式,替换多个 ToggleButtonGroup 实现

* feat(Timestamp): 更新单位和时区选择样式,支持自适应宽度和换行

* feat: 更新比较按钮样式,添加 nowrap 属性以防止换行

* style(HTML转Markdown页): 替换旧版InputProps为新版slotProps

将Material UI组件的InputProps属性更新为slotProps语法,适配最新组件API

* feat: 添加 TextInputArea 多行文本输入组件及配套测试

新增功能完整的 TextInputArea 组件,支持受控/非受控模式、字符计数、复制清空、表单验证、自定义操作按钮等特性,同时编写了全面的单元测试覆盖所有核心功能。

* refactor(TopBar,JWT): 移动常量定义并重构JWT页面

1. 将TopBar的常量从pageTheme.ts移至组件内
2. 移除pageTheme.ts中废弃的topBarStyles定义
3. 重构JWT页面:替换TextField为TextInputArea组件
4. 简化JWT页面的输入处理逻辑

* feat(TextInputArea): 添加 sx 样式透传支持

新增 TextInputArea 组件的 sx 属性,支持透传样式到外层容器,新增对应测试用例验证样式生效

* refactor(TextStatistics): 抽离文本输入区域为公共组件

将原页面内的TextField文本输入框抽离为TextInputArea公共组件,简化当前页面代码,复用组件样式与逻辑

* feat(TextInputArea): 添加外部错误属性,支持父组件控制错误展示

- 新增externalError属性,优先级高于内部验证错误
- 新增配套测试用例验证外部错误的展示和优先级逻辑

* feat(textInputArea): 实现国际化适配并完善组件功能

1. 为文本输入区域组件添加多语言支持,新增中英文字段
2. 重构清空和复制按钮,使用国际化文案替代硬编码
3. 新增onClear回调属性,新增对应测试用例
4. 调整底部操作栏布局,合并操作按钮显示逻辑
5. 优化字符计数提示文案为国际化形式

* refactor(utils/jwt): 替换硬编码错误提示为国际化文案

1. 新增中英文多语言错误提示文案
2. 将decodeBase64Url和parseJwt中的硬编码错误信息替换为i18n翻译内容
3. 更新JWT格式错误的校验提示文案
4. 调整测试用例适配新的错误信息校验逻辑

* feat(components): 添加 DecodeResultPaper 通用结果展示组件及测试用例

提取 FileMode 和 ImageMode 共用的解码结果展示Paper结构,包含标题、预览区、文件信息、文件名输入和下载按钮,同时补充完整的单元测试覆盖渲染和交互逻辑

* refactor(Base64Converter): 重构页面组件,统一使用TextInputArea组件

1.  替换原有TextField为公共TextInputArea组件,减少重复代码
2.  抽离工具栏操作逻辑,使用useMemo缓存动作配置
3.  统一错误提示和输出区域的渲染逻辑,复用DecodeResultPaper组件
4.  更新测试用例,适配新的元素选择方式

* refactor(JsonTools): 替换原生TextField为复用的TextInputArea组件

统一JSON工具页面的输入框组件,移除冗余的样式和错误提示代码,简化页面代码结构

* refactor(Jwt页面): 优化错误提示实现方式

将手动实现的错误提示组件替换为通过externalError属性传递的方式,移除未使用的ErrorOutlineIcon导入,简化代码结构

* ci(github workflow): 修正CI脚本中的类型检查命令

将原本执行编译的npm run compile替换为正确的类型检查命令npm run typecheck
2026-05-17 18:42:01 +08:00
LingandRX a6dcdddf66 Develop (#22)
* docs: 添加组件文档注释和类型导入

refactor: 统一使用 SnackbarOptions 类型
style: 优化导入语句顺序和格式

* refactor: 简化假数据生成器中的faker导入和使用

Co-authored-by: Copilot <copilot@github.com>

* feat(form-recognizer): 增强表单识别功能并优化UI交互

- 新增字段类型偏好设置功能,支持按域名保存字段类型
- 重构FieldList组件,改进字段选择和类型修改体验
- 添加字段定位闪烁功能,便于在页面上快速找到对应字段
- 优化表单填充逻辑,支持单个字段覆盖默认填充模式
- 移除独立的侧边栏页面,统一使用主页面组件
- 改进useStorageState钩子,增加加载状态管理和防抖处理

* refactor: 移除未使用的组件文件

* refactor(页面头部): 提取通用 PageHeader 组件并替换各页面头部实现

重构各页面头部为统一的 PageHeader 组件,提高代码复用性和维护性

* style(组件): 调整自动刷新开关和存储选项网格的样式

优化自动刷新开关的文本内边距,重构存储选项网格的布局结构,调整间距和边框样式

* style(ui): 调整时间戳页面和结果视图的样式

- 为时区选择器添加圆角
- 优化结果视图的布局和对齐方式
- 调整结果项的内边距和文本样式

* ci(workflow): 移除Firefox测试以简化CI流程

仅保留Chrome浏览器的构建步骤,减少CI运行时间和资源消耗

* refactor(qrcode): 重构二维码解析功能并提取为独立模块

* style(页面样式): 统一页面头部图标颜色并优化表单样式

更新各页面头部组件的图标颜色配置,移除冗余的表单标签属性,优化输入框样式和按钮悬停效果

* feat(formMapping): 添加表单映射功能

- 创建 FormMappingPage 页面组件
- 实现表单扫描器 scanner.ts
- 实现高亮器 highlighter.ts
- 更新路由配置 routes.ts
- 更新内容脚本 content.ts
- 更新类型定义 storage.d.ts
- 添加 formMappingPageStyles 样式配置

* feat(表单映射): 添加配置导出功能及状态提示

添加配置导出为JSON文件的功能,包含导出按钮和错误处理。新增Snackbar组件用于显示导出成功和错误状态。优化页面布局结构,将状态提示移至全局容器外。

* feat(表单填充): 新增智能表单填充功能

添加智能表单填充功能,包括:
1. 新增表单填充页面和路由配置
2. 实现模糊匹配引擎和智能注入引擎
3. 添加Mock数据生成器和视觉反馈渲染器
4. 扩展消息接口支持填充操作

* ci: 精简触发CI的工作流分支

移除对develop分支及其变体的触发,仅保留main分支的触发

* test: 更新路由测试以匹配新增的路由数量

* test: 将测试文件中的描述和断言翻译为中文

* test(PageHeader): 添加组件测试用例验证渲染逻辑和样式

* refactor(theme): 重构主题样式并优化仪表盘卡片组件

将页面样式配置集中管理,移除各页面中硬编码的背景色
新增 DashboardCard 组件封装通用卡片逻辑
添加 dashboardCards 配置文件统一管理卡片数据

* refactor(Button): 统一按钮样式并移除重复样式定义

将按钮样式统一封装到 Button 组件中,移除各页面重复的样式定义
更新组件文档说明,提供更清晰的用法示例

* docs: 更新 README 文件中的目录结构说明

添加新组件和配置文件的说明,保持文档与代码同步

* refactor: 优化代码类型声明和UI布局

修复类型声明从any改为never以提高类型安全性
调整TimestampPage页面布局间距
移除DashboardPage中不必要的实时时钟状态
更新ESLint配置以使用推荐配置
添加选项页面打开的错误处理

* refactor(消息通信): 使用 @webext-core/messaging 重构消息处理逻辑

将原有的 chrome.runtime.onMessage 和 chrome.tabs.sendMessage 替换为类型安全的 @webext-core/messaging 实现
添加 ProtocolMap 类型定义确保消息类型安全
更新相关页面和后台脚本使用新的消息通信方式

* fix: 修复依赖项缺失导致的潜在问题

修复 useStorageState 中缺少 defaultValue 依赖的问题
重构 RouterProvider 的初始化逻辑,使用 useCallback 优化性能

* refactor: 重构表单识别和消息处理逻辑

将表单映射UI逻辑提取到独立文件
将消息处理器提取到独立文件
将表单识别页面逻辑提取到自定义hook
优化代码结构和可维护性

* feat(侧边栏): 添加侧边栏状态变化通知功能

- 在消息类型中新增侧边栏状态变化枚举和字段
- 侧边栏打开和关闭时发送状态通知
- 替换轮询检查方式为消息监听机制

* refactor: 优化高亮组件渲染逻辑并添加防抖处理

重构高亮组件渲染逻辑,使用 requestAnimationFrame 进行节流优化
在 useStorageCleaner 中添加防抖处理以避免频繁加载

* refactor: 统一组件导出方式为默认导出

* refactor: 重构样式系统并迁移至 MUI 主题

删除冗余的 CSS 文件,统一使用 MUI 主题管理样式
新增 useActiveTabDomain 和 useSidePanelState 自定义 Hook
优化各入口点的主题集成和布局处理

* docs: 更新项目文档以反映新增功能和技术栈细节

更新 AGENTS.md 文档,详细描述新增的功能模块(存储管理、URL 管理、二维码生成、表单工具套件等)和更新的技术栈信息

* feat: 添加错误边界组件以捕获子组件错误

在 popup、sidepanel 和 options 入口点添加 ErrorBoundary 组件,用于捕获并处理子组件中的 JavaScript 错误。当错误发生时,显示友好的错误界面并提供刷新功能。

* refactor(ui): 使用全局 snackbar 替换本地通知组件

重构表单页面中的通知系统,移除本地 Snackbar 和 Alert 组件,改用全局 GlobalSnackbar 组件统一管理通知
标准化主题配置中的 CSS 属性命名,移除带前缀的样式属性

* refactor(snackbar): 重构全局消息提示为 SnackbarProvider 组件

将原本分散在各页面的 GlobalSnackbar 组件重构为集中管理的 SnackbarProvider,通过 Context 提供统一的消息提示功能。主要变更包括:

1. 创建新的 SnackbarProvider 组件作为全局消息提示容器
2. 提供 useSnackbar hook 供子组件调用
3. 移除各页面中独立的 GlobalSnackbar 实例
4. 在 App 根组件中统一集成 SnackbarProvider
5. 优化消息提示样式和交互行为

* refactor(Snackbar): 重构消息提示组件并优化样式

重构 Snackbar 组件,将状态管理逻辑提取到 GlobalSnackbar 中复用
优化消息提示样式,调整阴影效果和最小尺寸
统一各页面使用 Snackbar 的方式,移除冗余配置

* refactor: 重构路由和功能配置系统

- 将路由配置和仪表盘卡片配置合并为统一的 features 配置
- 重构 RouterProvider 增加数据校验和本地快照功能
- 优化表单字段识别和填充逻辑,提取公共方法
- 更新相关组件和文档以适配新的配置系统
- 删除废弃的 dashboardCards 和 routes 配置文件

* refactor(GlobalSnackbar): 合并SnackbarProvider到GlobalSnackbar组件

重构Snackbar相关代码,将SnackbarProvider合并到GlobalSnackbar组件中,简化代码结构
更新相关组件引用路径,从SnackbarProvider改为GlobalSnackbar
修复StorageCleanerPage禁用状态逻辑问题

* feat(表单映射): 增强高亮器功能并优化元素定位

- 新增高亮器闪烁功能,支持自定义持续时间
- 改进元素定位算法,支持 Shadow DOM 穿透
- 优化拾取模式,支持连续拾取和悬停预览
- 增强错误处理和信息反馈
- 添加测试用例确保核心功能稳定性

* refactor: 移除表单映射相关功能

删除表单映射相关的UI、逻辑和测试代码,包括表单识别、智能填充和高亮功能
简化消息处理逻辑和配置文件,保留核心功能

* feat: 实现页面组件懒加载并优化加载状态显示

refactor: 移除未使用的数据生成和验证工具类

chore: 更新应用图标文件

* refactor(qrcode): 替换二维码生成和解析库以减小体积

使用 qr-scanner 替代 jsqr 进行二维码解析,使用 qrious 替代 qrcode 进行二维码生成
新的库体积更小且性能更好,同时移除了不再使用的 @faker-js/faker 依赖

* feat: 添加在标签页打开功能并优化错误处理

修复测试中的加载状态检查
将控制台日志改为错误捕获
添加全局消息提示组件

* feat: 支持独立标签页模式并重构路由配置

重构路由系统以支持独立标签页模式,替换原有的独立窗口模式
- 新增标签页路由、可见页面列表和页面顺序的存储键
- 修改 RouterProvider 以支持不同窗口类型的独立配置
- 更新选项页面以支持为不同窗口类型单独配置
- 添加相关单元测试

* feat(ui): 优化响应式布局和样式设计

重构选项页面布局,增强移动端适配性
更新主题配置和组件样式,提升用户体验

* refactor: 移除URL工具相关代码

移除不再需要的URL工具功能,包括组件、页面、样式和类型定义

* refactor: 简化页面标题和存储选项网格的响应式布局

* refactor: 优化样式和错误处理

- 简化 chrome.runtime.openOptionsPage 的错误处理
- 拆分 html, body, #root 的样式定义,使用 minHeight 替代 height

* refactor: 重构页面组件结构
- 将页面组件从 entrypoints/popup/pages 移动到 pages 目录
- 更新文档中的路径引用

* style(DashboardPage): 优化仪表盘布局为响应式网格

refactor(ToolCard): 重构工具卡片组件并添加详细注释

* feat: 新增文本统计功能

添加文本统计工具页面及相关功能,包括:
- 文本统计工具页面组件
- 文本统计计算工具函数
- 相关类型定义和样式配置
- 单元测试覆盖核心功能

* feat(jwt): 添加 JWT 解析工具功能

新增 JWT 解析工具页面,包含以下内容:
- 添加 JWT 类型定义和页面样式配置
- 实现 JWT 解析核心功能及测试用例
- 创建 JWT 解析页面组件
- 在功能配置中注册 JWT 工具

* docs: 更新 README 以更清晰地描述项目功能和结构

- 重构功能描述部分,使用更简洁的列表形式展示
- 更新技术栈和项目结构说明
- 优化开发流程部分的描述
- 移除冗余的权限和依赖说明

* docs: 更新AGENTS.md文档以反映项目最新状态

- 重写项目概述和核心命令部分,使其更简洁准确
- 重构目录结构描述,采用更清晰的树状格式
- 移除过时的功能模块描述,聚焦当前核心功能
- 更新AI代理开发准则和权限管理说明
- 优化文档整体结构和可读性

* test: 更新特征计数测试以匹配新增 textStatistics 和 jwt 工具

* style: 统一导入语句顺序

* refactor(pages): 重构工具页面组件结构并优化功能实现

- 将各工具页面组件迁移至独立目录并重构为模块化结构
- 新增存储清理工具相关组件及状态管理逻辑
- 实现时间戳转换工具的自定义hook和UI组件
- 优化JWT解析工具的代码结构和样式
- 更新文本统计工具的性能计算逻辑
- 调整配置文件中的页面引用路径
- 删除无用文件和过时文档

* feat(顶部栏): 添加搜索功能和历史记录

实现顶部栏搜索框功能,支持模糊搜索工具并显示搜索结果列表
添加搜索历史记录功能,自动保存最近搜索并显示
支持键盘导航和选择搜索结果

* feat: 添加多语言支持(i18n)

实现应用的多语言功能,支持中英文切换
添加i18next和相关依赖
创建语言资源文件
更新组件以使用翻译功能
重构文本统计工具页面
完善测试和构建配置

* feat(顶部栏): 添加语言切换功能并优化国际化支持
feat(i18n): 增加语言归一化和同步功能
fix(剪贴板): 使用国际化文本替换剪贴板消息
test(顶部栏): 更新测试以匹配国际化标题

* refactor: 重构组件目录结构,将组件移动到对应功能页面目录

将多个组件从通用组件目录移动到各自功能页面目录下,包括 ToolCard、StorageCleanerConfirm、UrlToQrCodeSection 和 QrCodeToUrlSection。同时新增 Dashboard 页面相关组件 DashboardCard 和 ToolCard,并优化了组件导入路径。

重构旨在提高代码组织性,使组件与其所属功能页面更紧密关联,便于维护和理解代码结构。所有组件功能保持不变,仅调整文件位置和导入路径。

* refactor(Dashboard): 优化卡片组件结构并移除无用属性

重构 DashboardCard 组件接口,直接传递属性而非配置对象
移除 ToolCard 中无用的 hasAI 属性
使用 useMemo 优化可见性判断性能
统一使用 alpha 函数处理颜色透明度

* refactor(timestamp): 合并输入处理函数并优化样式结构

重构时间戳转换器的输入处理逻辑,将 setTsInput 和 setDtInput 合并为统一的 setInput 函数
将组件样式提取到 pageTheme 配置中统一管理
使用 ToggleButtonGroup 替换自定义模式切换器实现
移除不必要的 showMessage 属性传递,改用 useSnackbar 钩子

* refactor(dashboard): 重构仪表盘卡片组件并优化布局

- 删除冗余的 DashboardCard 组件,直接使用 ToolCard
- 将网格布局样式移至主题配置
- 使用 CardActionArea 增强可访问性
- 简化点击事件处理逻辑
- 更新相关测试用例

* refactor(storageCleaner): 重构样式代码并优化国际化处理

将分散的样式代码集中到pageTheme.ts中统一管理
优化组件中的国际化文本引用
修复useStorageCleaner中的状态同步问题
更新测试用例以匹配国际化文本

* refactor(StorageCleaner): 优化条件判断和清理未使用的代码

简化禁用按钮的条件判断逻辑
移除未使用的 resultTimeoutRef 及相关清理代码

* refactor(QrCode): 重构二维码页面样式和组件结构

将内联样式提取到主题配置文件中
移除未使用的全局消息提示依赖
优化组件结构和代码复用
删除未使用的QrCodeUploader组件

* refactor(ErrorBoundary): 移除冗余状态更新并调整错误背景色

移除 componentDidCatch 中的冗余 setState 调用
将错误背景色从 'error.shortest' 改为 rgba 格式

* feat(TopBar): 优化顶部导航栏功能并添加样式配置

- 新增 openExtensionPage 工具函数用于打开扩展页面
- 将顶部导航栏样式配置提取到 pageTheme 中统一管理
- 改进搜索历史功能,增加错误处理和配置化限制
- 优化键盘导航和搜索结果的交互逻辑

* chore: 在.gitignore中添加docs目录

避免将文档生成目录提交到版本控制

* feat(JsonDiff): add type definitions for diff engine

* feat: 新增 JSON 差异比较工具功能

添加 JSON 差异比较工具页面,支持并排和统一视图模式,包含以下功能:
- 左右两侧 JSON 输入框及校验
- 差异高亮显示及导航
- 支持对象和数组的深度比较
- 国际化支持
- 单元测试覆盖核心差异算法

* feat(主题): 添加暗色模式支持并重构主题系统

- 新增 ThemeModeProvider 提供亮色/暗色/系统模式切换功能
- 重构所有组件使用 MUI 主题变量替代硬编码颜色
- 更新测试用例以适配新的主题系统
- 添加 i18n 翻译支持主题切换相关文本
- 统一入口点使用 AppRoot 组件封装主题和路由提供者

* feat(jsonDiff): 新增 JSON 格式化功能模块

- 新增 jsonFormat 语言资源,支持中英文文本
- 新增 JsonFormatSection 组件,提供 JSON 格式化输入与结果展示
- 在 JsonDiff 页面加入模式切换,支持差异比较与格式化双模式
- 实现 formatJson 和 validateJson 工具方法,支持缩进大小和键名排序选项
- 增加格式化结果显示大小统计和一键复制功能
- 写入大量单元测试,覆盖格式化和校验功能边界
- 优化页面布局与交互,提升用户体验和功能集成度

* feat(jsonDiff): 新增 JSON 转 YAML/TOML 及压缩功能

- 在语言文件(en/zh)中添加 YAML、TOML 和 JSON 压缩模式相关文案
- 扩展 JsonDiff 页面,新增 yaml、toml、minify 三种页面模式
- 新增 JsonConvertSection 组件,提供输入校验、转换操作及结果展示
- 实现 jsonToYaml 工具函数,将 JSON 转换为符合规范的 YAML 格式
- 实现 jsonToToml 工具函数,将 JSON 转换为符合规范且支持嵌套的 TOML 格式
- 实现 minifyJson 工具函数,实现 JSON 字符串压缩为单行格式
- 为所有新功能增加单元测试,覆盖正常及异常场景,保证转换正确性和稳定性
- 在页面中添加对应的切换按钮和图标,提升用户体验和操作便捷性

* refactor(jsonTools): 重命名 JSON 差异比较为 JSON 工具

- 将 JsonDiff 页面组件重命名为 JsonTools 并更新相关引用
- 修改功能配置中对应组件的名称以匹配新命名
- 更新英文和中文语言包中该功能的标题及描述
- 调整测试文件中导入路径以使用新的 JsonTools 文件夹名

* feat(storage): 优化本地存储状态管理和 JSON 工具页模式持久化

- 添加 useStorageState 钩子,支持同步快照和异步加载存储状态
- 实现快照机制,防止页面首屏闪烁现象
- 增加状态值验证器功能,保证恢复数据合法性
- JSON 工具页面模式改用受控存储状态持久化
- 新增 JsonToolsPageMode 类型定义,统一页面模式类型
- 为 useStorageState 编写完整单元测试,覆盖快照、校验及存储同步场景

* feat(base64Converter): 新增 Base64 转换器功能模块

- 添加 Base64 转换器页面及相关样式配置
- 支持文本、文件及图像三种转换模式,含编码和解码功能
- 实现拖拽、文件选择及图像预览功能
- 增加中文与英文多语言资源文件支持
- 补充相关工具函数,支持编码解码及格式校验
- 扩展路由提供者逻辑,合并已保存列表与默认功能集合
- 更新测试用例,覆盖合并新功能的场景验证
- 在特性配置中加入 Base64 转换器信息,支持图标和默认显示配置

* feat(markdown): 新增 Markdown 转 HTML 功能模块

- 添加 marked 依赖及类型定义支持 Markdown 解析
- 新增 MarkdownToHtml 页面,支持实时编辑、预览及 HTML 输出三种模式切换
- 实现 markdownToHtml 工具函数进行 Markdown 转 HTML 操作,含错误处理
- 包装完整 HTML 文档功能,支持打印和下载导出
- 配置全局主题样式,优化 Markdown 渲染视觉效果
- 完善 i18n 国际化资源,支持多语言页面展示
- 统一类型定义,新增 MarkdownToHtml 相关存储状态类型
- 添加相关单元测试,覆盖转换逻辑及功能组件
- 在功能配置中注册 Markdown 转 HTML 工具,支持界面调用和路由访问

* feat(htmlToMarkdown): 添加 HTML 转 Markdown 功能页面及转换工具

- 新增 HtmlToMarkdownPage 组件,支持实时转换 HTML 为 Markdown
- 增加 htmlToMarkdown 工具函数,实现基于 DOM 解析的转换逻辑
- 新增下载 Markdown 文件功能,支持一键导出转换结果
- 配置文件增加 htmlToMarkdown 相关功能项和页面样式
- 国际化资源添加对应中英文文案
- 更新类型定义,新增 HTML 转 Markdown 相关类型和存储键
- 补充对应的单元测试,覆盖转换逻辑和导出功能
- 修正路由提供器测试,确保新增页面被正确识别和排序

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: yll <mtmtchb@outlook.com>
2026-05-10 23:44:45 +08:00
LingandRX 3c9c9e740e Develop (#21)
docs: 添加组件文档注释和类型导入
refactor: 统一使用 SnackbarOptions 类型
style: 优化导入语句顺序和格式
refactor: 简化假数据生成器中的faker导入和使用
feat(form-recognizer): 增强表单识别功能并优化UI交互
refactor: 移除未使用的组件文件
refactor(页面头部): 提取通用 PageHeader 组件
style(组件): 调整自动刷新开关和存储选项网格的样式
style(ui): 调整时间戳页面和结果视图的样式
ci(workflow): 移除Firefox测试以简化CI流程
refactor(qrcode): 重构二维码解析功能并提取为独立模块
style(页面样式): 统一页面头部图标颜色并优化表单样式
feat(formMapping): 添加表单映射功能
feat(表单映射): 添加配置导出功能及状态提示
feat(表单填充): 新增智能表单填充功能
ci: 精简触发CI的工作流分支
test: 更新路由测试以匹配新增的路由数量
test: 将测试文件中的描述和断言翻译为中文
test(PageHeader): 添加组件测试用例
refactor(theme): 重构主题样式并优化仪表盘卡片组件
refactor(Button): 统一按钮样式并移除重复样式定义
docs: 更新 README 文件中的目录结构说明
refactor: 优化代码类型声明和UI布局
refactor(消息通信): 使用 @webext-core/messaging 重构消息处理逻辑
fix: 修复依赖项缺失导致的潜在问题
refactor: 重构表单识别和消息处理逻辑
feat(侧边栏): 添加侧边栏状态变化通知功能
refactor: 优化高亮组件渲染逻辑并添加防抖处理
refactor: 统一组件导出方式为默认导出
refactor: 重构样式系统并迁移至 MUI 主题
docs: 更新项目文档以反映新增功能和技术栈细节
feat: 添加错误边界组件以捕获子组件错误
refactor(ui): 使用全局 snackbar 替换本地通知组件
refactor(snackbar): 重构全局消息提示为 SnackbarProvider 组件
refactor(Snackbar): 重构消息提示组件并优化样式
refactor: 重构路由和功能配置系统
refactor(GlobalSnackbar): 合并SnackbarProvider到GlobalSnackbar组件
feat(表单映射): 增强高亮器功能并优化元素定位
refactor: 移除表单映射相关功能
feat: 实现页面组件懒加载并优化加载状态显示
refactor: 移除未使用的数据生成和验证工具类
chore: 更新应用图标文件
refactor(qrcode): 替换二维码生成和解析库以减小体积
feat: 添加在标签页打开功能并优化错误处理
feat: 支持独立标签页模式并重构路由配置
feat(ui): 优化响应式布局和样式设计
refactor: 移除URL工具相关代码
refactor: 简化页面标题和存储选项网格的响应式布局
refactor: 优化样式和错误处理
refactor: 重构页面组件结构
style(DashboardPage): 优化仪表盘布局为响应式网格
refactor(ToolCard): 重构工具卡片组件并添加详细注释
feat: 新增文本统计功能
feat(jwt): 添加 JWT 解析工具功能
docs: 更新 README 以更清晰地描述项目功能和结构
docs: 更新AGENTS.md文档以反映项目最新状态
test: 更新特征计数测试以匹配新增工具
style: 统一导入语句顺序
refactor(pages): 重构工具页面组件结构并优化功能实现
feat(顶部栏): 添加搜索功能和历史记录
feat: 添加多语言支持(i18n)
feat(顶部栏): 添加语言切换功能并优化国际化支持
feat(i18n): 增加语言归一化和同步功能
fix(剪贴板): 使用国际化文本替换剪贴板消息
test(顶部栏): 更新测试以匹配国际化标题
refactor: 重构组件目录结构
refactor(Dashboard): 优化卡片组件结构并移除无用属性
refactor(timestamp): 合并输入处理函数并优化样式结构
refactor(dashboard): 重构仪表盘卡片组件并优化布局
refactor(storageCleaner): 重构样式代码并优化国际化处理
refactor(StorageCleaner): 优化条件判断和清理未使用的代码
refactor(QrCode): 重构二维码页面样式和组件结构
refactor(ErrorBoundary): 移除冗余状态更新并调整错误背景色
feat(TopBar): 优化顶部导航栏功能并添加样式配置
chore: 在.gitignore中添加docs目录
feat(JsonDiff): 添加差异引擎类型定义
feat: 新增 JSON 差异比较工具功能
feat(主题): 添加暗色模式支持并重构主题系统
2026-05-09 00:18:29 +08:00
LingandRX ff964e3608 Develop (#20)
* docs: 添加组件文档注释和类型导入

refactor: 统一使用 SnackbarOptions 类型
style: 优化导入语句顺序和格式

* refactor: 简化假数据生成器中的faker导入和使用

Co-authored-by: Copilot <copilot@github.com>

* feat(form-recognizer): 增强表单识别功能并优化UI交互

- 新增字段类型偏好设置功能,支持按域名保存字段类型
- 重构FieldList组件,改进字段选择和类型修改体验
- 添加字段定位闪烁功能,便于在页面上快速找到对应字段
- 优化表单填充逻辑,支持单个字段覆盖默认填充模式
- 移除独立的侧边栏页面,统一使用主页面组件
- 改进useStorageState钩子,增加加载状态管理和防抖处理

* refactor: 移除未使用的组件文件

* refactor(页面头部): 提取通用 PageHeader 组件并替换各页面头部实现

重构各页面头部为统一的 PageHeader 组件,提高代码复用性和维护性

* style(组件): 调整自动刷新开关和存储选项网格的样式

优化自动刷新开关的文本内边距,重构存储选项网格的布局结构,调整间距和边框样式

* style(ui): 调整时间戳页面和结果视图的样式

- 为时区选择器添加圆角
- 优化结果视图的布局和对齐方式
- 调整结果项的内边距和文本样式

* ci(workflow): 移除Firefox测试以简化CI流程

仅保留Chrome浏览器的构建步骤,减少CI运行时间和资源消耗

* refactor(qrcode): 重构二维码解析功能并提取为独立模块

* style(页面样式): 统一页面头部图标颜色并优化表单样式

更新各页面头部组件的图标颜色配置,移除冗余的表单标签属性,优化输入框样式和按钮悬停效果

* feat(formMapping): 添加表单映射功能

- 创建 FormMappingPage 页面组件
- 实现表单扫描器 scanner.ts
- 实现高亮器 highlighter.ts
- 更新路由配置 routes.ts
- 更新内容脚本 content.ts
- 更新类型定义 storage.d.ts
- 添加 formMappingPageStyles 样式配置

* feat(表单映射): 添加配置导出功能及状态提示

添加配置导出为JSON文件的功能,包含导出按钮和错误处理。新增Snackbar组件用于显示导出成功和错误状态。优化页面布局结构,将状态提示移至全局容器外。

* feat(表单填充): 新增智能表单填充功能

添加智能表单填充功能,包括:
1. 新增表单填充页面和路由配置
2. 实现模糊匹配引擎和智能注入引擎
3. 添加Mock数据生成器和视觉反馈渲染器
4. 扩展消息接口支持填充操作

* ci: 精简触发CI的工作流分支

移除对develop分支及其变体的触发,仅保留main分支的触发

* test: 更新路由测试以匹配新增的路由数量

* test: 将测试文件中的描述和断言翻译为中文

* test(PageHeader): 添加组件测试用例验证渲染逻辑和样式

* refactor(theme): 重构主题样式并优化仪表盘卡片组件

将页面样式配置集中管理,移除各页面中硬编码的背景色
新增 DashboardCard 组件封装通用卡片逻辑
添加 dashboardCards 配置文件统一管理卡片数据

* refactor(Button): 统一按钮样式并移除重复样式定义

将按钮样式统一封装到 Button 组件中,移除各页面重复的样式定义
更新组件文档说明,提供更清晰的用法示例

* docs: 更新 README 文件中的目录结构说明

添加新组件和配置文件的说明,保持文档与代码同步

* refactor: 优化代码类型声明和UI布局

修复类型声明从any改为never以提高类型安全性
调整TimestampPage页面布局间距
移除DashboardPage中不必要的实时时钟状态
更新ESLint配置以使用推荐配置
添加选项页面打开的错误处理

* refactor(消息通信): 使用 @webext-core/messaging 重构消息处理逻辑

将原有的 chrome.runtime.onMessage 和 chrome.tabs.sendMessage 替换为类型安全的 @webext-core/messaging 实现
添加 ProtocolMap 类型定义确保消息类型安全
更新相关页面和后台脚本使用新的消息通信方式

* fix: 修复依赖项缺失导致的潜在问题

修复 useStorageState 中缺少 defaultValue 依赖的问题
重构 RouterProvider 的初始化逻辑,使用 useCallback 优化性能

* refactor: 重构表单识别和消息处理逻辑

将表单映射UI逻辑提取到独立文件
将消息处理器提取到独立文件
将表单识别页面逻辑提取到自定义hook
优化代码结构和可维护性

* feat(侧边栏): 添加侧边栏状态变化通知功能

- 在消息类型中新增侧边栏状态变化枚举和字段
- 侧边栏打开和关闭时发送状态通知
- 替换轮询检查方式为消息监听机制

* refactor: 优化高亮组件渲染逻辑并添加防抖处理

重构高亮组件渲染逻辑,使用 requestAnimationFrame 进行节流优化
在 useStorageCleaner 中添加防抖处理以避免频繁加载

* refactor: 统一组件导出方式为默认导出

* refactor: 重构样式系统并迁移至 MUI 主题

删除冗余的 CSS 文件,统一使用 MUI 主题管理样式
新增 useActiveTabDomain 和 useSidePanelState 自定义 Hook
优化各入口点的主题集成和布局处理

* docs: 更新项目文档以反映新增功能和技术栈细节

更新 AGENTS.md 文档,详细描述新增的功能模块(存储管理、URL 管理、二维码生成、表单工具套件等)和更新的技术栈信息

* feat: 添加错误边界组件以捕获子组件错误

在 popup、sidepanel 和 options 入口点添加 ErrorBoundary 组件,用于捕获并处理子组件中的 JavaScript 错误。当错误发生时,显示友好的错误界面并提供刷新功能。

* refactor(ui): 使用全局 snackbar 替换本地通知组件

重构表单页面中的通知系统,移除本地 Snackbar 和 Alert 组件,改用全局 GlobalSnackbar 组件统一管理通知
标准化主题配置中的 CSS 属性命名,移除带前缀的样式属性

* refactor(snackbar): 重构全局消息提示为 SnackbarProvider 组件

将原本分散在各页面的 GlobalSnackbar 组件重构为集中管理的 SnackbarProvider,通过 Context 提供统一的消息提示功能。主要变更包括:

1. 创建新的 SnackbarProvider 组件作为全局消息提示容器
2. 提供 useSnackbar hook 供子组件调用
3. 移除各页面中独立的 GlobalSnackbar 实例
4. 在 App 根组件中统一集成 SnackbarProvider
5. 优化消息提示样式和交互行为

* refactor(Snackbar): 重构消息提示组件并优化样式

重构 Snackbar 组件,将状态管理逻辑提取到 GlobalSnackbar 中复用
优化消息提示样式,调整阴影效果和最小尺寸
统一各页面使用 Snackbar 的方式,移除冗余配置

* refactor: 重构路由和功能配置系统

- 将路由配置和仪表盘卡片配置合并为统一的 features 配置
- 重构 RouterProvider 增加数据校验和本地快照功能
- 优化表单字段识别和填充逻辑,提取公共方法
- 更新相关组件和文档以适配新的配置系统
- 删除废弃的 dashboardCards 和 routes 配置文件

* refactor(GlobalSnackbar): 合并SnackbarProvider到GlobalSnackbar组件

重构Snackbar相关代码,将SnackbarProvider合并到GlobalSnackbar组件中,简化代码结构
更新相关组件引用路径,从SnackbarProvider改为GlobalSnackbar
修复StorageCleanerPage禁用状态逻辑问题

* feat(表单映射): 增强高亮器功能并优化元素定位

- 新增高亮器闪烁功能,支持自定义持续时间
- 改进元素定位算法,支持 Shadow DOM 穿透
- 优化拾取模式,支持连续拾取和悬停预览
- 增强错误处理和信息反馈
- 添加测试用例确保核心功能稳定性

* refactor: 移除表单映射相关功能

删除表单映射相关的UI、逻辑和测试代码,包括表单识别、智能填充和高亮功能
简化消息处理逻辑和配置文件,保留核心功能

* feat: 实现页面组件懒加载并优化加载状态显示

refactor: 移除未使用的数据生成和验证工具类

chore: 更新应用图标文件

* refactor(qrcode): 替换二维码生成和解析库以减小体积

使用 qr-scanner 替代 jsqr 进行二维码解析,使用 qrious 替代 qrcode 进行二维码生成
新的库体积更小且性能更好,同时移除了不再使用的 @faker-js/faker 依赖

* feat: 添加在标签页打开功能并优化错误处理

修复测试中的加载状态检查
将控制台日志改为错误捕获
添加全局消息提示组件

* feat: 支持独立标签页模式并重构路由配置

重构路由系统以支持独立标签页模式,替换原有的独立窗口模式
- 新增标签页路由、可见页面列表和页面顺序的存储键
- 修改 RouterProvider 以支持不同窗口类型的独立配置
- 更新选项页面以支持为不同窗口类型单独配置
- 添加相关单元测试

* feat(ui): 优化响应式布局和样式设计

重构选项页面布局,增强移动端适配性
更新主题配置和组件样式,提升用户体验

* refactor: 移除URL工具相关代码

移除不再需要的URL工具功能,包括组件、页面、样式和类型定义

* refactor: 简化页面标题和存储选项网格的响应式布局

* refactor: 优化样式和错误处理

- 简化 chrome.runtime.openOptionsPage 的错误处理
- 拆分 html, body, #root 的样式定义,使用 minHeight 替代 height

* refactor: 重构页面组件结构
- 将页面组件从 entrypoints/popup/pages 移动到 pages 目录
- 更新文档中的路径引用

* style(DashboardPage): 优化仪表盘布局为响应式网格

refactor(ToolCard): 重构工具卡片组件并添加详细注释

* feat: 新增文本统计功能

添加文本统计工具页面及相关功能,包括:
- 文本统计工具页面组件
- 文本统计计算工具函数
- 相关类型定义和样式配置
- 单元测试覆盖核心功能

* feat(jwt): 添加 JWT 解析工具功能

新增 JWT 解析工具页面,包含以下内容:
- 添加 JWT 类型定义和页面样式配置
- 实现 JWT 解析核心功能及测试用例
- 创建 JWT 解析页面组件
- 在功能配置中注册 JWT 工具

* docs: 更新 README 以更清晰地描述项目功能和结构

- 重构功能描述部分,使用更简洁的列表形式展示
- 更新技术栈和项目结构说明
- 优化开发流程部分的描述
- 移除冗余的权限和依赖说明

* docs: 更新AGENTS.md文档以反映项目最新状态

- 重写项目概述和核心命令部分,使其更简洁准确
- 重构目录结构描述,采用更清晰的树状格式
- 移除过时的功能模块描述,聚焦当前核心功能
- 更新AI代理开发准则和权限管理说明
- 优化文档整体结构和可读性

* test: 更新特征计数测试以匹配新增 textStatistics 和 jwt 工具

* style: 统一导入语句顺序

* refactor(pages): 重构工具页面组件结构并优化功能实现

- 将各工具页面组件迁移至独立目录并重构为模块化结构
- 新增存储清理工具相关组件及状态管理逻辑
- 实现时间戳转换工具的自定义hook和UI组件
- 优化JWT解析工具的代码结构和样式
- 更新文本统计工具的性能计算逻辑
- 调整配置文件中的页面引用路径
- 删除无用文件和过时文档

* feat(顶部栏): 添加搜索功能和历史记录

实现顶部栏搜索框功能,支持模糊搜索工具并显示搜索结果列表
添加搜索历史记录功能,自动保存最近搜索并显示
支持键盘导航和选择搜索结果

* feat: 添加多语言支持(i18n)

实现应用的多语言功能,支持中英文切换
添加i18next和相关依赖
创建语言资源文件
更新组件以使用翻译功能
重构文本统计工具页面
完善测试和构建配置

* feat(顶部栏): 添加语言切换功能并优化国际化支持
feat(i18n): 增加语言归一化和同步功能
fix(剪贴板): 使用国际化文本替换剪贴板消息
test(顶部栏): 更新测试以匹配国际化标题

* refactor: 重构组件目录结构,将组件移动到对应功能页面目录

将多个组件从通用组件目录移动到各自功能页面目录下,包括 ToolCard、StorageCleanerConfirm、UrlToQrCodeSection 和 QrCodeToUrlSection。同时新增 Dashboard 页面相关组件 DashboardCard 和 ToolCard,并优化了组件导入路径。

重构旨在提高代码组织性,使组件与其所属功能页面更紧密关联,便于维护和理解代码结构。所有组件功能保持不变,仅调整文件位置和导入路径。

* refactor(Dashboard): 优化卡片组件结构并移除无用属性

重构 DashboardCard 组件接口,直接传递属性而非配置对象
移除 ToolCard 中无用的 hasAI 属性
使用 useMemo 优化可见性判断性能
统一使用 alpha 函数处理颜色透明度

* refactor(timestamp): 合并输入处理函数并优化样式结构

重构时间戳转换器的输入处理逻辑,将 setTsInput 和 setDtInput 合并为统一的 setInput 函数
将组件样式提取到 pageTheme 配置中统一管理
使用 ToggleButtonGroup 替换自定义模式切换器实现
移除不必要的 showMessage 属性传递,改用 useSnackbar 钩子

* refactor(dashboard): 重构仪表盘卡片组件并优化布局

- 删除冗余的 DashboardCard 组件,直接使用 ToolCard
- 将网格布局样式移至主题配置
- 使用 CardActionArea 增强可访问性
- 简化点击事件处理逻辑
- 更新相关测试用例

* refactor(storageCleaner): 重构样式代码并优化国际化处理

将分散的样式代码集中到pageTheme.ts中统一管理
优化组件中的国际化文本引用
修复useStorageCleaner中的状态同步问题
更新测试用例以匹配国际化文本

* refactor(StorageCleaner): 优化条件判断和清理未使用的代码

简化禁用按钮的条件判断逻辑
移除未使用的 resultTimeoutRef 及相关清理代码

* refactor(QrCode): 重构二维码页面样式和组件结构

将内联样式提取到主题配置文件中
移除未使用的全局消息提示依赖
优化组件结构和代码复用
删除未使用的QrCodeUploader组件

* refactor(ErrorBoundary): 移除冗余状态更新并调整错误背景色

移除 componentDidCatch 中的冗余 setState 调用
将错误背景色从 'error.shortest' 改为 rgba 格式

* feat(TopBar): 优化顶部导航栏功能并添加样式配置

- 新增 openExtensionPage 工具函数用于打开扩展页面
- 将顶部导航栏样式配置提取到 pageTheme 中统一管理
- 改进搜索历史功能,增加错误处理和配置化限制
- 优化键盘导航和搜索结果的交互逻辑

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: yll <mtmtchb@outlook.com>
2026-05-06 23:11:27 +08:00
雨霖铃 9d1d8a362a fix(剪贴板): 改进剪贴板操作的错误处理
在复制文本和图片到剪贴板时,捕获并返回更详细的错误信息
同时更新GlobalSnackbar组件,优化类型导入和消息显示
2026-05-04 13:26:33 +08:00
雨霖铃 2a0a5658b5 refactor(clipboard): 重构剪贴板工具函数并优化相关组件
将 copyToClipboard 拆分为 copyTextToClipboard 和 copyImageToClipboard
移除 useClipboard hook 并将消息处理移至调用方
优化组件中的剪贴板操作错误处理逻辑
2026-05-03 20:43:40 +08:00
LingandRX 57ea4d9858 Enhance form recognition, optimize UI, and unify components (#19)
- **docs**: 完善组件注释、README 目录结构及 AGENTS.md 文档。
- **refactor**:
  - 提取通用 `PageHeader`、`Button`、`DashboardCard` 及 `ErrorBoundary` 组件。
  - 重构消息通信机制,采用 `@webext-core/messaging` 实现类型安全。
  - 将全局通知系统重构为 `SnackbarProvider` (后合并至 `GlobalSnackbar`)。
  - 迁移样式系统至 MUI 主题,移除冗余 CSS。
  - 优化路由配置,支持独立标签页模式及页面懒加载。
  - 移除未使用文件、URL 工具及表单映射相关功能。
- **feat**:
  - 新增配置导出功能(JSON)及状态提示。
  - 新增侧边栏状态变化通知机制。
  - 新增文本统计及 JWT 解析工具。
  - 优化二维码生成与解析逻辑,换用更轻量的 `qrious` 和 `qr-scanner`。
  - 增强高亮器功能,支持闪烁效果及 Shadow DOM 穿透。
- **style**: 优化仪表盘响应式网格布局及 UI 细节。
- **fix**: 修复 `useStorageState` 依赖缺失及路由初始化性能问题。
- **test**: 更新单元测试以覆盖新增的工具函数及功能特性。
2026-05-03 17:06:32 +08:00
LingandRX 8d36f21f1b Refactor and enhance form recognition, mapping, and UI components (#18)
* docs: 添加组件文档注释和类型导入

refactor: 统一使用 SnackbarOptions 类型
style: 优化导入语句顺序和格式

* refactor: 简化假数据生成器中的faker导入和使用

Co-authored-by: Copilot <copilot@github.com>

* feat(form-recognizer): 增强表单识别功能并优化UI交互

- 新增字段类型偏好设置功能,支持按域名保存字段类型
- 重构FieldList组件,改进字段选择和类型修改体验
- 添加字段定位闪烁功能,便于在页面上快速找到对应字段
- 优化表单填充逻辑,支持单个字段覆盖默认填充模式
- 移除独立的侧边栏页面,统一使用主页面组件
- 改进useStorageState钩子,增加加载状态管理和防抖处理

* refactor: 移除未使用的组件文件

* refactor(页面头部): 提取通用 PageHeader 组件并替换各页面头部实现

重构各页面头部为统一的 PageHeader 组件,提高代码复用性和维护性

* style(组件): 调整自动刷新开关和存储选项网格的样式

优化自动刷新开关的文本内边距,重构存储选项网格的布局结构,调整间距和边框样式

* style(ui): 调整时间戳页面和结果视图的样式

- 为时区选择器添加圆角
- 优化结果视图的布局和对齐方式
- 调整结果项的内边距和文本样式

* ci(workflow): 移除Firefox测试以简化CI流程

仅保留Chrome浏览器的构建步骤,减少CI运行时间和资源消耗

* refactor(qrcode): 重构二维码解析功能并提取为独立模块

* style(页面样式): 统一页面头部图标颜色并优化表单样式

更新各页面头部组件的图标颜色配置,移除冗余的表单标签属性,优化输入框样式和按钮悬停效果

* feat(formMapping): 添加表单映射功能

- 创建 FormMappingPage 页面组件
- 实现表单扫描器 scanner.ts
- 实现高亮器 highlighter.ts
- 更新路由配置 routes.ts
- 更新内容脚本 content.ts
- 更新类型定义 storage.d.ts
- 添加 formMappingPageStyles 样式配置

* feat(表单映射): 添加配置导出功能及状态提示

添加配置导出为JSON文件的功能,包含导出按钮和错误处理。新增Snackbar组件用于显示导出成功和错误状态。优化页面布局结构,将状态提示移至全局容器外。

* feat(表单填充): 新增智能表单填充功能

添加智能表单填充功能,包括:
1. 新增表单填充页面和路由配置
2. 实现模糊匹配引擎和智能注入引擎
3. 添加Mock数据生成器和视觉反馈渲染器
4. 扩展消息接口支持填充操作

* ci: 精简触发CI的工作流分支

移除对develop分支及其变体的触发,仅保留main分支的触发

* test: 更新路由测试以匹配新增的路由数量

* test: 将测试文件中的描述和断言翻译为中文

* test(PageHeader): 添加组件测试用例验证渲染逻辑和样式

* refactor(theme): 重构主题样式并优化仪表盘卡片组件

将页面样式配置集中管理,移除各页面中硬编码的背景色
新增 DashboardCard 组件封装通用卡片逻辑
添加 dashboardCards 配置文件统一管理卡片数据

* refactor(Button): 统一按钮样式并移除重复样式定义

将按钮样式统一封装到 Button 组件中,移除各页面重复的样式定义
更新组件文档说明,提供更清晰的用法示例

* docs: 更新 README 文件中的目录结构说明

添加新组件和配置文件的说明,保持文档与代码同步

* refactor: 优化代码类型声明和UI布局

修复类型声明从any改为never以提高类型安全性
调整TimestampPage页面布局间距
移除DashboardPage中不必要的实时时钟状态
更新ESLint配置以使用推荐配置
添加选项页面打开的错误处理

* refactor(消息通信): 使用 @webext-core/messaging 重构消息处理逻辑

将原有的 chrome.runtime.onMessage 和 chrome.tabs.sendMessage 替换为类型安全的 @webext-core/messaging 实现
添加 ProtocolMap 类型定义确保消息类型安全
更新相关页面和后台脚本使用新的消息通信方式

* fix: 修复依赖项缺失导致的潜在问题

修复 useStorageState 中缺少 defaultValue 依赖的问题
重构 RouterProvider 的初始化逻辑,使用 useCallback 优化性能

* refactor: 重构表单识别和消息处理逻辑

将表单映射UI逻辑提取到独立文件
将消息处理器提取到独立文件
将表单识别页面逻辑提取到自定义hook
优化代码结构和可维护性

* feat(侧边栏): 添加侧边栏状态变化通知功能

- 在消息类型中新增侧边栏状态变化枚举和字段
- 侧边栏打开和关闭时发送状态通知
- 替换轮询检查方式为消息监听机制

* refactor: 优化高亮组件渲染逻辑并添加防抖处理

重构高亮组件渲染逻辑,使用 requestAnimationFrame 进行节流优化
在 useStorageCleaner 中添加防抖处理以避免频繁加载

* refactor: 统一组件导出方式为默认导出

* refactor: 重构样式系统并迁移至 MUI 主题

删除冗余的 CSS 文件,统一使用 MUI 主题管理样式
新增 useActiveTabDomain 和 useSidePanelState 自定义 Hook
优化各入口点的主题集成和布局处理

* docs: 更新项目文档以反映新增功能和技术栈细节

更新 AGENTS.md 文档,详细描述新增的功能模块(存储管理、URL 管理、二维码生成、表单工具套件等)和更新的技术栈信息

* feat: 添加错误边界组件以捕获子组件错误

在 popup、sidepanel 和 options 入口点添加 ErrorBoundary 组件,用于捕获并处理子组件中的 JavaScript 错误。当错误发生时,显示友好的错误界面并提供刷新功能。

* refactor(ui): 使用全局 snackbar 替换本地通知组件

重构表单页面中的通知系统,移除本地 Snackbar 和 Alert 组件,改用全局 GlobalSnackbar 组件统一管理通知
标准化主题配置中的 CSS 属性命名,移除带前缀的样式属性

* refactor(snackbar): 重构全局消息提示为 SnackbarProvider 组件

将原本分散在各页面的 GlobalSnackbar 组件重构为集中管理的 SnackbarProvider,通过 Context 提供统一的消息提示功能。主要变更包括:

1. 创建新的 SnackbarProvider 组件作为全局消息提示容器
2. 提供 useSnackbar hook 供子组件调用
3. 移除各页面中独立的 GlobalSnackbar 实例
4. 在 App 根组件中统一集成 SnackbarProvider
5. 优化消息提示样式和交互行为

* refactor(Snackbar): 重构消息提示组件并优化样式

重构 Snackbar 组件,将状态管理逻辑提取到 GlobalSnackbar 中复用
优化消息提示样式,调整阴影效果和最小尺寸
统一各页面使用 Snackbar 的方式,移除冗余配置

* refactor: 重构路由和功能配置系统

- 将路由配置和仪表盘卡片配置合并为统一的 features 配置
- 重构 RouterProvider 增加数据校验和本地快照功能
- 优化表单字段识别和填充逻辑,提取公共方法
- 更新相关组件和文档以适配新的配置系统
- 删除废弃的 dashboardCards 和 routes 配置文件
2026-05-01 18:00:06 +08:00
LingandRX 60537f6e2e Develop (#17)
新功能 (Features)
智能表单引擎: 新增智能表单填充功能,内置模糊匹配引擎、Mock数据生成器与视觉反馈渲染器。

表单映射与导出: 实现表单映射页面(包含扫描器和高亮器),并支持将配置导出为 JSON 文件,附带 Snackbar 状态提示。

表单识别增强: 增加按域名保存字段类型偏好的功能;添加字段定位闪烁以辅助查找;优化填充逻辑(支持单字段覆盖默认模式);重构 FieldList 组件以提升操作体验。

♻️ 代码重构 (Refactor)
通用组件提取: 提取并统一应用通用的 PageHeader 组件,移除独立的侧边栏页面及未使用的组件文件。

状态与逻辑优化: 改进 useStorageState 钩子(增加加载状态管理与防抖处理);将二维码解析功能重构为独立模块。

类型与依赖简化: 统一使用 SnackbarOptions 类型;简化假数据生成器中 faker 的导入与使用逻辑。

💄 样式与界面 (Style)
UI 细节打磨: 统一各页面头部图标颜色,调整表单输入框与按钮交互样式;优化时间戳页面、结果视图布局(增加圆角、调整内边距/对齐方式);重构存储选项网格及自动刷新开关样式。

代码格式: 优化项目中导入语句的顺序与格式。

👷 持续集成 (CI)
流程提效: 移除 Firefox 测试步骤以减少资源消耗;收紧工作流触发条件,移除 develop 及其变体分支,仅保留 main 分支触发。

📝 文档 (Docs)
代码维护: 补充组件的文档注释与类型导入。
2026-04-30 19:56:32 +08:00
LingandRX c039475119 Develop (#15)
* docs: 添加组件文档注释和类型导入

refactor: 统一使用 SnackbarOptions 类型
style: 优化导入语句顺序和格式

* refactor: 简化假数据生成器中的faker导入和使用

Co-authored-by: Copilot <copilot@github.com>

* feat(form-recognizer): 增强表单识别功能并优化UI交互

- 新增字段类型偏好设置功能,支持按域名保存字段类型
- 重构FieldList组件,改进字段选择和类型修改体验
- 添加字段定位闪烁功能,便于在页面上快速找到对应字段
- 优化表单填充逻辑,支持单个字段覆盖默认填充模式
- 移除独立的侧边栏页面,统一使用主页面组件
- 改进useStorageState钩子,增加加载状态管理和防抖处理

* refactor: 移除未使用的组件文件

* refactor(页面头部): 提取通用 PageHeader 组件并替换各页面头部实现

重构各页面头部为统一的 PageHeader 组件,提高代码复用性和维护性

* style(组件): 调整自动刷新开关和存储选项网格的样式

优化自动刷新开关的文本内边距,重构存储选项网格的布局结构,调整间距和边框样式

* style(ui): 调整时间戳页面和结果视图的样式

- 为时区选择器添加圆角
- 优化结果视图的布局和对齐方式
- 调整结果项的内边距和文本样式

* ci(workflow): 移除Firefox测试以简化CI流程

仅保留Chrome浏览器的构建步骤,减少CI运行时间和资源消耗
2026-04-28 08:56:47 +08:00
LingandRX fb5f98dd3b Delete .workbuddy/memory directory (#14) 2026-04-23 22:11:15 +08:00
LingandRX af0089c980 Develop fill form (#13)
* feat(formRecognizer): 添加表单识别功能及相关组件

添加表单识别功能,包括以下内容:
1. 在路由配置中添加表单识别页面
2. 实现表单识别页面和侧边栏面板
3. 添加表单数据生成工具类
4. 实现与内容脚本的通信机制
5. 添加faker-js依赖用于生成测试数据
6. 支持不同入口点(popup/sidepanel)的组件渲染

* refactor(消息通信): 重构消息通信机制并集中管理消息协议

将分散的消息协议和通信逻辑集中到 utils/messages.ts 中
移除旧的 messages.tsx 文件并更新相关引用
添加消息动作枚举和类型定义,提高类型安全性
优化内容脚本注入失败时的处理逻辑

* feat(QR码): 添加粘贴图片功能并优化上传组件

添加全局粘贴事件监听,支持从剪贴板直接粘贴二维码图片进行解析。重构上传组件为独立组件QrCodeUploader,包含拖拽上传、预览、进度显示和错误处理功能。优化页面样式和用户体验。

- 在QrCodePage添加粘贴事件监听
- 创建QrCodeUploader组件整合上传功能
- 更新测试用例格式
- 调整多个页面的背景色样式

* feat(表单识别): 新增表单识别页面功能与模板管理

- 添加表单识别页面样式配置
- 实现表单字段扫描与展示功能
- 新增数据模板管理工具类
- 添加数据验证工具类
- 扩展表单识别页面功能,包括操作历史记录
- 支持模板的导入导出功能
- 优化表单填充操作的用户体验

* feat(消息系统): 添加标签页刷新功能

在消息系统中新增 RELOAD_TAB 动作类型和 tabId 字段,用于处理标签页刷新请求
修改 StorageCleanerPage 使用后台脚本发送刷新请求,确保弹窗关闭后仍能执行
在 background.ts 中添加标签页刷新处理逻辑,包括错误处理和响应返回

* refactor(theme): 重构主题颜色和样式配置

- 更新主题颜色以满足 WCAG AA 可访问性标准
- 提取全局样式配置到统一变量
- 使用语义化颜色变量替换硬编码值
- 为输入框样式创建统一配置

* feat: add URL entry management components and QR code generation feature

- Introduced `UrlEntryItem` and `UrlEntryList` components for displaying and managing URL entries.
- Added `UrlToQrCodeSection` component for generating QR codes from URLs with download and copy functionality.
- Implemented `AutoRefreshToggle`, `CleaningResult`, `DomainHeader`, `ErrorDisplay`, `OptionItem`, and `StorageOptionsGrid` components for enhanced user interface in storage cleaning.
- Created custom hooks `useStorageCleaner` and `useStorageState` for managing storage-related states and preferences.
- Added utility hook `useUrlPreferences` for handling URL entry preferences.

* feat: 新增时间戳转换器和相关组件,优化时间戳页面功能

* Refactor message handling and storage cleaning logic

* feat: 添加 GitHub Actions CI/CD 工作流,支持自动化构建与发布

* fix(background): 修复标签页刷新使用browser.tabs代替chrome.tabs
style(popup): 调整QR码页面样式顺序
refactor(storage): 重构存储清理逻辑并移除useSnackbar依赖
style(confirm): 优化存储清理确认对话框的UI样式
chore: 更新.gitignore忽略.trae目录

* chore: 移除 workbuddy 相关文件并更新 gitignore

* test: 更新 StorageCleanerConfirm 组件测试中的文本匹配
2026-04-23 22:10:09 +08:00
LingandRX 4850c92365 Develop fill form (#11)
* feat(formRecognizer): 添加表单识别功能及相关组件

添加表单识别功能,包括以下内容:
1. 在路由配置中添加表单识别页面
2. 实现表单识别页面和侧边栏面板
3. 添加表单数据生成工具类
4. 实现与内容脚本的通信机制
5. 添加faker-js依赖用于生成测试数据
6. 支持不同入口点(popup/sidepanel)的组件渲染

* refactor(消息通信): 重构消息通信机制并集中管理消息协议

将分散的消息协议和通信逻辑集中到 utils/messages.ts 中
移除旧的 messages.tsx 文件并更新相关引用
添加消息动作枚举和类型定义,提高类型安全性
优化内容脚本注入失败时的处理逻辑

* feat(QR码): 添加粘贴图片功能并优化上传组件

添加全局粘贴事件监听,支持从剪贴板直接粘贴二维码图片进行解析。重构上传组件为独立组件QrCodeUploader,包含拖拽上传、预览、进度显示和错误处理功能。优化页面样式和用户体验。

- 在QrCodePage添加粘贴事件监听
- 创建QrCodeUploader组件整合上传功能
- 更新测试用例格式
- 调整多个页面的背景色样式

* feat(表单识别): 新增表单识别页面功能与模板管理

- 添加表单识别页面样式配置
- 实现表单字段扫描与展示功能
- 新增数据模板管理工具类
- 添加数据验证工具类
- 扩展表单识别页面功能,包括操作历史记录
- 支持模板的导入导出功能
- 优化表单填充操作的用户体验

* feat(消息系统): 添加标签页刷新功能

在消息系统中新增 RELOAD_TAB 动作类型和 tabId 字段,用于处理标签页刷新请求
修改 StorageCleanerPage 使用后台脚本发送刷新请求,确保弹窗关闭后仍能执行
在 background.ts 中添加标签页刷新处理逻辑,包括错误处理和响应返回

* refactor(theme): 重构主题颜色和样式配置

- 更新主题颜色以满足 WCAG AA 可访问性标准
- 提取全局样式配置到统一变量
- 使用语义化颜色变量替换硬编码值
- 为输入框样式创建统一配置

* feat: add URL entry management components and QR code generation feature

- Introduced `UrlEntryItem` and `UrlEntryList` components for displaying and managing URL entries.
- Added `UrlToQrCodeSection` component for generating QR codes from URLs with download and copy functionality.
- Implemented `AutoRefreshToggle`, `CleaningResult`, `DomainHeader`, `ErrorDisplay`, `OptionItem`, and `StorageOptionsGrid` components for enhanced user interface in storage cleaning.
- Created custom hooks `useStorageCleaner` and `useStorageState` for managing storage-related states and preferences.
- Added utility hook `useUrlPreferences` for handling URL entry preferences.

* feat: 新增时间戳转换器和相关组件,优化时间戳页面功能

* Refactor message handling and storage cleaning logic

* feat: 添加 GitHub Actions CI/CD 工作流,支持自动化构建与发布
2026-04-23 13:57:55 +08:00
LingandRX 0379f96e80 Develop fill form (#10)
* feat(formRecognizer): 添加表单识别功能及相关组件

添加表单识别功能,包括以下内容:
1. 在路由配置中添加表单识别页面
2. 实现表单识别页面和侧边栏面板
3. 添加表单数据生成工具类
4. 实现与内容脚本的通信机制
5. 添加faker-js依赖用于生成测试数据
6. 支持不同入口点(popup/sidepanel)的组件渲染

* refactor(消息通信): 重构消息通信机制并集中管理消息协议

将分散的消息协议和通信逻辑集中到 utils/messages.ts 中
移除旧的 messages.tsx 文件并更新相关引用
添加消息动作枚举和类型定义,提高类型安全性
优化内容脚本注入失败时的处理逻辑
2026-04-20 22:50:01 +08:00
LingandRX be5e2f02ee Develop (#9)
* feat: optimize popup standalone window layout and enhance storage cleaner synchronization

* docs: 更新README文档并删除过时文件

- 更新README文档,添加项目结构、功能特性和路由系统等详细信息
- 删除不再使用的文档文件,包括CLAUDE.md、GEMINI.md和多个设计规范文档
- 清理项目中的过时配置文件和计划文档

* feat: 添加 Vitest 测试框架和组件测试

- 添加 Vitest 配置 (vitest.config.ts, vitest.setup.ts)
- 创建组件测试: Button, ToolCard, GlobalSnackbar, TopBar, RouterContainer, StorageCleanerConfirm
- 创建工具测试: routes, storageCleaner
- 修复 background.ts 监听器参数问题
- 修复 options/App.tsx 硬编码默认值
- 更新 lint-staged.config.mjs (添加 .mjs 支持, 添加 --no-warn-ignored)
- 更新 tsconfig.json (添加测试类型支持, 移除测试文件排除)
- 更新 package.json (添加测试脚本和依赖)

* fix: 修复 StorageCleanerPage Chrome API 监听器内存泄漏

使用 useRef 模式存储 loadInfo 函数引用,避免依赖数组变化导致的监听器重复注册问题

* refactor(popup): 优化 OpenUrl 页面样式和导航逻辑

重构 OpenUrl 页面输入框样式,改进聚焦状态效果
移除 RouterProvider 依赖,直接通过存储设置侧边栏路由
在 OpenUrlViewer 页面添加加载状态指示器和错误处理
监听存储变化实现 URL 自动更新

* feat(ui): 优化存储清理页面UI和交互效果

重构存储清理页面组件,增强视觉层次和交互体验:
- 使用新的错误提示样式和布局
- 改进选项卡片样式,增加悬停动画和选中状态
- 调整整体间距和排版,提升视觉一致性
- 添加微交互效果如悬停缩放和阴影
- 优化颜色方案和过渡动画
- 统一组件尺寸和字体层级

* feat: 添加二维码工具页面,支持URL转二维码和二维码解析功能

* chore: update package-lock.json (npm audit fix)

* refactor(主题): 将页面样式抽离到统一配置文件

将各页面的颜色和样式配置抽离到config/pageTheme.ts中统一管理
优化测试用例中使用each替代forEach
更新路由测试以包含新的qrCode页面

* feat(二维码页面): 添加复制二维码功能并优化样式

添加复制二维码到剪贴板的功能,并调整按钮布局和样式。同时将 ContentCopyIcon 导入位置调整到其他图标导入之后,并修复缩进问题。在 tsconfig.json 中添加 vitest/globals 类型支持。

* feat(theme): 为所有页面添加统一的背景色和卡片背景色

为应用中的所有页面添加了统一的浅灰色背景(#f5f5f5)和白色卡片背景(#ffffff),以保持视觉一致性。修改了ToolCard组件以支持自定义卡片背景色,并更新了所有相关页面使用新的主题配置。

* feat: 添加复制按钮组件并优化现有复制功能

refactor(utils): 创建剪贴板工具函数
feat(components): 新增可复用的CopyButton组件
refactor(pages): 在QrCodePage和TimestampPage中使用CopyButton
style: 格式化代码并调整部分样式

* refactor(存储): 统一qrCode相关存储键名

将'qrCode/expanded'重命名为'qrCode/qrExpanded'以保持命名一致性

* feat: 添加二维码工具功能并更新项目配置

- 新增二维码工具页面及相关组件和工具函数
- 添加 MIT 许可证文件
- 更新 package.json 配置为公开项目
- 更新 README 文档说明新功能
2026-04-19 15:31:14 +08:00
LingandRX bbd0507bcd Develop fastapi (#8)
* feat: add side panel with navigation and storage cleaner functionality

* feat: add OpenUrlPage and integrate into app navigation

* feat: 添加 GlobalSnackbar 组件并在多个页面中集成,替换原有 Snackbar 实现

* feat: fix OpenUrl sidebar issue with architecture refactor

- 修复原问题:不再直接替换侧边栏 URL,保持插件导航可见
- 采用配置页 + 查看页分离架构:OpenUrlPage (配置) + OpenUrlViewerPage (查看)
- 支持多个 URL 快捷方式管理(添加/删除)
- 每个 URL 提供两种打开方式:在侧边栏打开 / 在新标签页打开
- 侧边栏查看页使用 iframe 占满全部剩余空间
- 更新 TypeScript 类型定义
- 保留原有混合内容警告检查
- 数据持久化到 Chrome Storage

* refactor: centralized route management - consolidate routing config into single source

* feat: 更换logo

* refactor: code review fixes - security and race condition improvements

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Changes:

- wxt.config.ts: Remove unused `debugger` permission (no code uses it)
- OpenUrlPage.tsx: Fix unreachable showMessage after window.close()
- OpenUrlPage.tsx: Replace unnecessary div wrapper with Fragment to reduce DOM nesting
- OpenUrlViewerPage.tsx: Add URL validation to prevent XSS via javascript:/data: URLs
- OpenUrlViewerPage.tsx: Add sandbox attribute to iframe for security isolation
- OpenUrlViewerPage.tsx: Add error handling for invalid URLs
- StorageCleanerPage.tsx: Fix race condition in handleOptionChange preference saving
- StorageCleanerPage.tsx: Remove unnecessary storage reads when saving preferences (use state directly)
- StorageCleanerPage.tsx: Add timeout cleanup for setTimeout to follow React best practices

* feat: implement drill-down navigation with master-detail dashboard

* feat: enhance dashboard dynamism and refine options UI

* feat: overhaul storage cleaner UI with real-time size estimation and modern aesthetics

* feat: overhaul TimestampPage UI/UX and fix GlobalSnackbar positioning

* fix: decouple popup routing from storage sync and enhance OpenUrlPage UI

* fix: avoid closing sidepanel when opening URL preview from within sidepanel

* fix: enhance storage cleaner size detection and optimize UI layout

* feat: implement independent route persistence and fix standalone window tab identification

* feat(options): add card sorting functionality to dashboard tools
2026-04-16 23:59:11 +08:00
LingandRX 9a947d7431 Develop fastapi (#7)
* feat: add side panel with navigation and storage cleaner functionality

* feat: add OpenUrlPage and integrate into app navigation

* feat: 添加 GlobalSnackbar 组件并在多个页面中集成,替换原有 Snackbar 实现

* feat: fix OpenUrl sidebar issue with architecture refactor

- 修复原问题:不再直接替换侧边栏 URL,保持插件导航可见
- 采用配置页 + 查看页分离架构:OpenUrlPage (配置) + OpenUrlViewerPage (查看)
- 支持多个 URL 快捷方式管理(添加/删除)
- 每个 URL 提供两种打开方式:在侧边栏打开 / 在新标签页打开
- 侧边栏查看页使用 iframe 占满全部剩余空间
- 更新 TypeScript 类型定义
- 保留原有混合内容警告检查
- 数据持久化到 Chrome Storage

* refactor: centralized route management - consolidate routing config into single source

* feat: 更换logo

* refactor: code review fixes - security and race condition improvements

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Changes:

- wxt.config.ts: Remove unused `debugger` permission (no code uses it)
- OpenUrlPage.tsx: Fix unreachable showMessage after window.close()
- OpenUrlPage.tsx: Replace unnecessary div wrapper with Fragment to reduce DOM nesting
- OpenUrlViewerPage.tsx: Add URL validation to prevent XSS via javascript:/data: URLs
- OpenUrlViewerPage.tsx: Add sandbox attribute to iframe for security isolation
- OpenUrlViewerPage.tsx: Add error handling for invalid URLs
- StorageCleanerPage.tsx: Fix race condition in handleOptionChange preference saving
- StorageCleanerPage.tsx: Remove unnecessary storage reads when saving preferences (use state directly)
- StorageCleanerPage.tsx: Add timeout cleanup for setTimeout to follow React best practices

* feat: implement drill-down navigation with master-detail dashboard

* feat: enhance dashboard dynamism and refine options UI

* feat: overhaul storage cleaner UI with real-time size estimation and modern aesthetics

* feat: overhaul TimestampPage UI/UX and fix GlobalSnackbar positioning

* fix: decouple popup routing from storage sync and enhance OpenUrlPage UI

* fix: avoid closing sidepanel when opening URL preview from within sidepanel

* fix: enhance storage cleaner size detection and optimize UI layout
2026-04-16 18:05:34 +08:00
LingandRX 84ffd2a132 Develop fastapi (#6)
* feat: add side panel with navigation and storage cleaner functionality

* feat: add OpenUrlPage and integrate into app navigation

* feat: 添加 GlobalSnackbar 组件并在多个页面中集成,替换原有 Snackbar 实现

* feat: fix OpenUrl sidebar issue with architecture refactor

- 修复原问题:不再直接替换侧边栏 URL,保持插件导航可见
- 采用配置页 + 查看页分离架构:OpenUrlPage (配置) + OpenUrlViewerPage (查看)
- 支持多个 URL 快捷方式管理(添加/删除)
- 每个 URL 提供两种打开方式:在侧边栏打开 / 在新标签页打开
- 侧边栏查看页使用 iframe 占满全部剩余空间
- 更新 TypeScript 类型定义
- 保留原有混合内容警告检查
- 数据持久化到 Chrome Storage

* refactor: centralized route management - consolidate routing config into single source

* feat: 更换logo

* refactor: code review fixes - security and race condition improvements

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Changes:

- wxt.config.ts: Remove unused `debugger` permission (no code uses it)
- OpenUrlPage.tsx: Fix unreachable showMessage after window.close()
- OpenUrlPage.tsx: Replace unnecessary div wrapper with Fragment to reduce DOM nesting
- OpenUrlViewerPage.tsx: Add URL validation to prevent XSS via javascript:/data: URLs
- OpenUrlViewerPage.tsx: Add sandbox attribute to iframe for security isolation
- OpenUrlViewerPage.tsx: Add error handling for invalid URLs
- StorageCleanerPage.tsx: Fix race condition in handleOptionChange preference saving
- StorageCleanerPage.tsx: Remove unnecessary storage reads when saving preferences (use state directly)
- StorageCleanerPage.tsx: Add timeout cleanup for setTimeout to follow React best practices

* feat: implement drill-down navigation with master-detail dashboard

* feat: enhance dashboard dynamism and refine options UI

* feat: overhaul storage cleaner UI with real-time size estimation and modern aesthetics

* feat: overhaul TimestampPage UI/UX and fix GlobalSnackbar positioning

* fix: decouple popup routing from storage sync and enhance OpenUrlPage UI

* fix: avoid closing sidepanel when opening URL preview from within sidepanel
2026-04-16 08:54:23 +08:00
LingandRX 2cd21973f3 Develop (#5)
* feat: enhance StorageCleanerPage with accordion for storage options and immediate preference saving

* feat: add reusable Button component and update StorageCleanerPage and TimestampPage to use it

* feat: add StorageCleanerConfirm component for confirmation dialog in StorageCleanerPage

* feat: implement fixed height and minimal scrollbar for Popup layout
2026-04-06 17:49:47 +08:00
LingandRX 979b45a898 feat: enhance StorageCleanerPage with accordion for storage options and immediate preference saving (#4) 2026-04-05 17:57:54 +08:00
雨霖铃 8bc11eb696 Fix CLAUDE.md formatting from pre-commit hook
- Apply Prettier formatting changes to CLAUDE.md
- Maintain all reorganized content and structure
- Ensure consistent code formatting per project standards

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 21:17:03 +08:00
雨霖铃 71b9dcc35c Reorganize CLAUDE.md for better clarity and flow
- Restructured content into logical sections: Quick Start, Architecture Overview, Core Features, Development Workflow, Configuration & Implementation, CI/CD & Project Context
- Added command table for better readability of npm scripts
- Simplified directory structure while maintaining essential information
- Added missing technical context: path aliases (@/), TypeScript configuration highlights
- Preserved all original information while reducing redundancy
- Created design document documenting the reorganization approach

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 21:17:03 +08:00
雨霖铃 f4c2f6d374 feat: complete testing-tool browser extension with timestamp converter and storage cleaner 2026-03-23 08:11:05 +08:00
303 changed files with 39514 additions and 17246 deletions
+19
View File
@@ -0,0 +1,19 @@
{
"permissions": {
"allow": [
"Bash(git add:*)",
"Bash(git commit:*)",
"Bash(npx vitest:*)",
"Bash(git rm:*)",
"Bash(git stash:*)",
"Bash(git reset:*)",
"Bash(git checkout:*)",
"Bash(npx tsc:*)",
"Bash(npx eslint:*)",
"Bash(npm run:*)",
"Bash(npm test:*)",
"Bash(pnpm typecheck *)",
"Bash(pnpm test *)"
]
}
}
-29
View File
@@ -1,29 +0,0 @@
{
"env": {
"browser": true,
"es2021": true,
"webextensions": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended"
],
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": [
"react"
],
"rules": {
"react/react-in-jsx-scope": "off",
"react/prop-types": "off",
"no-undef": "error"
},
"globals": {
"chrome": "readonly"
}
}
+17
View File
@@ -0,0 +1,17 @@
# CI/CD 配置
## CI 步骤
严格顺序,任一步骤失败则停止并标记 CI 失败:
1. `setup`(安装依赖、`wxt prepare`
2. 并行运行 `lint``typecheck``test`(三者全部通过才继续)
3. `build`(仅当步骤 2 全部成功时执行)
## Pre-commit Hook
`.husky/pre-commit` 调用 `lint-staged`,任一步骤返回非零则终止提交:
1. 代码文件 (`*.{ts,tsx,js,jsx,mjs}`):运行 `eslint --fix --max-warnings=0 --no-warn-ignored`;若失败则终止并报告错误
2. 同一代码文件:运行 `prettier --write`
3. 其他文件 (`*.{json,css,scss,md}`):运行 `prettier --write`
File diff suppressed because it is too large Load Diff
+151
View File
@@ -0,0 +1,151 @@
name: CI
on:
push:
branches:
- main
pull_request:
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
setup:
name: Prepare Dependencies
runs-on: ubuntu-latest
outputs:
cache-key: ${{ steps.cache-info.outputs.key }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Cache Node Modules
id: cache-nodemodules
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node-v22-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-v22-
- name: Install dependencies
if: steps.cache-nodemodules.outputs.cache-hit != 'true'
run: npm ci
- name: Output Cache Key
id: cache-info
run: echo "key=${{ runner.os }}-node-v22-${{ hashFiles('**/package-lock.json') }}" >> $GITHUB_OUTPUT
lint:
name: Lint
runs-on: ubuntu-latest
needs: setup
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Restore Node Modules Instantantly
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node-v22-${{ hashFiles('**/package-lock.json') }}
- name: Run ESLint
run: npm run lint
typecheck:
name: TypeScript Check
runs-on: ubuntu-latest
needs: setup
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Restore Node Modules Instantantly
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node-v22-${{ hashFiles('**/package-lock.json') }}
- name: Generate WXT types
run: npx wxt prepare
- name: Run TypeScript type check
run: npm run typecheck
test:
name: Unit Tests
runs-on: ubuntu-latest
needs: setup
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Restore Node Modules Instantantly
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node-v22-${{ hashFiles('**/package-lock.json') }}
- name: Generate WXT types
run: npx wxt prepare
- name: Run tests
run: npm run test
build:
name: Build (${{ matrix.browser }})
runs-on: ubuntu-latest
needs: [ lint, typecheck, test ]
strategy:
matrix:
browser: [ chrome, firefox ]
fail-fast: false
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Restore Node Modules Instantantly
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node-v22-${{ hashFiles('**/package-lock.json') }}
- name: Generate WXT types
run: npx wxt prepare
- name: Build Extension (${{ matrix.browser }})
run: |
if npm run | grep -q "build:${{ matrix.browser }}"; then
npm run build:${{ matrix.browser }}
else
npm run build -- --browser ${{ matrix.browser }}
fi
+162
View File
@@ -0,0 +1,162 @@
name: Release
on:
push:
tags:
- 'v*'
permissions:
contents: write
jobs:
# ── Phase 1: 依赖准备 ───────────────────
setup:
name: Prepare Dependencies
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- uses: actions/cache@v4
id: cache-nodemodules
with:
path: node_modules
key: ${{ runner.os }}-release-v22-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-release-v22-
- if: steps.cache-nodemodules.outputs.cache-hit != 'true'
run: npm ci
# ── Phase 2: 质量检查 ──────────────────
lint:
name: Lint
runs-on: ubuntu-latest
needs: setup
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-release-v22-${{ hashFiles('**/package-lock.json') }}
- run: npm run lint
typecheck:
name: TypeScript Check
runs-on: ubuntu-latest
needs: setup
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-release-v22-${{ hashFiles('**/package-lock.json') }}
- run: npx wxt prepare
- run: npm run typecheck
test:
name: Unit Tests
runs-on: ubuntu-latest
needs: setup
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-release-v22-${{ hashFiles('**/package-lock.json') }}
- run: npx wxt prepare
- run: npm run test
# ── Phase 3: 打包 ───────────────────────
build-extension:
name: Package Extension
runs-on: ubuntu-latest
needs: [lint, typecheck, test]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-release-v22-${{ hashFiles('**/package-lock.json') }}
- name: Sync version from tag
run: |
VERSION="${GITHUB_REF_NAME#v}"
echo "Setting version to $VERSION"
npm version "$VERSION" --no-git-tag-version
- name: Build and Zip Extension
run: |
npm run zip
npm run zip:firefox
# 🔍 调试:确认 zip 文件位置
- name: Verify output files
run: |
echo "=== .output/ contents ==="
ls -la .output/ || echo "Not found"
echo "=== All zip files ==="
find . -name "*.zip" -type f
# 💡 关键修复:将 zip 文件复制到固定目录,避免 glob 问题
- name: Prepare release artifacts
run: |
mkdir -p release-archives
cp .output/*.zip release-archives/ 2>/dev/null || true
echo "Files in release-archives:"
ls -la release-archives/
- name: Upload Extension Artifacts
uses: actions/upload-artifact@v4
with:
name: extension-zips
path: release-archives/
if-no-files-found: error
retention-days: 7
# ── Phase 4: 发布 ───────────────────────
release:
name: Create GitHub Release
runs-on: ubuntu-latest
needs: build-extension
steps:
- uses: actions/checkout@v4
- name: Download Extension Artifacts
uses: actions/download-artifact@v4
with:
name: extension-zips
path: release-artifacts
# 🔍 调试:确认下载成功
- name: Verify downloaded artifacts
run: |
echo "=== release-artifacts/ ==="
ls -la release-artifacts/
- name: Extract version from tag
id: version
run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
name: "v${{ steps.version.outputs.version }}"
tag_name: ${{ github.ref_name }}
draft: false
prerelease: ${{ contains(github.ref_name, '-') }}
generate_release_notes: true
files: |
release-artifacts/*.zip
+45 -20
View File
@@ -1,25 +1,50 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.output
stats.html
stats-*.json
.wxt
.vitest
# Environment variables
.env
.env.local
.env.*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.trae/*
.workbuddy/*
.qoder/*
dev/*
# Build outputs
dist/
build/
# Test coverage
coverage/
# IDE files
*.code-workspace
# Other
*.tsbuildinfo
*.tsxbuildinfo
+5
View File
@@ -0,0 +1,5 @@
# 后台运行类型检查,结果输出到 stderr 但不阻塞提交
npx tsc --noEmit &
# 前台运行 lint-staged(自动修复 + 格式化)
npx lint-staged
+13
View File
@@ -0,0 +1,13 @@
# pre-push: 严格检查,阻塞有问题的代码推送到远程
# 类型检查(全项目,因为类型错误可能跨文件传播)
npx tsc --noEmit
# ESLint 严格检查(仅检查本次推送的变更文件,不阻塞不相关的旧代码)
# 新分支无 upstream 时,回退到与 origin/main 对比
MERGE_BASE=$(git merge-base HEAD @{upstream} 2>/dev/null || git merge-base HEAD origin/main 2>/dev/null)
if [ -n "$MERGE_BASE" ]; then
CHANGED_FILES=$(git diff --name-only --diff-filter=d "$MERGE_BASE" HEAD -- '*.ts' '*.tsx' '*.js' '*.jsx' '*.mjs')
if [ -n "$CHANGED_FILES" ]; then
echo "$CHANGED_FILES" | xargs npx eslint --max-warnings=0
fi
fi
+11 -10
View File
@@ -3,17 +3,18 @@
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": false,
"quoteProps": "as-needed",
"singleQuote": true,
"jsxSingleQuote": false,
"trailingComma": "none",
"trailingComma": "all",
"bracketSpacing": true,
"jsxBracketSameLine": false,
"arrowParens": "always",
"requirePragma": false,
"insertPragma": false,
"proseWrap": "preserve",
"htmlWhitespaceSensitivity": "ignore",
"endOfLine": "auto"
"endOfLine": "lf",
"overrides": [
{
"files": "*.json",
"options": {
"trailingComma": "none"
}
}
]
}
+192
View File
@@ -0,0 +1,192 @@
# AGENTS.md
WXT 浏览器扩展项目 (React 19 + TypeScript)。提供时间戳转换、存储清理、JWT 解析、JSON 工具、二维码、Base64、测试数据生成器等测试效率工具。
## 核心命令
```bash
npm run dev # Chrome 开发模式 (HMR)
npm run dev:firefox # Firefox 开发模式
npm run build # Chrome 生产构建
npm run build:firefox # Firefox 生产构建
npm run zip # 打包 Chrome 扩展 (.output/*.zip)
npm run zip:firefox # 打包 Firefox 扩展
npm run lint # ESLint (--max-warnings=0)
npm run typecheck # tsc --noEmit
npm run test # vitest run (单次执行)
npm run test:watch # vitest 监视模式
npm run test:coverage # 带覆盖率的测试
npx vitest run path/to/file.test.ts # 运行单个测试文件
npx wxt prepare # 重新生成 .wxt/ 类型声明(npm install 时 postinstall 会自动执行)
```
修改 `package.json` 或首次克隆仓库后需执行 `npm install`,会自动触发 `postinstall``wxt prepare`
## 验证流程
详见 [CI 配置](./.github/CI.md)。本地与 CI 的检查层次如下。
### Pre-commit`.husky/pre-commit`
1. 后台运行 `tsc --noEmit`(不阻塞提交,结果输出到 stderr)
2. 前台运行 `lint-staged`
- 代码文件 (`*.{ts,tsx,js,jsx,mjs}`)`eslint --fix --max-warnings=0`,再 `prettier --write`
- 其他文件 (`*.{json,css,scss,md}`)`prettier --write`
### Pre-push`.husky/pre-push`
1. 全项目 `tsc --noEmit`(阻塞推送)
2. 对本次推送相对 upstream(或 `origin/main`)变更的 `*.{ts,tsx,js,jsx,mjs}` 文件运行 `eslint --max-warnings=0`
### CIGitHub Actions
1. `setup` — 安装依赖
2. 并行 `lint``typecheck`(含 `wxt prepare`)、`test`
3. `build` — Chrome + Firefox 矩阵构建(仅当步骤 2 全部通过)
## 项目结构
```
src/ # 源代码根目录
config/features.tsx # 功能定义(路由 + 元数据的单一事实来源)
entrypoints/ # 扩展入口点 (popup/, sidepanel/, background.ts, content.ts)
layout/ # 应用壳层布局(TopBar 导航、搜索、主题切换)
pages/ # 功能页面组件 (懒加载)
components/ # 可复用 UI 组件
components/ui/ # shadcn/ui 基础组件 (button, dialog, select 等)
providers/ # React Context (Router, Theme 等)
hooks/ # 自定义 React Hooks
utils/ # 工具函数与服务抽象
types/ # TypeScript 类型声明
lib/ # 通用工具函数(cn、utils)及数据生成器定义
workers/ # Web Worker(数据生成等耗时任务)
spec/ # 功能规格、修复方案与验收标准(见 spec/README.md
public/ # 静态资源(图标等)
.wxt/ # wxt prepare 自动生成,含类型声明与扩展 tsconfig(勿手动编辑)
.output/ # 生产构建输出目录
```
### layout/
应用壳层,与 popup / sidepanel / tab 入口绑定。当前含 `TopBar/`(搜索、主题切换、返回导航、「在标签页打开」),遵循与 `pages/` 相同的 UI + Hook 模式。详见 [layout/README.md](./src/layout/README.md)。
### spec/
功能规格与验收标准文档,重大改动前优先查阅。索引见 [spec/README.md](./spec/README.md)。
### 页面组件模式
典型功能页面遵循 **UI + Hook 分离** 模式。详见 [CODING_STANDARDS.md § 11](./.github/CODING_STANDARDS.md#11-页面开发规范)。
```
src/pages/FeatureName/
├── index.tsx # 页面 UI(纯展示,仅负责渲染布局)
├── useFeatureName.ts # 业务逻辑 Hook(状态管理 + 转换逻辑)
├── constants.ts # 常量定义(可选,≥3 个常量时创建)
└── __tests__/
└── index.test.tsx
```
- 页面入口组件统一命名为 `Index`,通过 `export default function Index()` 导出
- Hook 负责所有状态管理和业务逻辑,通过返回值暴露给页面
- 子组件可以独立调用全局 Hook
-`index.tsx` 超过 150 行时,必须拆分为 UI + Hook 模式
- 复杂页面可增加 `contexts/``hooks/``components/` 子目录
### 测试数据生成器模块
```
src/pages/TestDataGenerator/
├── index.tsx # 主页面(字段配置 + 标签页切换)
├── hooks/useGenerator.ts # Web Worker 管理 Hook(创建、复用、通信、销毁)
└── components/
├── FieldList.tsx # 字段列表(虚拟滚动 + @dnd-kit 拖拽排序 + 规则保存)
├── FieldItem.tsx # 字段卡片展示
├── FieldEditor.tsx # 字段编辑器(名称校验、生成器选择、参数配置)
├── GeneratorSelector.tsx # 生成器选择器(分类 + 搜索)
├── GeneratorConfig.tsx # 生成器参数表单(动态渲染 string/number/boolean/select/array
├── GenerateOptions.tsx # 生成选项(数量、格式)
├── GenerateButton.tsx # 生成按钮 + 进度条
├── DataPreview.tsx # 示例数据预览(JSON 语法高亮)
├── ResultPanel.tsx # 生成结果状态面板
├── ExportPanel.tsx # 导出面板(复制/下载 JSON/CSV)
└── RuleManager.tsx # 规则管理(CRUD、搜索、导入/导出)
src/utils/
├── ruleStorage.ts # 规则持久化存储(localStorage
└── dataExporter.ts # 数据导出工具(JSON/CSV 转换、下载、剪贴板)
src/lib/generators/ # 内置生成器定义(个人信息、企业、技术、基础类型)
src/workers/
└── generator.worker.ts # 数据生成 Web Worker
src/types/
└── testDataGenerator.ts # 类型定义(FieldConfig, DataRule, GeneratorDefinition 等)
```
## 关键架构决策
**路由**: 不使用 React Router。通过 `src/config/features.tsx``FEATURES` 数组管理,`RouterProvider` 根据 `PageType`
渲染对应组件。支持三种渲染模式:popup(弹窗)、sidepanel(侧边栏)和 tab(浏览器新标签页,TopBar「在标签页打开」调用 `openExtensionPage('popup.html', { mode: 'tab' })`,由 `getEntryPointType()` 根据 URL 参数 `mode=tab` 识别)。
每种模式有独立的路由和可见页面配置(`app/popupRoute``app/sidepanelRoute``app/tabRoute` 等)。
**存储**: 所有 Chrome Storage 键必须在 `src/types/storage.d.ts``StorageSchema` 中定义,键名使用 kebab-case 格式(如 `app/currentRoute`)。
使用 `src/utils/chromeStorage.ts` 及其 Hook。Router 同时使用 `chrome.storage.local``localStorage` 快照(`snapshot/{key}`)消除首屏闪烁。
异步加载完成前禁止写入 storage(`RouterProvider``canPersistRef``useStorageState``loadSucceededRef`),避免默认值覆盖已有数据。
**通信**: 使用 `@webext-core/messaging`,协议定义在 `src/utils/messages.ts`
**路径别名**: `@/` 映射到 `src/` 目录(已在 `.wxt/tsconfig.json``vitest.config.ts` 中配置)。项目根目录使用 `@@/`
**浏览器兼容**: 优先使用 `wxt/browser` 导出的 `browser` 对象,而非原生 `chrome` API。
## 测试环境
- 环境: jsdom
- 全局变量: `vitest/globals` (describe, it, expect 等无需导入)
- Setup 文件: `vitest.setup.ts` 自动 mock:
- `chrome.*` / `browser.*` API (storage, tabs, runtime, cookies 等)
- `window.matchMedia`
- 测试文件命名: `__tests__/*.test.{ts,tsx}``*.test.{ts,tsx}`
- Mock 模式: 使用 `vi.mock()` 进行模块级 mock,避免在测试文件中重复 mock 代码
- 测试工具: `@testing-library/react` + `@testing-library/user-event` 进行组件测试
## UI 文案
项目已移除 `chrome.i18n`UI 文案直接在代码中使用中文。
- **功能元数据**: `src/config/features.tsx``FEATURES` 数组定义 `label``description`(用于 Dashboard 卡片与搜索)
- **页面文案**: 在组件 JSX、`constants.ts` 或 Hook 中直接写中文
- **Manifest 文案**: 扩展名称与描述在 `wxt.config.ts``manifest` 中维护
- **Toast / 错误提示**: 在 Hook 或 `constants.ts` 中定义,使用 `sonner``toast()` 展示
## 新功能开发清单
1.`src/types/storage.d.ts` 添加 `PageType` 联合类型
2.`src/config/features.tsx``FEATURES` 数组添加配置(指定 key、label、description、图标、三种渲染模式的组件)
3.`src/pages/` 创建页面组件 (懒加载)
- `index.tsx` — UI 组件(纯展示)
- `useFeatureName.ts` — 业务逻辑 Hook
- `constants.ts` — 常量定义(可选,≥3 个常量时创建)
4. 如需新权限,更新 `wxt.config.ts``manifest.permissions`
5. 添加对应的单元测试
## 代码规范
- 禁止使用 `any` (测试文件除外)
- 未使用变量/参数: 使用 `_` 前缀 (如 `_unused`)
- 样式: 使用 Tailwind CSS + shadcn/ui (通过 `className``cn()` 工具)
- UI 组件: 优先使用 `src/components/ui/` 下的 shadcn/ui 组件 (button, dialog, select 等)
- 图标: 使用 `lucide-react` 图标库
- 格式: Prettier [配置](./.prettierrc)
- ESLint 使用 `typescript-eslint``projectService: true`
- Git Commit: 使用中文描述,遵循 [Conventional Commits 规范](https://www.conventionalcommits.org/zh-hans/v1.0.0/)
## 关键外部库(非显而易见的)
- `@webext-core/messaging` — 扩展消息通信
- `@dnd-kit` — 拖拽排序(用于页面顺序管理和字段列表排序)
- `qrious` + `qr-scanner` — 二维码生成与解析
- `dayjs` — 日期处理(时间戳转换)
- `sonner` — Toast 通知
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Testing Tools
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+134 -38
View File
@@ -1,70 +1,166 @@
# Getting Started with Create React App
# Testing Tools Browser Extension
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
这是一个基于 WXT 框架的浏览器扩展项目,为开发者和测试人员提供实用的效率工具.
## Available Scripts
## 项目概述
In the project directory, you can run:
**Testing Tools** 是一个轻量级、功能丰富的浏览器扩展,采用现代化的技术栈构建. 它旨在简化日常开发和测试任务,如时间戳转换、存储管理、JWT 解析等. 项目利用 [WXT (Web Extension Toolkit)](https://wxt.dev/) 框架,提供了卓越的开发体验和跨浏览器支持.
### `npm start`
## 功能特性
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
### Dashboard 首页
The page will reload when you make changes.\
You may also see any lint errors in the console.
- **工具导航**: 快速访问所有可用工具.
- **个性化定制**: 支持自定义工具的排序和可见性.
- **实时预览**: 在卡片上直接查看实时数据(如当前时间戳).
### `npm test`
### 时间戳转换工具
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
- **实时显示**: 毫秒级精度显示当前系统时间.
- **双向转换**: 日期字符串与 Unix 时间戳(秒/毫秒)之间的无缝转换.
- **多时区支持**: 预设常用时区(亚洲/上海、美洲/纽约、欧洲/伦敦),支持快速切换.
- **快捷操作**: 一键复制转换结果,支持多种格式.
### `npm run build`
### 存储清理工具
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
- **智能识别**: 自动检测并显示当前活动标签页的域名.
- **全面清理**: 支持一键清理 localStorage、sessionStorage、IndexedDB、Cookies、Cache Storage 和 Service Workers.
- **细粒度控制**: 可根据需要选择特定的清理项.
- **自动刷新**: 提供清理后自动刷新页面的选项,确保状态同步.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
### 文本统计工具
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
- **实时分析**: 键入即统计,无需额外操作.
- **多维指标**: 统计字符数、单词数、行数以及精确的字节大小.
- **性能优化**: 采用高性能分词算法,支持大文本处理.
### `npm run eject`
### JWT 解析工具
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
- **快速解码**: 自动解析 JSON Web Token 的 Header 和 Payload.
- **格式化显示**: 以着色和格式化的 JSON 视图展示数据,方便阅读.
- **安全检查**: 自动去除 `Bearer` 前缀,处理异常输入并提供友好提示.
- **签名查看**: 展示 JWT 签名部分,辅助验证令牌完整性.
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
### 右键恢复工具
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
- **当前页面检测**: 自动识别当前活动标签页的域名.
- **一键恢复**: 解除网站对右键菜单的限制,恢复复制、粘贴等基础操作.
- **状态可视化**: 通过 Badge 组件直观展示当前页面的锁定/解锁状态.
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
### 二维码工具
## Learn More
- **生成器**: 将当前 URL 或自定义文本快速转换为二维码,支持下载.
- **解析器**: 支持通过上传图片或粘贴图片来解析二维码内容.
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
### JSON 工具
To learn React, check out the [React documentation](https://reactjs.org/).
- **差异比较**: 对比两段 JSON 数据,高亮展示差异.
- **格式化**: 支持 JSON 美化、压缩、转 YAML / TOML.
### Code Splitting
### Base64 转换器
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
- **文本编解码**: 支持文本内容的 Base64 编码与解码.
- **文件转换**: 支持文件与 Base64 字符串互转.
- **图像预览**: 支持图片 Base64 编码与实时预览.
### Analyzing the Bundle Size
### 测试数据生成器
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
- **可视化字段配置**: 通过 UI 界面定义数据字段,支持拖拽排序、最多 40 个字段.
- **丰富的内置生成器**: 涵盖个人信息(姓名、手机、邮箱)、企业数据(公司名、职位)、技术数据(IP、MAC 地址、UUID)、基础类型(数字、日期、枚举)等多个分类.
- **灵活的参数配置**: 每个生成器支持自定义参数(如数字范围、日期格式、枚举值列表等).
- **空值率与唯一性**: 可为非必填字段设置空值率,支持字段唯一性约束.
- **规则管理**: 保存、加载、编辑、复制、导入/导出字段配置规则,方便复用.
- **批量生成**: 支持 1 ~ 100,000 条数据生成,通过 Web Worker 异步处理避免阻塞 UI.
- **实时预览**: 配置字段后即时预览示例数据结构.
- **多格式导出**: 支持 JSON 和 CSV 格式,提供复制到剪贴板和下载文件两种导出方式.
### Making a Progressive Web App
## 技术栈
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
- **框架**: [WXT (Web Extension Toolkit)](https://wxt.dev/)
- **前端**: React 19 + TypeScript
- **UI 组件**: shadcn/ui (基于 Radix UI 的无头组件库)
- **样式**: Tailwind CSS + class-variance-authority + cn() 工具函数
- **日期处理**: dayjs (集成 UTC 和 Timezone 插件)
- **UI 文案**: 组件内直接使用中文字符串;功能名称与描述定义在 `src/config/features.tsx`
- **通信**: @webext-core/messaging
- **存储**: Chrome Storage API (类型安全封装)
- **解析引擎**: qr-scanner (二维码解析), qrious (二维码生成)
- **测试**: Vitest + Testing Library
- **拖拽排序**: @dnd-kit/core + @dnd-kit/sortable
- **异步生成**: Web Worker (批量数据生成)
### Advanced Configuration
## 项目结构
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
```text
├── src/ # 源代码根目录
│ ├── components/ # 可复用 React 组件
│ ├── config/ # 应用配置
│ │ └── features.tsx # 功能定义与路由映射
│ ├── entrypoints/ # 扩展程序入口点
│ │ ├── popup/ # 点击图标弹出的主界面
│ │ ├── sidepanel/ # 浏览器侧边栏集成
│ │ ├── background.ts # 后台 Service Worker
│ │ └── content.ts # 网页注入脚本
│ ├── pages/ # 各功能模块的页面组件
│ ├── workers/ # Web Worker (数据生成等耗时任务)
│ ├── providers/ # 全局状态提供者 (Router, Theme 等)
│ ├── hooks/ # 自定义 React Hooks
│ ├── utils/ # 工具函数与服务抽象
│ ├── types/ # TypeScript 类型声明
│ └── lib/ # 通用工具函数与生成器库 (cn, utils, generators 等)
├── public/ # 静态资源 (图标等)
├── wxt.config.ts # WXT 框架核心配置
└── package.json # 项目元数据与依赖管理
```
### Deployment
## 开发与部署
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
### 开发环境要求
### `npm run build` fails to minify
- Node.js >= 18.x
- npm 或 pnpm
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
### 常用命令
| 命令 | 说明 |
| ----------------------- | --------------------------------- |
| `npm run dev` | 启动 Chrome 开发模式(支持 HMR |
| `npm run dev:firefox` | 启动 Firefox 开发模式 |
| `npm run build` | 构建 Chrome 生产版本 |
| `npm run build:firefox` | 构建 Firefox 生产版本 |
| `npm run zip` | 打包 Chrome 扩展 (.output/\*.zip) |
| `npm run zip:firefox` | 打包 Firefox 扩展 |
| `npm run typecheck` | 执行 TypeScript 类型检查 |
| `npm run lint` | 执行 ESLint 代码规范检查 |
| `npm run test` | 运行单元测试 |
| `npm run test:coverage` | 生成测试覆盖率报告 |
运行单个测试: `npx vitest run path/to/file.test.ts`
### 自动化流程
项目通过 GitHub Actions 实现了完善的 CI/CD 流程:
- **CI**: 每次推送或 PR 都会自动执行 Lint、类型检查、测试和构建验证.
- **Release**: 推送以 `v*` 开头的 Tag 会自动打包并创建 GitHub Release.
## 权限说明
本扩展根据功能需要申请了以下权限:
- `storage` & `unlimitedStorage`: 存储用户设置、工具配置及大量数据.
- `activeTab` & `tabs`: 获取当前页面 URL 及其元数据.
- `scripting`: 在网页中执行清理和右键恢复脚本.
- `cookies`: 管理和清理网站 Cookie.
- `sidePanel`: 支持在浏览器侧边栏中运行.
- `clipboardWrite`: 提供一键复制功能.
- `contextMenus`: 注册右键菜单,支持快捷操作.
## 浏览器支持
- Chrome (及其它 Chromium 内核浏览器)
- Firefox
## 许可证
基于 [MIT License](LICENSE) 开源.
+20
View File
@@ -0,0 +1,20 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/styles/shell.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}
+868
View File
@@ -0,0 +1,868 @@
# Testing Tools — 视觉规范文档
> **版本**: 1.0.0
> **日期**: 2026-05-29
> **适用范围**: 所有新页面、新组件、UI 修改
> **设计系统**: 基于 [shadcn/ui](https://ui.shadcn.com/) + Tailwind CSS
---
## 目录
1. [设计原则](#1-设计原则)
2. [色彩系统](#2-色彩系统)
3. [排版规范](#3-排版规范)
4. [间距与布局](#4-间距与布局)
5. [圆角与阴影](#5-圆角与阴影)
6. [组件规范](#6-组件规范)
7. [交互与动效](#7-交互与动效)
8. [暗色模式](#8-暗色模式)
9. [工具色彩标识](#9-工具色彩标识)
10. [代码规范](#10-代码规范)
11. [反模式清单](#11-反模式清单)
---
## 1. 设计原则
### 1.1 核心定位
Testing Tools 是一款**浏览器扩展开发者工具集**,视觉风格遵循:
- **专业克制** — 低饱和度色彩,避免视觉噪音
- **信息密度优先** — 紧凑布局,在 400×600px 的 popup 空间内高效展示
- **开发者友好** — 等宽字体用于代码/数据,清晰的信息层级
- **一致性至上** — 所有页面、组件遵循同一套视觉语言
### 1.2 设计关键词
```
简洁 · 现代 · 功能导向 · 低对比度 · 微圆角 · 微妙阴影
```
### 1.3 与 shadcn/ui 的关系
本项目以 shadcn/ui 为底座,所有基础组件(Button、Input、Select 等)均来自或对齐 shadcn/ui 的默认样式。业务组件在此基础上扩展,**不得破坏底层设计语言的统一性**。
---
## 2. 色彩系统
### 2.1 CSS 变量定义
所有色彩通过 CSS 自定义属性(HSL 格式)管理,定义于 `src/index.css`
#### 亮色模式 (`:root`)
| 变量名 | HSL 值 | 用途 | 近似色 |
| -------------------------- | ------------------- | ------------- | --------- |
| `--background` | `0 0% 100%` | 页面背景 | `#ffffff` |
| `--foreground` | `222.2 84% 4.9%` | 主文字 | `#020617` |
| `--card` | `0 0% 100%` | 卡片背景 | `#ffffff` |
| `--card-foreground` | `222.2 84% 4.9%` | 卡片文字 | `#020617` |
| `--popover` | `0 0% 100%` | 浮层背景 | `#ffffff` |
| `--popover-foreground` | `222.2 84% 4.9%` | 浮层文字 | `#020617` |
| `--primary` | `222.2 47.4% 11.2%` | 主按钮/强调 | `#0f172a` |
| `--primary-foreground` | `210 40% 98%` | 主按钮文字 | `#f8fafc` |
| `--secondary` | `210 40% 96.1%` | 次级背景 | `#f1f5f9` |
| `--secondary-foreground` | `222.2 47.4% 11.2%` | 次级文字 | `#0f172a` |
| `--muted` | `210 40% 96.1%` | 静音/禁用背景 | `#f1f5f9` |
| `--muted-foreground` | `215.4 16.3% 46.9%` | 次要文字 | `#64748b` |
| `--accent` | `210 40% 96.1%` | 悬停高亮 | `#f1f5f9` |
| `--accent-foreground` | `222.2 47.4% 11.2%` | 悬停文字 | `#0f172a` |
| `--destructive` | `0 84.2% 60.2%` | 错误/删除 | `#ef4444` |
| `--destructive-foreground` | `210 40% 98%` | 错误文字 | `#f8fafc` |
| `--border` | `214.3 31.8% 91.4%` | 边框 | `#e2e8f0` |
| `--input` | `214.3 31.8% 91.4%` | 输入框边框 | `#e2e8f0` |
| `--ring` | `222.2 84% 4.9%` | 焦点环 | `#020617` |
| `--radius` | `0.5rem` | 全局圆角 | `8px` |
#### 暗色模式 (`.dark`)
暗色模式下所有变量自动反转,保持对比度关系:
| 变量名 | HSL 值 | 近似色 |
| ---------------------- | ------------------- | --------- |
| `--background` | `222.2 84% 4.9%` | `#020617` |
| `--foreground` | `210 40% 98%` | `#f8fafc` |
| `--primary` | `210 40% 98%` | `#f8fafc` |
| `--primary-foreground` | `222.2 47.4% 11.2%` | `#0f172a` |
| `--secondary` | `217.2 32.6% 17.5%` | `#1e293b` |
| `--muted` | `217.2 32.6% 17.5%` | `#1e293b` |
| `--border` | `217.2 32.6% 17.5%` | `#1e293b` |
### 2.2 使用规范
```tsx
// ✅ 正确:使用 CSS 变量
<div className="bg-background text-foreground border-border">
// ✅ 正确:使用语义化色彩名
<Button className="bg-primary text-primary-foreground">
<span className="text-muted-foreground">
// ❌ 错误:硬编码颜色值
<div className="bg-white text-black">
<div className="bg-[#f1f5f9]">
```
### 2.3 语义化色彩使用场景
| 色彩 | 场景 |
| -------------------------- | -------------------------------- |
| `background` | 页面根背景 |
| `foreground` | 主标题、正文 |
| `muted-foreground` | 描述文字、占位符、次级标签 |
| `border` | 卡片边框、分割线、输入框边框 |
| `card` + `card-foreground` | 卡片容器及其内容 |
| `primary` | 主按钮、选中状态、关键操作 |
| `secondary` | 次级按钮、工具栏背景、标签页背景 |
| `destructive` | 错误提示、删除操作、验证失败 |
| `accent` | 悬停背景、下拉选中项 |
---
## 3. 排版规范
### 3.1 字体栈
项目使用系统默认字体栈(Tailwind 默认),**不引入自定义字体**。
```css
/* Tailwind 默认 sans-serif */
font-family:
ui-sans-serif,
system-ui,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
'Helvetica Neue',
Arial,
sans-serif;
/* 等宽字体用于代码/数据 */
font-family:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
```
### 3.2 字号层级
| 层级 | 类名 | 大小 | 字重 | 用途 |
| --------- | ------------------------------------------------ | ---- | ------- | ---------------------- |
| 页面标题 | `text-base font-bold` | 16px | 700 | 页面主标题(极少使用) |
| 卡片标题 | `text-sm font-bold tracking-tight` | 14px | 700 | 卡片/区块标题 |
| 正文 | `text-sm` | 14px | 400 | 普通正文 |
| 次级文字 | `text-xs` | 12px | 400/500 | 描述、标签 |
| 微标签 | `text-[10px] font-bold uppercase tracking-wider` | 10px | 700 | 区域标签、分类标题 |
| 数据/代码 | `font-mono text-sm` | 14px | 400 | 时间戳、JSON、代码 |
### 3.3 排版模式
```tsx
// 区域标签(Section Label)— 最常用
<span className="text-[10px] font-bold text-muted-foreground/90 uppercase tracking-wider">
</span>
// 卡片标题
<h4 className="font-bold text-sm tracking-tight text-foreground leading-snug">
</h4>
// 描述文字
<p className="text-[11px] font-medium text-muted-foreground/90 leading-normal">
</p>
// 数据展示
<span className="font-mono font-bold text-foreground text-sm tracking-tight tabular-nums">
1716950400000
</span>
```
### 3.4 行高与字间距
| 属性 | 值 | 场景 |
| ----------------- | -------- | ------------------ |
| `leading-none` | 1 | 单行数据、紧凑布局 |
| `leading-snug` | 1.375 | 标题、短文本 |
| `leading-relaxed` | 1.625 | 长文本、代码块 |
| `tracking-tight` | -0.025em | 标题、数据 |
| `tracking-wider` | 0.05em | 大写标签 |
---
## 4. 间距与布局
### 4.1 容器尺寸
```tsx
// Popup 模式(默认)
<div className="w-[400px] max-w-[400px] min-w-[400px] h-[600px] min-h-[600px]">
// Tab 模式(全屏自适应)
<div className="sm:w-screen sm:max-w-none sm:min-w-0 sm:h-screen sm:min-h-0">
```
### 4.2 间距节奏
| Token | 值 | 使用场景 |
| --------------- | ----------- | ------------------ |
| `p-3` / `p-3.5` | 12px / 14px | 页面内边距(紧凑) |
| `p-4` | 16px | 标准页面内边距 |
| `p-5` | 20px | 卡片内部填充 |
| `gap-2` | 8px | 紧凑元素间距 |
| `gap-3` | 12px | 标准元素间距 |
| `gap-4` | 16px | 区块间距 |
| `gap-6` | 24px | 大区块间距 |
### 4.3 布局模式
#### 页面布局
```tsx
// 标准页面结构
<div className="p-4 w-full flex flex-col space-y-4 min-h-[500px] select-none">{/* 页面内容 */}</div>
```
#### 卡片布局
```tsx
// 标准卡片
<div className="p-5 rounded-xl border border-border bg-card text-card-foreground shadow-sm">
{/* 卡片内容 */}
</div>
// 可聚焦卡片(含焦点环)
<div className="border border-border rounded-xl bg-card ... focus-within:ring-1 focus-within:ring-ring focus-within:border-ring">
```
#### 双栏网格
```tsx
// 响应式双栏
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 items-stretch">
```
#### 工具卡片网格(Dashboard
```tsx
// Dashboard 紧凑网格
<div className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 gap-2">
```
---
## 5. 圆角与阴影
### 5.1 圆角体系
| Token | 值 | 使用元素 |
| -------------- | ------ | ---------------------- |
| `rounded-sm` | 2px | Checkbox、小标签 |
| `rounded-md` | 6px | 按钮、输入框、Select |
| `rounded-lg` | 8px | 搜索框、小卡片 |
| `rounded-xl` | 12px | 大卡片、面板、图标容器 |
| `rounded-full` | 9999px | 标签、Avatar |
### 5.2 阴影体系
| 级别 | 类名 | 用途 |
| ---- | --------------------- | ---------------------- |
| 无 | — | 静态元素 |
| 低 | `shadow-sm` | 卡片、输入框、按钮 |
| 中 | `shadow-lg` | 下拉菜单、浮层、Dialog |
| 动态 | 自定义 `shadow-[...]` | 卡片悬停时的彩色阴影 |
### 5.3 彩色阴影规范(工具卡片专用)
```tsx
// 工具卡片悬停阴影 — 必须使用 rgba 格式配合 CSS 变量
className="hover:shadow-[0_8px_24px_-8px_rgba(var(--tool-color),0.14)]
dark:hover:shadow-[0_8px_30px_-10px_rgba(var(--tool-color),0.25)]"
```
---
## 6. 组件规范
### 6.1 Button
来源:`src/components/ui/button.tsx`
#### 变体
| 变体 | 类名 | 场景 |
| ------------- | -------------------------------------------- | ------------------ |
| `default` | `bg-primary text-primary-foreground` | 主操作 |
| `destructive` | `bg-destructive text-destructive-foreground` | 删除、危险操作 |
| `outline` | `border border-input bg-background` | 次级操作、取消 |
| `secondary` | `bg-secondary text-secondary-foreground` | 次要操作 |
| `ghost` | 仅悬停背景 | 图标按钮、低优先级 |
| `link` | 下划线文字 | 跳转链接 |
#### 尺寸
| 尺寸 | 高度 | 内边距 | 场景 |
| --------- | ------- | ----------- | -------- |
| `default` | 40px | `px-4 py-2` | 标准按钮 |
| `sm` | 36px | `px-3` | 紧凑按钮 |
| `lg` | 44px | `px-8` | 突出按钮 |
| `icon` | 40×40px | — | 图标按钮 |
#### 使用示例
```tsx
// 主操作
<Button></Button>
// 图标按钮
<Button variant="ghost" size="icon">
<Copy className="h-4 w-4" />
</Button>
// 危险操作
<Button variant="destructive" size="sm"></Button>
```
### 6.2 Input
来源:`src/components/ui/input.tsx`
```tsx
// 标准输入框
<Input
className="font-mono font-semibold h-10 shadow-sm placeholder:text-muted-foreground/60 focus:bg-background"
/>
// 错误状态
<Input
className="border-destructive focus-visible:ring-destructive"
/>
```
**规范要点**
- 高度统一为 `h-10`40px
- 等宽字体用于数据输入
- 占位符使用 `text-muted-foreground/60`
- 错误时边框变红并调整焦点环
### 6.3 SwitchButtonGroup
来源:`src/components/ui/switch.tsx`
```tsx
// 分段控制器
<SwitchButtonGroup
value={mode}
options={[
{ value: 'ts2dt', label: '转日期' },
{ value: 'dt2ts', label: '转时间戳' },
]}
onChange={setMode}
size="small"
/>
```
**规范要点**
- 容器:`rounded-lg bg-muted p-1`
- 选中项:`bg-background text-foreground shadow-sm font-semibold`
- 未选中项:`hover:bg-background/50 hover:text-foreground/80`
- 尺寸:`small`32px)用于工具页,`medium`36px)标准
### 6.4 Card(工具卡片)
来源:`src/pages/Dashboard/ToolCard.tsx`
```tsx
// 标准工具卡片结构
<div className="group relative rounded-xl border border-border/70 bg-card p-4 h-auto flex flex-col gap-3 shadow-sm">
{/* 上半部分:图标 + 标题 + 箭头 */}
<div className="flex items-center justify-between">
<div className="flex gap-3 items-center">
{/* 图标容器 */}
<div className="flex items-center justify-center w-10 h-10 rounded-xl bg-[rgba(var(--tool-color),0.08)] text-[rgb(var(--tool-color))]">
<Icon className="h-5 w-5" />
</div>
{/* 文字 */}
<div>
<h4 className="font-bold text-sm"></h4>
<p className="text-[11px] text-muted-foreground/90"></p>
</div>
</div>
<ChevronRight className="h-4 w-4" />
</div>
{/* 下半部分:预览区(可选) */}
<div className="mt-1 pt-3 border-t border-dashed border-border/80">{snapshot}</div>
</div>
```
### 6.5 TextInputArea
来源:`src/components/TextInputArea.tsx`
```tsx
// 多行文本输入区
<TextInputArea
value={input}
onChange={setInput}
placeholder="输入内容..."
showCount={true}
showClear={true}
allowCopy={true}
minRows={6}
maxRows={12}
/>
```
**规范要点**
- 外容器:`rounded-md border border-input bg-background shadow-sm`
- 焦点状态:`focus-within:ring-1 focus-within:ring-ring`
- 错误状态:`border-destructive focus-within:ring-destructive`
- 底部工具栏:`h-10 bg-muted/30 border-t border-border/50`
- 字体:`font-mono text-sm`
### 6.6 Dialog
来源:`src/components/ui/dialog.tsx`
```tsx
// 对话框内容
<DialogContent className="sm:rounded-lg">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
{/* 内容 */}
<DialogFooter>
<Button variant="outline"></Button>
<Button></Button>
</DialogFooter>
</DialogContent>
```
### 6.7 Select
来源:`src/components/ui/select.tsx`
```tsx
<Select>
<SelectTrigger className="h-9 shadow-sm bg-background">
<SelectValue />
</SelectTrigger>
<SelectContent className="max-h-64">
<SelectItem className="text-xs font-semibold focus:bg-accent cursor-pointer"></SelectItem>
</SelectContent>
</Select>
```
### 6.8 Checkbox
来源:`src/components/ui/checkbox.tsx`
```tsx
// 标准复选框
<Checkbox className="h-4 w-4 rounded-sm border-primary data-[state=checked]:bg-primary" />
// 小型复选框(工具栏内)
<Checkbox className="h-3.5 w-3.5 rounded border-input data-[state=checked]:bg-primary shadow-sm" />
```
### 6.9 Badge
来源:`src/components/ui/badge.tsx`
| 变体 | 场景 |
| ------------- | ------------------ |
| `default` | 状态标签、分类 |
| `secondary` | 次要标签 |
| `destructive` | 错误标签 |
| `outline` | 可点击标签、筛选器 |
### 6.10 CopyButton
来源:`src/components/CopyButton.tsx`
```tsx
// 标准复制按钮
<CopyButton text={content} />
// 小型复制按钮
<CopyButton text={content} size="sm" className="h-7 w-7 rounded-md border" />
```
**规范要点**
- 默认 `variant="ghost" size="icon"`
- 复制成功后变为绿色背景 + 对勾图标
- 使用 `sonner` toast 提示复制结果
---
## 7. 交互与动效
### 7.1 过渡规范
| 属性 | 值 | 场景 |
| ------------------- | ---------- | ----------------------------- |
| `transition-colors` | 150ms ease | 色彩变化(悬停、焦点) |
| `transition-all` | 150ms ease | 综合变化(SwitchButtonGroup |
| `duration-200` | 200ms | 复制按钮状态切换 |
### 7.2 焦点状态
所有可交互元素必须有可见的焦点指示器:
```tsx
// 标准焦点环
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2
// 紧凑焦点环(图标按钮)
focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring
// 输入框焦点
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2
```
### 7.3 悬停状态
```tsx
// 按钮悬停
hover:bg-accent hover:text-accent-foreground
// 卡片悬停
hover:bg-muted/30 hover:border-[rgba(var(--tool-color),0.45)]
// 链接/文字悬停
hover:text-foreground hover:underline
```
### 7.4 动画规范
使用 `tailwindcss-animate` 提供的动画:
```tsx
// 淡入
animate-in fade-in duration-150
// 淡入 + 缩放(SwitchButtonGroup 选中项)
animate-in fade-in-50 zoom-in-95 duration-150
// 从顶部滑入(下拉菜单)
animate-in fade-in slide-in-from-top-2 duration-150
// 错误提示出现
animate-in fade-in slide-in-from-top-1 duration-150
// 骨架屏脉冲
animate-pulse
```
### 7.5 禁用状态
```tsx
// 统一禁用样式
disabled:pointer-events-none disabled:opacity-50
```
---
## 8. 暗色模式
### 8.1 实现方式
通过 `darkMode: 'class'`Tailwind 配置)+ `.dark` 类切换:
```tsx
// ThemeModeProvider 自动处理
document.documentElement.classList.toggle('dark', resolvedMode === 'dark');
```
### 8.2 暗色模式下的特殊处理
```tsx
// 彩色阴影增强(暗色模式下阴影需要更高透明度)
shadow-[0_8px_24px_-8px_rgba(var(--tool-color),0.14)]
dark:shadow-[0_8px_30px_-10px_rgba(var(--tool-color),0.25)]
// 图标容器背景增强
bg-[rgba(var(--tool-color),0.08)]
dark:bg-[rgba(var(--tool-color),0.12)]
// 成功状态文字调整
text-emerald-600 dark:text-emerald-400
```
### 8.3 暗色模式色彩映射原则
| 亮色 | 暗色 | 说明 |
| ---------------- | ---------------- | ------------------------------- |
| 纯白背景 | 深蓝黑背景 | 避免纯黑 `#000`,使用 `#020617` |
| 浅灰背景 | 深灰背景 | 保持层次关系 |
| 深文字 | 浅文字 | 反转对比度 |
| 彩色阴影低透明度 | 彩色阴影高透明度 | 暗色需要更强视觉反馈 |
---
## 9. 工具色彩标识
### 9.1 色板定义
每个工具分配一个主题色,定义于 `src/config/features.tsx`
```ts
const PALETTE_COLORS: Record<PaletteColorKey, string> = {
primary: '13, 148, 136', // teal (#0d9488)
success: '22, 163, 74', // green (#16a34a)
warning: '217, 119, 6', // amber (#d97706)
error: '220, 38, 38', // red (#dc2626)
secondary: '147, 51, 232', // purple (#9333e8)
info: '37, 99, 235', // blue (#2563eb)
};
```
### 9.2 工具色彩分配
| 工具 | 色彩键 | 色值 |
| ----------- | ----------- | ------ |
| 时间戳转换 | `primary` | Teal |
| 存储清理 | `warning` | Amber |
| 二维码工具 | `success` | Green |
| 文本统计 | `secondary` | Purple |
| JWT 解析 | `info` | Blue |
| JSON 对比 | `primary` | Teal |
| Base64 转换 | `info` | Blue |
| 右键还原 | `success` | Green |
### 9.3 工具色彩使用规范
```tsx
// 1. 通过 style 注入 CSS 变量
<div style={{ ['--tool-color' as string]: rgbValues }}>
// 2. 图标容器背景(低透明度)
bg-[rgba(var(--tool-color),0.08)]
dark:bg-[rgba(var(--tool-color),0.12)]
// 3. 图标颜色
text-[rgb(var(--tool-color))]
// 4. 悬停边框
hover:border-[rgba(var(--tool-color),0.45)]
// 5. 悬停阴影
hover:shadow-[0_8px_24px_-8px_rgba(var(--tool-color),0.14)]
// 6. 箭头悬停色
group-hover:text-[rgb(var(--tool-color))]
```
**注意**:工具色彩仅用于**标识和装饰**,不得用于功能性色彩(如成功/错误状态)。
---
## 10. 代码规范
### 10.1 Tailwind 类名组织顺序
使用 `cn()` 工具函数(`clsx` + `tailwind-merge`)组合类名,按以下顺序排列:
```tsx
className={cn(
// 1. 布局(display, position, flex, grid
'flex items-center justify-between',
// 2. 尺寸(width, height, padding, margin
'w-full h-10 px-4',
// 3. 外观(background, border, shadow, rounded
'rounded-md border border-input bg-background shadow-sm',
// 4. 文字(color, font, text-align
'text-sm font-medium text-foreground',
// 5. 交互(hover, focus, disabled, cursor
'hover:bg-accent focus-visible:ring-2 disabled:opacity-50',
// 6. 动画(transition, animate
'transition-colors duration-150',
// 7. 条件类
isActive && 'bg-primary text-primary-foreground',
// 8. 外部传入
className,
)}
```
### 10.2 颜色使用检查清单
- [ ] 所有颜色使用 CSS 变量(`bg-background` 而非 `bg-white`
- [ ] 边框使用 `border-border` 及其透明度变体
- [ ] 文字层级使用 `foreground``muted-foreground``muted-foreground/60`
- [ ] 错误状态使用 `destructive` 系列
- [ ] 工具色彩仅用于装饰性元素
### 10.3 组件文件组织
```
src/
├── components/ui/ # shadcn 基础组件(只读,不修改)
├── components/ # 业务组件
│ ├── CopyButton.tsx
│ ├── SwitchButtonGroup.tsx
│ ├── TextInputArea.tsx
│ └── ...
├── pages/ # 页面组件
│ ├── <ToolName>/
│ │ ├── index.tsx # 页面入口
│ │ ├── use<ToolName>.ts # 业务逻辑 Hook
│ │ └── components/ # 页面私有组件
│ └── ...
└── providers/ # Context Providers
```
### 10.4 新增页面模板
```tsx
// src/pages/NewTool/index.tsx
import SwitchButtonGroup from '@/components/SwitchButtonGroup';
import { cn } from '@/lib/utils';
export default function Index() {
return (
<div className="p-4 w-full flex flex-col space-y-4 select-none">
{/* 页面内容 */}
<div className="p-5 rounded-xl border border-border bg-card text-card-foreground shadow-sm">
<h4 className="font-bold text-sm tracking-tight"></h4>
</div>
</div>
);
}
```
---
## 11. 反模式清单
以下模式**禁止**在项目中使用:
### 11.1 色彩反模式
```tsx
// ❌ 硬编码颜色
<div className="bg-white text-black">
<div className="bg-gray-100">
<div className="text-gray-500">
// ❌ 使用非语义化 Tailwind 颜色
<div className="bg-slate-50">
<div className="text-zinc-400">
// ✅ 使用 CSS 变量
<div className="bg-background text-foreground">
<div className="bg-muted text-muted-foreground">
```
### 11.2 布局反模式
```tsx
// ❌ 固定高度导致内容截断
<div className="h-[200px]">
// ✅ 使用 min-height 或自适应
<div className="min-h-[200px]">
<div className="h-auto">
// ❌ 使用 margin 做组件间距
<div className="mb-4">
// ✅ 使用 gap
<div className="flex flex-col gap-4">
```
### 11.3 组件反模式
```tsx
// ❌ 修改 shadcn/ui 基础组件样式
// 如需修改,通过 className 覆盖或创建包装组件
// ❌ 内联样式用于颜色(工具色彩除外)
<div style={{ backgroundColor: '#f1f5f9' }}>
// ❌ 混合使用不同圆角体系
<Button className="rounded-lg"> // Button 应为 rounded-md
// ❌ 忽略焦点状态
<button className="..."> // 缺少 focus-visible 样式
```
### 11.4 暗色模式反模式
```tsx
// ❌ 仅适配部分元素
<div className="bg-white text-black dark:bg-gray-900 dark:text-white">
// ✅ 使用 CSS 变量自动适配
<div className="bg-background text-foreground">
// ❌ 暗色模式下使用不合适的透明度
<div className="bg-black/5 dark:bg-white/5"> // 对比度不足
// ✅ 使用语义化变量
<div className="bg-muted">
```
---
## 12. 附录
### 12.1 常用类名速查
```
// 页面容器
p-4 w-full flex flex-col space-y-4 min-h-[500px] select-none
// 标准卡片
p-5 rounded-xl border border-border bg-card text-card-foreground shadow-sm
// 工具栏
flex h-10 items-center justify-between px-1.5 bg-secondary/40 rounded-xl border border-border/60
// 区域标签
text-[10px] font-bold text-muted-foreground/90 uppercase tracking-wider
// 数据展示
font-mono font-semibold text-foreground text-sm
// 错误提示
text-xs font-medium text-destructive
// 空状态
p-8 rounded-xl bg-muted/30 border border-dashed border-border/80 text-center
// 图标按钮容器
flex h-8 w-8 items-center justify-center rounded-md border border-input bg-background shadow-sm
// 焦点环
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2
```
### 12.2 相关文件
| 文件 | 说明 |
| ------------------------------------- | ----------------------- |
| `src/index.css` | CSS 变量定义、全局样式 |
| `tailwind.config.js` | Tailwind 配置、色彩映射 |
| `src/lib/utils.ts` | `cn()` 工具函数 |
| `src/components/ui/*.tsx` | shadcn/ui 基础组件 |
| `src/config/features.tsx` | 工具配置、色彩分配 |
| `src/providers/ThemeModeProvider.tsx` | 主题模式管理 |
### 12.3 参考资源
- [shadcn/ui 文档](https://ui.shadcn.com/docs)
- [Tailwind CSS 文档](https://tailwindcss.com/docs)
- [Radix UI 文档](https://www.radix-ui.com/)
- [Lucide Icons](https://lucide.dev/)
---
_本文档随项目迭代更新。新增组件或修改视觉风格时,请同步更新此文档。_
+7
View File
@@ -0,0 +1,7 @@
# Spec 目录
功能规格、修复方案与验收标准文档。
| 文档 | 状态 | 说明 |
| -------------------------------------------------------------------------------- | ----------------- | ---------------------------------------------------- |
| [storage-cleaner/indexeddb-fix-plan.md](./storage-cleaner/indexeddb-fix-plan.md) | ✅ Phase 3 已完成 | Storage Cleaner IndexedDB 清理逻辑修复方案与验收标准 |
@@ -0,0 +1,435 @@
# Storage Cleaner — IndexedDB 修复方案与验收标准
> 创建时间: 2026-06-25
> 状态: ✅ Phase 3 已完成(2026-06-25
> 关联模块: `src/utils/storageCleaner.ts`
> 前置审查: Code Review`storageCleaner.ts` 修改版)
## 背景与目标
本次修复针对 `storageCleaner.ts` 中 IndexedDB 清理逻辑及错误处理链路的审查结论,按优先级分三阶段实施。
| 问题域 | 现状 | 目标 |
| ------------------- | ---------------------------------------------- | --------------------------------- |
| IndexedDB fallback | `store.clear()` 完成后立即 `db.close()` | 等 transaction commit 后再关闭 |
| deleteDatabase 超时 | 超时后仍可能触发 `onsuccess`,与 fallback 并发 | 单次删除生命周期内只 resolve 一次 |
| 部分成功 | 多 DB 部分失败时 `count` 丢失 | 失败结果保留已清理数量 |
| 代码结构 | `runScript` / `runCleanScript` 重复 | 统一 executeScript 入口 |
| 测试 | 缺多 DB 混合场景 | 补单元测试覆盖 |
---
## Phase 1 — 合并前必做(P1
### 1.1 等待 IndexedDB transaction 完成后再关闭连接
#### 问题
`clearObjectStores``Promise.all(clearStore...)` 结束后立刻 `db.close()`。单个 `clearReq.onsuccess` 只表示 request 完成,transaction 可能尚未 commit,存在清空被回滚的风险。
#### 根因
IndexedDB 规范中,transaction 的持久化以 `transaction.oncomplete` 为准,而非单个 request 的 `onsuccess`
#### 修复方案
在注入脚本内的 `clearObjectStores` 中,增加 `waitForTransaction` 辅助函数:
```typescript
const waitForTransaction = (tx: IDBTransaction): Promise<void> =>
new Promise((resolve, reject) => {
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error ?? new Error('Transaction failed'));
tx.onabort = () => reject(tx.error ?? new Error('Transaction aborted'));
});
```
修改 `openReq.onsuccess` 分支:
```typescript
const transaction = db.transaction(storeNames, 'readwrite');
const errors = (
await Promise.all(
storeNames.map((storeName) => clearStore(transaction.objectStore(storeName), storeName)),
)
).filter((error): error is string => Boolean(error));
try {
await waitForTransaction(transaction);
} catch {
db.close();
resolve({
success: false,
errors: [`清空 IndexedDB 失败(${dbName}),请刷新后重试`],
});
return;
}
db.close();
resolve({ success: errors.length === 0, errors });
```
#### 涉及文件
- `src/utils/storageCleaner.ts``injectClearIndexedDB``clearObjectStores`
#### 新增测试
```typescript
it('should wait for transaction complete before closing db', async () => {
// mock: clear onsuccess 先于 transaction.oncomplete 触发
// 断言 db.close 在 transaction.oncomplete 之后调用
});
```
---
### 1.2 消除 deleteDatabase 超时竞态
#### 问题
超时 `resolve('timeout')` 后,`deleteReq.onsuccess` 仍可能触发;此时 fallback 的 `indexedDB.open` 与进行中的 `deleteDatabase` 可能并发,行为未定义。
#### 修复方案
为每个 DB 删除引入 **单次 settle** 状态:
```typescript
const waitForDeleteDatabase = (dbName: string, timeoutMs: number) =>
new Promise<'deleted' | 'blocked' | 'timeout' | 'error'>((resolve) => {
let settled = false;
const settle = (status: 'deleted' | 'blocked' | 'timeout' | 'error') => {
if (settled) return;
settled = true;
clearTimeout(timeout);
resolve(status);
};
const deleteReq = indexedDB.deleteDatabase(dbName);
const timeout = setTimeout(() => {
console.warn('IndexedDB delete timeout:', dbName);
settle('timeout');
}, timeoutMs);
deleteReq.onblocked = () => {
console.warn('IndexedDB delete blocked:', dbName);
settle('blocked');
};
deleteReq.onsuccess = () => settle('deleted');
deleteReq.onerror = () => settle('error');
});
```
#### timeout / blocked 后的 fallback 策略
| 状态 | 行为 |
| --------- | ------------------------------------------------------------------ |
| `blocked` | 立即 fallback `clearObjectStores`(页面仍占用连接,open 通常可行) |
| `timeout` | 先 `await delay(100~200ms)` 再 fallback,降低与 delete 并发概率 |
| `error` | 不 fallback,直接报错 |
#### 涉及文件
- `src/utils/storageCleaner.ts` — 替换现有 `new Promise` 删除逻辑
#### 新增测试
```typescript
it('should ignore late onsuccess after delete timeout', async () => {
// deleteBehavior: timeout5000ms 后 resolve timeout
// 6000ms 后再触发 onsuccess
// 断言:只走 fallback 一次,count 不因 late onsuccess 重复 +1
});
```
---
## Phase 2 — 建议同 PR 或紧接 follow-upP2
### 2.1 IndexedDB 部分成功时保留 count
#### 问题
多 DB 场景返回 `{ count: 2, errors: ['...'] }` 时,`runCleanScript` 只返回 `{ success: false, error }`,用户看不到已清理 2 个库。
#### 修复方案(推荐)
扩展失败分支类型,可选 `count`
```typescript
// src/types/storage.d.ts
export type StorageCleanResult =
| { success: true; count: number }
| { success: false; error: string; count?: number }; // 部分成功时的已清理数
```
修改 `runCleanScript`
```typescript
if (raw.errors?.length) {
const errorMsg = raw.errors.join('\n');
const partialHint = raw.count > 0 ? `(已成功清理 ${raw.count} 个数据库,但部分失败)\n` : '';
return {
success: false,
error: partialHint + errorMsg,
...(raw.count > 0 ? { count: raw.count } : {}),
};
}
```
#### UI 层(可选增强)
`CleaningResult.tsx` 失败时若 `result.indexedDB?.count` 存在,可展示部分成功提示(非必须,error 字符串已含 hint 即可)。
#### 涉及文件
- `src/types/storage.d.ts`
- `src/utils/storageCleaner.ts``runCleanScript`
- `src/utils/__tests__/storageCleaner.test.ts`
- (可选)`src/pages/StorageCleaner/components/CleaningResult.tsx`
#### 新增测试
```typescript
it('should preserve partial count when some IndexedDB databases fail', async () => {
// 3 个 DB2 成功删除,1 blocked 且 fallback 失败
// expect: success false, count 2, error 含「已成功清理 2 个」
});
```
---
### 2.2 统一 executeScript 调用入口
#### 问题
`runScript``runCleanScript` 各自调用 `browser.scripting.executeScript`,行为不一致(吞错 vs 抛错)。
#### 修复方案
抽取底层函数:
```typescript
type ExecuteScriptMode = 'fallback' | 'throw';
async function executeInTab<T>(
tabId: number,
func: () => T | Promise<T>,
options: { errorLabel: string; mode: 'fallback'; fallback: T },
): Promise<T>;
async function executeInTab<T>(
tabId: number,
func: () => T | Promise<T>,
options: { errorLabel: string; mode: 'throw' },
): Promise<T>;
async function executeInTab<T>(...) {
try {
const [result] = await browser.scripting.executeScript({ target: { tabId }, func });
return (result?.result as T) ?? (options.mode === 'fallback' ? options.fallback : undefined as T);
} catch (error) {
console.error(`Failed to ${options.errorLabel}:`, error);
if (options.mode === 'throw') throw error;
return options.fallback;
}
}
```
- `runScript``executeInTab(..., { mode: 'fallback', fallback })`
- `runCleanScript``executeInTab(..., { mode: 'throw' })` + 结果解析
#### 涉及文件
- `src/utils/storageCleaner.ts`
#### 验收
现有 11 个测试全部通过,无行为回归。
---
### 2.3 错误信息分隔符统一
#### 问题
`runCleanScript``'; '` 拼接,`clearStorage``result.error``'\n'`UI 用 `break-all` 展示,多错误时可读性不一致。
#### 修复方案
IndexedDB 内部多错误统一改为 `'\n'`
```typescript
error: raw.errors.join('\n');
```
#### 涉及文件
- `src/utils/storageCleaner.ts`
- 相关测试断言(若有 `'; '` 期望)
---
### 2.4 补充多 DB 混合场景测试
| 用例 | 输入 | 期望 |
| ------------------ | ------------------------------ | -------------------------------------------- |
| 全部成功 | 3 DB,均 delete success | `success: true, count: 3` |
| 部分 fallback 成功 | 2 success + 1 blocked→clear OK | `success: true, count: 3` |
| 部分失败 | 2 success + 1 error | `success: false, count: 2, error 含失败库名` |
| 空库列表 | `databases()` 返回 `[]` | `success: true, count: 0` |
#### 涉及文件
- `src/utils/__tests__/storageCleaner.test.ts`
- 扩展 `createIndexedDBMock` 支持 per-db 不同 `deleteBehavior`
---
## Phase 3 — 可选优化(P3
### 3.1 超时常量提升到模块级
```typescript
// src/utils/storageCleaner.ts 或 src/pages/StorageCleaner/constants.ts
const INDEXED_DB_DELETE_TIMEOUT_MS = 5000;
const INDEXED_DB_CLEAR_STORE_TIMEOUT_MS = 5000;
```
注入脚本通过闭包引用(executeScript 会序列化 func,常量需在 func 外部定义并 capture,或仍写在 func 内但从模块常量赋值)。
### 3.2 IndexedDB 逻辑拆分(长期)
`clearObjectStores``waitForDeleteDatabase` 等抽到 `src/utils/indexedDbCleaner.ts` 的纯函数,注入层只做:
```typescript
async () => clearAllIndexedDBs(INDEXED_DB_DELETE_TIMEOUT_MS);
```
便于单测,不依赖 `mockExecuteScriptEval` 间接执行注入函数。工作量大,建议单独 PR。
---
## 实施顺序
```mermaid
flowchart TD
A[1.1 transaction.oncomplete] --> B[1.2 delete settle 防竞态]
B --> C[2.4 补多 DB 测试]
C --> D[2.1 部分成功 count]
D --> E[2.2 统一 executeInTab]
E --> F[2.3 错误分隔符]
F --> G[3.x 可选重构]
```
| 阶段 | 预估工作量 | 风险 |
| ------- | ---------- | ---------------- |
| Phase 1 | 0.5~1 天 | 低,逻辑局部 |
| Phase 2 | 0.5~1 天 | 中,涉及类型扩展 |
| Phase 3 | 1~2 天 | 低,可延后 |
---
## 验收标准
### A. 自动化(CI 必须通过)
```bash
npm run test -- src/utils/__tests__/storageCleaner.test.ts
npm run typecheck
npm run lint
```
| 编号 | 标准 |
| ---- | ------------------------------------------------------------------------------ |
| A-1 | 全部单元测试通过,新增测试 ≥ 3(transaction 顺序、late onsuccess、多 DB 混合) |
| A-2 | `tsc --noEmit` 无错误;若扩展 `StorageCleanResult`,所有引用处类型正确 |
| A-3 | ESLint `--max-warnings=0` 通过 |
---
### B. 功能行为
| 编号 | 场景 | 期望结果 |
| ---- | ---------------------------------------------------- | ----------------------------------------------------------------------------- |
| B-1 | 单 DB`deleteDatabase` 成功 | `indexedDB: { success: true, count: 1 }``overallSuccess: true` |
| B-2 | 单 DB`deleteDatabase` blockedfallback clear 成功 | `success: true, count: 1`transaction 在 `oncomplete``db.close` |
| B-3 | 单 DBdelete 超时 5sfallback clear 成功 | 5s 内进入 fallback;不因 late `onsuccess` 重复计数 |
| B-4 | fallback 中某 store clear hang 5s | `success: false`error 含 `dbName/storeName` |
| B-5 | 3 DB2 成功 + 1 失败 | `success: false``count: 2`Phase 2.1 后),error 含失败库名与部分成功提示 |
| B-6 | `executeScript` 注入失败 | `success: false`**不得** `{ success: true, count: 0 }` |
| B-7 | localStorage 成功 + cookies 失败 | `overallSuccess: false``result.error``Cookies: ...`(换行分隔多项失败) |
---
### C. 回归与 UI
| 编号 | 标准 |
| ---- | --------------------------------------------------------------------------------------------------------- |
| C-1 | `formatCleaningResult` 成功路径不变 |
| C-2 | `CleaningResult` 失败时展示 `result.error`;含 `\n` 时多行可读(现有 `leading-relaxed break-all` 可接受) |
| C-3 | `reloadAfterClean=true``overallSuccess=false` 时不刷新页面(`useStorageCleaner` 现有逻辑) |
| C-4 | Cookie 清理:domain 前导 `.` 剥离逻辑不变 |
---
### D. 手动验收(扩展环境)
在 Chrome 加载 unpacked extension,选普通 HTTPS 页面:
| 编号 | 步骤 | 期望 |
| ---- | ------------------------------------------------- | ----------------------------------------------------------------------- |
| D-1 | 页面写入 localStorage + IndexedDB,仅清 IndexedDB | 成功提示或明确错误;DevTools → Application → IndexedDB 数据为空或库已删 |
| D-2 | 打开 DevTools 保持 IndexedDB 面板,执行清理 | 若 blocked,显示中文提示;fallback 成功后数据不可见 |
| D-3 | 勾选「清理后刷新」且全部成功 | Toast「清理成功,即将刷新页面」,页面刷新 |
| D-4 | 部分失败 | 不刷新;结果区红色展示错误详情 |
---
### E. 代码质量
| 编号 | 标准 |
| ---- | ------------------------------------------------------------------ |
| E-1 | 注入脚本内无重复 `settled` / timeout 逻辑(删除与 clear 各自封装) |
| E-2 | 错误文案仍为中文,含库名/store 名 |
| E-3 | 无 `any`(测试文件除外) |
| E-4 | Phase 1 合并后,P1 项在 PR 描述中标注「已修复」并附测试名 |
---
## PR 检查清单
```markdown
## 修复内容
- [ ] P1: transaction.oncomplete 后再 db.close
- [ ] P1: deleteDatabase settle 防竞态
- [ ] P2: 部分成功保留 count(可选)
- [ ] P2: 多 DB 混合测试
- [ ] P2: 错误信息 `\n` 分隔(可选)
## 验收
- [ ] npm run test / typecheck / lint 通过
- [ ] 新增测试覆盖 B-2、B-3、B-5
- [ ] 手动 D-1 ~ D-4 至少测 D-1、D-3
```
---
## 风险与边界说明
1. **fallback 清空 ≠ 删除库**blocked 时只清 object store,库结构仍在;成功 `count` 表示「有效清理动作完成」,需在 UI/文档中说明(可选文案:「数据已清空,数据库结构可能仍存在」)。
2. **timeout 延迟 fallback**:100~200ms 为经验值,无法完全消除竞态,只能降低概率;完全消除需浏览器不支持 abort delete 的前提下接受 best-effort。
3. **类型扩展**`StorageCleanResult` 加可选 `count` 为向后兼容;消费方用 `'count' in result && result.count` 判断即可。
---
## 相关文件索引
| 文件 | 说明 |
| -------------------------------------------------------- | -------------------------------------------- |
| `src/utils/storageCleaner.ts` | 核心清理逻辑 |
| `src/utils/__tests__/storageCleaner.test.ts` | 单元测试 |
| `src/types/storage.d.ts` | `StorageCleanResult` / `CleaningResult` 类型 |
| `src/pages/StorageCleaner/constants.ts` | 选项标签与键名 |
| `src/pages/StorageCleaner/useStorageCleaner.ts` | 清理流程编排 |
| `src/pages/StorageCleaner/components/CleaningResult.tsx` | 结果展示 UI |
+48
View File
@@ -0,0 +1,48 @@
# 测试数据生成器 - 功能设计文档
> 版本: v1.0
> 创建时间: 2024-01-20
> 状态: 已实现(见 `src/pages/TestDataGenerator/`、`src/lib/generators/`、`src/workers/generator.worker.ts`
## 产品定位
**轻量级可视化测试数据生成器**
- 纯前端工具,无需后端
- 可视化配置,无需编码
- 本地生成,数据安全
## 目录
- [核心功能](./core-features.md)
- [生成器库](./generators.md)
- [规则管理](./rule-management.md)
- [界面设计](./ui-design.md)
- [技术实现](./technical-implementation.md)
## 当前实现入口
- 页面入口:`src/pages/TestDataGenerator/index.tsx`
- Worker`src/workers/generator.worker.ts`
- 生成器库:`src/lib/generators/`
- 规则存储:`src/utils/ruleStorage.ts`
- 导出工具:`src/utils/dataExporter.ts`
实现约束与任务完成状态记录在 [TASKS.md](./TASKS.md)。
## 开发者注意事项(与源码同步)
以下行为以 `src/` 源码为准;设计文档中的旧版 class API、`metadata`/`options` 嵌套结构已废弃。
### Worker 任务 ID`generationId`
`useGenerator` 每次调用 `generate()` 递增 `generationIdRef`,经 `start` 消息传入 Worker。Worker 所有响应(`progress` / `complete` / `error`)均携带同一 `generationId`
- **取消**`cancel()` 先递增 ID 再发送 `cancel`,使进行中的 Worker 响应被主线程忽略;Worker 每生成 100 行让出事件循环以处理 cancel。
- **快速重试**:新任务 ID 大于旧响应时,旧消息被丢弃,避免 UI 状态错乱。
类型见 `WorkerRequestMessage` / `WorkerResponseMessage``src/types/testDataGenerator.ts`)。
### 规则存储写入失败
`ruleStorage.save()` / `update()``localStorage.setItem` 失败时返回 `null`(不部分提交)。页面层(如 `FieldList.tsx`)仅在返回值非空时 Toast 成功。详见 [rule-management.md § 存储机制](./rule-management.md#存储机制) 与 `src/utils/README.md`
+384
View File
@@ -0,0 +1,384 @@
# 测试数据生成器 - 实现任务列表
> 创建时间: 2026-06-06
> 状态: ✅ 已完成
## 总体进度
| 子功能 | 状态 | 预估工时 |
| -------------------------- | --------- | --------- |
| 1. 类型定义与项目配置 | ✅ 已完成 | 0.5h |
| 2. 生成器库 | ✅ 已完成 | 4h |
| 3. Web Worker 数据生成引擎 | ✅ 已完成 | 2h |
| 4. 规则存储与管理 | ✅ 已完成 | 2h |
| 5. 数据导出功能 | ✅ 已完成 | 1h |
| 6. UI 组件开发 | ✅ 已完成 | 6h |
| 7. 页面集成与注册 | ✅ 已完成 | 1h |
| 8. 测试与优化 | ✅ 已完成 | 2h |
| **总计** | | **18.5h** |
---
## 1. 类型定义与项目配置
**目标**: 创建所有必要的类型定义文件,配置项目注册新功能
### 任务清单
- [x] 1.1 创建 `src/types/testDataGenerator.ts` 类型定义文件
- [x] 定义 `FieldConfig` 接口
- [x] 定义 `DataRule` 接口
- [x] 定义 `GeneratorDefinition` 接口
- [x] 定义 `GeneratorParam` 接口
- [x] 定义 `GenerateResult` 接口
- [x] 定义 `ExportFile` 接口
- [x] 1.2 更新 `src/types/storage.d.ts`
- [x]`PageType` 中添加 `'testDataGenerator'`
- [x]`StorageSchema` 中添加测试数据生成器相关配置(如需要)
- [x] 1.3 更新 `src/config/features.tsx`
- [x] 导入 TestDataGenerator 页面组件
- [x] 添加新功能配置到 `FEATURES` 数组
- [x] 选择合适的图标(如 `Database``FileSpreadsheet`
- [x] 1.4 更新国际化配置
- [x] 添加 `testDataGenerator_title` 翻译
- [x] 添加 `testDataGenerator_description` 翻译
---
## 2. 生成器库
**目标**: 实现 21 个数据生成器,支持多种生成策略
### 任务清单
#### 2.1 生成器基础框架
- [x] 2.1.1 创建 `src/lib/generators/types.ts`
- [x] 定义生成器内部类型(与 `testDataGenerator.ts` 分离)
- [x] 2.1.2 创建 `src/lib/generators/index.ts`
- [x] 导出所有生成器
- [x] 导出 `generatorCategories` 配置
#### 2.2 个人信息生成器 (6个)
- [x] 2.2.1 创建 `src/lib/generators/personal.ts`
- [x] 实现 `chineseName` 生成器
- [x] `generate()` 方法
- [x] `generateAtIndex()` 方法
- [x] 实现 `email` 生成器
- [x] `generate()` 方法
- [x] `generateAtIndex()` 方法
- [x] 实现 `chinesePhone` 生成器
- [x] `generate()` 方法
- [x] `generateAtIndex()` 方法
- [x] 实现 `idCard` 生成器
- [x] `generate()` 方法
- [x] `generateAtIndex()` 方法
- [x] 实现 `chineseAddress` 生成器
- [x] `generate()` 方法
- [x] `generateAtIndex()` 方法
- [x] 实现 `age` 生成器
- [x] `generate()` 方法(支持 realistic/uniform/demographic 策略)
- [x] `generateAtIndex()` 方法
- [x] 实现 `normalRandom()` 辅助函数
- [x] 实现 `demographicRandom()` 辅助函数
#### 2.3 业务数据生成器 (8个)
- [x] 2.3.1 创建 `src/lib/generators/business.ts`
- [x] 实现 `orderId` 生成器
- [x] 实现 `price` 生成器
- [x] 实现 `generatePsychologicalPrice()` 辅助函数
- [x] 实现 `generateRealisticPrice()` 辅助函数
- [x] 实现 `date` 生成器
- [x] 实现 `formatDate()` 辅助函数
- [x] 实现 `status` 生成器
- [x] 实现 `quantity` 生成器
- [x] 实现 `poissonRandom()` 辅助函数
- [x] 实现 `rating` 生成器
- [x] 实现 `skewedRandom()` 辅助函数
- [x] 实现 `discount` 生成器
- [x] 实现 `generatePsychologicalDiscount()` 辅助函数
- [x] 实现 `stock` 生成器
- [x] 实现 `exponentialRandom()` 辅助函数
#### 2.4 技术数据生成器 (3个)
- [x] 2.4.1 创建 `src/lib/generators/technical.ts`
- [x] 实现 `uuid` 生成器
- [x] 实现 `ipv4` 生成器
- [x] 实现 `url` 生成器
#### 2.5 基础类型生成器 (4个)
- [x] 2.5.1 创建 `src/lib/generators/basic.ts`
- [x] 实现 `randomInt` 生成器
- [x] 实现 `randomFloat` 生成器
- [x] 实现 `randomString` 生成器
- [x] 实现 `fromList` 生成器
---
## 3. Web Worker 数据生成引擎
**目标**: 实现后台数据生成,支持进度回调和取消
### 任务清单
- [x] 3.1 创建 `src/workers/generator.worker.ts`
- [x] 实现 `self.onmessage` 处理器
- [x] 验证生成器是否存在
- [x] 实现数据生成循环
- [x] 实现空值率控制逻辑
- [x] 实现唯一性约束逻辑
- [x] 小数据量:随机生成 + 重试(100次上限)
- [x] 大数据量:索引生成策略
- [x] 实现进度回调(每1000条)
- [x] 实现完成回调(包含统计数据)
- [x] 实现错误回调
- [x] 3.2 创建 `src/pages/TestDataGenerator/hooks/useGenerator.ts`
- [x] 实现 Worker 创建和销毁
- [x] 实现 `generate()` 方法
- [x] 实现 `terminate()` 方法
- [x] 管理进度状态
- [x] 管理生成结果状态
---
## 4. 规则存储与管理
**目标**: 实现规则的持久化存储和管理功能
### 任务清单
- [x] 4.1 创建 `src/utils/ruleStorage.ts`
- [x] 定义 `STORAGE_KEY` 常量
- [x] 定义 `MAX_RULES = 20` 常量
- [x] 实现 `getAll()` 方法
- [x] 实现 `getById()` 方法
- [x] 实现 `getCount()` 方法
- [x] 实现 `isMaxReached()` 方法
- [x] 实现 `save()` 方法(含数量限制检查)
- [x] 实现 `update()` 方法
- [x] 实现 `delete()` 方法
- [x] 实现 `duplicate()` 方法
- [x] 实现 `recordUse()` 方法
- [x] 实现 `search()` 方法
- [x] 实现 `getRecent()` 方法
- [x] 实现 `export()` 方法
- [x] 实现 `import()` 方法(含格式验证)
- [x] 实现 `clear()` 方法
- [x] 实现 `validateRule()` 私有方法
---
## 5. 数据导出功能
**目标**: 实现 JSON 和 CSV 格式的数据导出
### 任务清单
- [x] 5.1 创建 `src/utils/dataExporter.ts`
- [x] 实现 `toJSON()` 静态方法
- [x] 实现 `toCSV()` 静态方法
- [x] 实现 `escapeCSV()` 私有方法
- [x] 实现 `exportByFormat()` 静态方法
- [x] 实现 `toMultipleFiles()` 静态方法
- [x] 实现 `toSingleFile()` 静态方法
- [x] 实现 `getMimeType()` 私有方法
- [x] 实现 `download()` 静态方法
- [x] 实现 `downloadMultiple()` 静态方法
---
## 6. UI 组件开发
**目标**: 实现所有界面组件,遵循视觉规范
### 任务清单
#### 6.1 页面组件
- [x] 6.1.1 创建 `src/pages/TestDataGenerator/index.tsx` 主页面
- [x] 实现左右分栏布局(60% / 40%)
- [x] 集成所有子组件
- [x] 管理页面状态
#### 6.2 字段管理组件
- [x] 6.2.1 创建 `src/pages/TestDataGenerator/components/FieldList.tsx`
- [x] 实现字段列表展示
- [x] 实现添加字段按钮
- [x] 实现字段拖拽排序(可选,使用上移/下移按钮)
- [x] 6.2.2 创建 `src/pages/TestDataGenerator/components/FieldItem.tsx`
- [x] 实现单个字段项展示
- [x] 实现字段名编辑
- [x] 实现生成器切换下拉框
- [x] 实现配置按钮
- [x] 实现删除按钮
- [x] 实现上移/下移按钮
- [x] 6.2.3 创建 `src/pages/TestDataGenerator/components/FieldEditor.tsx`
- [x] 实现基础配置区(字段名、描述)
- [x] 实现必填/选填切换
- [x] 实现空值率配置(仅选填时显示)
- [x] 滑块组件
- [x] 预设按钮(低/中/高)
- [x] 实现唯一性约束开关
#### 6.3 生成器相关组件
- [x] 6.3.1 创建 `src/pages/TestDataGenerator/components/GeneratorSelector.tsx`
- [x] 实现分类展示(个人信息/业务数据/技术数据/基础类型)
- [x] 实现搜索功能
- [x] 实现生成器选择
- [x] 6.3.2 创建 `src/pages/TestDataGenerator/components/GeneratorConfig.tsx`
- [x] 实现参数配置表单
- [x] 支持不同参数类型(string/number/boolean/select/array
- [x] 实时预览更新
#### 6.4 数据预览组件
- [x] 6.4.1 创建 `src/pages/TestDataGenerator/components/DataPreview.tsx`
- [x] 实现 JSON 格式预览
- [x] 实现 CSV 格式预览
- [x] 实现格式切换按钮
- [x] 实现数据分页
- [x] 实现复制功能
#### 6.5 生成选项组件
- [x] 6.5.1 创建 `src/pages/TestDataGenerator/components/GenerateOptions.tsx`
- [x] 实现生成数量配置(预设 + 自定义)
- [x] 实现数据格式选择(JSON/CSV)
- [x] 实现默认空值率配置
- [x] 6.5.2 创建 `src/pages/TestDataGenerator/components/GenerateButton.tsx`
- [x] 实现生成按钮
- [x] 实现加载状态
- [x] 实现进度条显示
#### 6.6 导出组件
- [x] 6.6.1 创建 `src/pages/TestDataGenerator/components/ExportPanel.tsx`
- [x] 实现导出选项(复制/下载 JSON/下载 CSV)
- [x] 实现下载逻辑
#### 6.7 结果展示组件
- [x] 6.7.1 创建 `src/pages/TestDataGenerator/components/ResultPanel.tsx`
- [x] 实现成功状态展示
- [x] 实现警告状态展示(部分字段失败)
- [x] 实现错误状态展示
- [x] 实现数据统计展示
#### 6.8 规则管理组件
- [x] 6.8.1 创建 `src/pages/TestDataGenerator/components/RuleManager.tsx`
- [x] 实现规则列表展示
- [x] 实现搜索功能
- [x] 实现加载/编辑/删除/复制按钮
- [x] 实现导入/导出功能
- [x] 实现规则数量显示(已保存 X/20 条)
---
## 7. 页面集成与注册
**目标**: 将新功能集成到应用中
### 任务清单
- [x] 7.1 更新路由配置
- [x] 确保 `RouterProvider` 支持新页面类型
- [x] 7.2 更新功能注册
- [x] 验证 `features.tsx` 配置正确
- [x] 验证图标显示正常
- [x] 7.3 更新仪表盘
- [x] 在 Dashboard 中显示新工具卡片
---
## 8. 测试与优化
**目标**: 确保功能稳定性和性能
### 任务清单
#### 8.1 单元测试
- [x] 8.1.1 创建生成器测试
- [x] `src/lib/generators/__tests__/personal.test.ts`
- [x] `src/lib/generators/__tests__/business.test.ts`
- [x] `src/lib/generators/__tests__/technical.test.ts`
- [x] `src/lib/generators/__tests__/basic.test.ts`
- [x] 8.1.2 创建工具函数测试
- [x] `src/utils/__tests__/ruleStorage.test.ts`
- [x] `src/utils/__tests__/dataExporter.test.ts`
#### 8.2 集成测试
- [x] 8.2.1 创建页面测试
- [x] `src/pages/TestDataGenerator/__tests__/TestDataGenerator.test.tsx`
#### 8.3 性能优化
- [x] 8.3.1 验证 Web Worker 性能
- [x] 测试 10000+ 条数据生成
- [x] 验证 UI 不阻塞
- [x] 8.3.2 验证内存使用
- [x] 检查大数据量时的内存占用
- [x] 确保无内存泄漏
#### 8.4 代码质量
- [x] 8.4.1 代码审查
- [x] 遵循项目代码规范
- [x] 遵循视觉规范文档
- [x] 8.4.2 文档更新
- [x] 更新 AGENTS.md(如需要)
---
## 依赖关系
```
1. 类型定义与项目配置
2. 生成器库
3. Web Worker 数据生成引擎 (依赖 2)
4. 规则存储与管理
5. 数据导出功能
6. UI 组件开发 (依赖 1, 2, 3, 4, 5)
7. 页面集成与注册 (依赖 6)
8. 测试与优化 (依赖 7)
```
---
## 注意事项
1. **遵循视觉规范**: 所有 UI 组件必须遵循 `docs/VISUAL_STYLE_GUIDE.md`
2. **使用现有组件**: 优先使用 `src/components/ui/` 中的 shadcn/ui 组件
3. **TypeScript 严格模式**: 确保类型安全
4. **错误处理**: 使用 `sonner` 库进行 Toast 提示
5. **性能考虑**: 大数据量生成必须使用 Web Worker
6. **响应式设计**: 支持桌面端、平板端、移动端
+238
View File
@@ -0,0 +1,238 @@
# 核心功能
## 功能概览
| 功能模块 | 功能点 | 说明 | 优先级 |
| -------------- | ------------- | ------------------ | ------ |
| **字段配置** | 添加字段 | 可视化添加数据字段 | P0 |
| | 删除字段 | 删除不需要的字段 | P0 |
| | 字段排序 | 拖拽调整字段顺序 | P1 |
| | 字段命名 | 自定义字段名称 | P0 |
| | 必填/选填设置 | 控制字段是否必填 | P0 |
| | 空值率配置 | 选填字段空值概率 | P1 |
| **生成器系统** | 生成器选择 | 从生成器库中选择 | P0 |
| | 参数配置 | 配置生成器参数 | P0 |
| | 实时预览 | 配置时查看生成结果 | P1 |
| **数据生成** | 批量生成 | 支持 10000+ 条数据 | P0 |
| | 唯一性约束 | 保证字段值唯一 | P1 |
| | 组合策略 | 自动选择生成策略 | P1 |
| | 数据预览 | 生成前预览数据 | P1 |
| **数据导出** | JSON 导出 | 导出为 JSON 格式 | P0 |
| | CSV 导出 | 导出为 CSV 格式 | P0 |
## 功能详情
### 1. 字段配置
#### 添加字段
- 用户点击"添加字段"按钮
- 弹出生成器选择器
- 选择生成器后,自动创建新字段
- 字段默认名称为生成器名称
#### 删除字段
- 每个字段右侧有删除按钮
- 点击后弹出确认对话框
- 确认后删除该字段
#### 字段排序
- 支持拖拽排序
- 支持上移/下移按钮
- 排序后预览数据同步更新
#### 字段命名
- 字段名称可自定义
- 支持中英文命名
- 同一规则内字段名不可重复
#### 必填/选填设置
- 每个字段可设置为"必填"或"选填"
- **必填字段**: 100% 生成数据
- **选填字段**: 可配置空值概率(0-100%)
- 默认所有字段为必填
#### 空值率配置
- 选填字段可单独设置空值率
- 提供预设值:低(20%)、中(50%)、高(80%)
- 支持滑块精细调节(0-100%
- 未单独设置的选填字段使用全局默认空值率
---
### 2. 生成器系统
#### 生成器选择
- 以分类方式展示生成器
- 支持搜索生成器
- 点击选择后添加到字段配置
#### 价格生成器(业务数据)
生成真实的价格数据,支持多种策略:
| 策略 | 说明 | 适用场景 |
| -------- | ------------------------------ | ------------ |
| 真实分布 | 对数正态分布,模拟真实商品价格 | 通用电商场景 |
| 均匀随机 | 所有价格等概率 | 测试数据 |
| 心理定价 | .99/.98/.95 结尾 | 营销场景 |
**参数配置**
- 最低价/最高价:控制价格区间
- 小数位数:0(整数)、1、2 位
- 生成策略:选择分布类型
#### 年龄生成器(个人信息)
生成真实分布的年龄数据:
| 策略 | 说明 | 适用场景 |
| -------- | ---------------------------- | ------------ |
| 真实分布 | 正态分布,均值 35,标准差 10 | 通用用户数据 |
| 人口比例 | 按中国人口年龄比例分布 | 市场调研数据 |
| 均匀随机 | 所有年龄等概率 | 测试数据 |
**参数配置**
- 最小年龄/最大年龄:控制年龄区间
- 生成策略:选择分布类型
#### 数量生成器(业务数据)
生成真实分布的数量数据:
| 策略 | 说明 | 适用场景 |
| -------- | --------------------------- | -------- |
| 真实分布 | 泊松分布,大多数购买 1-3 件 | 零售订单 |
| 批发模式 | 10-100 之间均匀分布 | 批发订单 |
| 均匀随机 | 所有数量等概率 | 测试数据 |
**参数配置**
- 最小值/最大值:控制数量区间
- 生成策略:选择分布类型
#### 评分生成器(业务数据)
生成真实分布的评分数据:
| 策略 | 说明 | 适用场景 |
| -------- | ------------------ | -------- |
| 真实分布 | 偏态分布,偏向高分 | 电商平台 |
| 严格评价 | 偏态分布,偏向低分 | 严格评审 |
| 均匀随机 | 所有评分等概率 | 测试数据 |
**参数配置**
- 最低分/最高分:控制评分区间
- 小数位数:0、1 位
- 生成策略:选择分布类型
#### 折扣生成器(业务数据)
生成真实分布的折扣数据:
| 策略 | 说明 | 适用场景 |
| -------- | ------------------------ | -------- |
| 心理定价 | 常见折扣点(8折、9折等) | 电商促销 |
| 清仓模式 | 大折扣区间(3-7折) | 清仓处理 |
| 均匀随机 | 所有折扣等概率 | 测试数据 |
**参数配置**
- 最低折扣/最高折扣:控制折扣区间(0.1-0.9)
- 生成策略:选择分布类型
#### 库存生成器(业务数据)
生成真实分布的库存数据:
| 策略 | 说明 | 适用场景 |
| -------- | ------------------------ | -------- |
| 真实分布 | 指数分布,大多数库存较少 | 通用商品 |
| 热销商品 | 低库存区间(0-50) | 热销商品 |
| 均匀随机 | 所有库存等概率 | 测试数据 |
**参数配置**
- 最小库存/最大库存:控制库存区间
- 生成策略:选择分布类型
#### 参数配置
- 每个生成器有对应的参数配置界面
- 必填参数标记星号
- 修改参数后实时预览更新
#### 实时预览
- 配置参数时,右侧预览区实时更新
- 显示 5-10 条示例数据
- 帮助用户确认配置是否正确
---
### 3. 数据生成
#### 批量生成
- 支持配置生成数量
- 使用 Web Worker 后台生成
- 支持 10000+ 条数据
- 生成过程中显示进度条
#### 唯一性约束
- 每个字段可单独设置唯一性约束
- 启用后,该字段生成的值不会重复
- 如果无法生成唯一值,使用重试机制
#### 组合策略(唯一性生成)
系统根据数据量自动选择最优生成策略:
| 数据量 | 策略 | 说明 |
| --------- | --------------- | ---------------------- |
| ≤ 1000 条 | 随机生成 + 重试 | 保证随机性,性能可接受 |
| > 1000 条 | 索引生成 | 高性能,保证唯一性 |
**实现方式**
- 生成器提供两个方法:`generate()``generateAtIndex()`
- `generate()`: 随机生成,用于小数据量或非唯一性字段
- `generateAtIndex()`: 索引生成,用于大数据量唯一性字段
- 系统根据配置自动选择合适的方法
**示例**
- 手机号字段:`generateAtIndex(params, index)` 使用 index 生成唯一手机号
- 姓名字段:`generateAtIndex(params, index)` 使用 index 组合姓和名
#### 数据预览
- 生成前可预览数据
- 显示前 10 条数据
- 支持分页查看更多数据
---
### 4. 数据导出
#### JSON 导出
- 导出为标准 JSON 格式
- 支持压缩和格式化两种模式
- 包含字段名和数据
#### CSV 导出
- 导出为 CSV 格式
- 自动处理逗号转义
- 支持中文编码 (UTF-8 with BOM)
+702
View File
@@ -0,0 +1,702 @@
# 生成器库
## 分类概览
| 分类 | 生成器数量 | 说明 |
| -------- | ---------- | ------------------------------------------------ |
| 个人信息 | 6 个 | 中文姓名、邮箱、手机号、年龄等 |
| 业务数据 | 8 个 | 订单号、价格、日期、状态、数量、评分、折扣、库存 |
| 技术数据 | 3 个 | UUID、IP地址、URL |
| 基础类型 | 4 个 | 整数、浮点数、字符串等 |
| **总计** | **21 个** | |
---
## 个人信息生成器
### 1. 中文姓名 (chineseName)
**说明**: 生成中文姓名,如张三、李四
**参数**:
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
| --------------- | ---------------------------- | ---- | ------------------ | ------------ |
| surnamePool | string \| string[] | 否 | 百家姓前100个 | 姓氏池 |
| givenNameLength | { min: number, max: number } | 否 | { min: 1, max: 2 } | 名字长度范围 |
**示例**:
```json
{
"name": "name",
"generator": "chineseName",
"params": {
"surnamePool": ["张", "王", "李", "赵"],
"givenNameLength": { "min": 1, "max": 2 }
}
}
```
**生成结果**: 张伟, 李娜, 王强, 赵敏
---
### 2. 邮箱地址 (email)
**说明**: 生成邮箱地址
**参数**:
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
| --------- | -------------------- | ---- | ------------------------------------------------------------ | ---------- |
| domains | string[] | 否 | ['qq.com', '163.com', '126.com', 'gmail.com', 'outlook.com'] | 域名列表 |
| nameStyle | 'pinyin' \| 'random' | 否 | 'pinyin' | 用户名风格 |
**示例**:
```json
{
"name": "email",
"generator": "email",
"params": {
"domains": ["qq.com", "163.com"],
"nameStyle": "pinyin"
}
}
```
**生成结果**: zhangwei@qq.com, lina@163.com
---
### 3. 中国手机号 (chinesePhone)
**说明**: 生成 11 位中国手机号
**参数**:
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
| ------ | -------- | ---- | -------------- | ---------- |
| prefix | string[] | 否 | 常见手机号前缀 | 手机号前缀 |
**示例**:
```json
{
"name": "phone",
"generator": "chinesePhone",
"params": {
"prefix": ["138", "139", "150", "151"]
}
}
```
**生成结果**: 13812345678, 15087654321
---
### 4. 身份证号 (idCard)
**说明**: 生成 18 位中国身份证号
**参数**:
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
| ------ | ------ | ---- | ------ | ------ |
| region | string | 否 | 随机 | 地区码 |
**示例**:
```json
{
"name": "idCard",
"generator": "idCard",
"params": {
"region": "110101"
}
}
```
**生成结果**: 110101199001011234
---
### 5. 中文地址 (chineseAddress)
**说明**: 生成中国地址
**参数**:
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
| ------ | ------------------------------ | ---- | ------ | -------- |
| level | 'full' \| 'province' \| 'city' | 否 | 'full' | 详细程度 |
**示例**:
```json
{
"name": "address",
"generator": "chineseAddress",
"params": {
"level": "full"
}
}
```
**生成结果**: 北京市朝阳区建国路88号
---
### 6. 年龄 (age)
**说明**: 生成真实分布的年龄数据
**参数**:
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
| -------- | ----------------------------------------- | ---- | ----------- | -------- |
| min | number | 否 | 1 | 最小年龄 |
| max | number | 否 | 100 | 最大年龄 |
| strategy | 'realistic' \| 'demographic' \| 'uniform' | 否 | 'realistic' | 生成策略 |
**策略说明**:
| 策略 | 说明 | 适用场景 |
| ----------- | ---------------------------- | ------------ |
| realistic | 正态分布,均值 35,标准差 10 | 通用用户数据 |
| demographic | 按中国人口年龄比例分布 | 市场调研数据 |
| uniform | 均匀随机 | 测试数据 |
**示例**:
```json
{
"name": "age",
"generator": "age",
"params": {
"min": 18,
"max": 65,
"strategy": "realistic"
}
}
```
**生成结果**: 28, 42, 35, 51, 23
---
## 业务数据生成器
### 1. 订单号 (orderId)
**说明**: 生成订单号
**参数**:
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
| ------------ | ------- | ---- | ------ | ------------ |
| prefix | string | 否 | 'ORD' | 前缀 |
| dateIncluded | boolean | 否 | true | 是否包含日期 |
**示例**:
```json
{
"name": "orderId",
"generator": "orderId",
"params": {
"prefix": "ORD",
"dateIncluded": true
}
}
```
**生成结果**: ORD20240115001, ORD20240115002
---
### 2. 价格 (price)
**说明**: 生成真实的价格数据,支持多种分布策略
**参数**:
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
| -------- | ------------------------------------------- | ---- | ----------- | -------- |
| min | number | 是 | 1 | 最低价 |
| max | number | 是 | 9999 | 最高价 |
| decimals | 0 \| 1 \| 2 | 否 | 2 | 小数位数 |
| strategy | 'realistic' \| 'uniform' \| 'psychological' | 否 | 'realistic' | 生成策略 |
**生成策略说明**:
| 策略 | 说明 | 适用场景 |
| ------------- | ------------------------------ | ------------ |
| realistic | 对数正态分布,模拟真实商品价格 | 通用电商场景 |
| uniform | 均匀随机,所有价格等概率 | 测试数据 |
| psychological | 心理定价,.99/.98/.95 结尾 | 营销场景 |
**示例**:
```json
{
"name": "price",
"generator": "price",
"params": {
"min": 10,
"max": 5000,
"decimals": 2,
"strategy": "realistic"
}
}
```
**生成结果**:
- 真实分布: 299.00, 1599.50, 49.99, 1299.00
- 心理定价: 299.99, 1599.98, 49.95, 1299.99
- 均匀随机: 1234.56, 5678.90, 890.12
---
### 3. 日期时间 (date)
**说明**: 生成日期时间
**参数**:
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
| ------ | ------ | ---- | --------------------- | -------- |
| format | string | 否 | 'YYYY-MM-DD HH:mm:ss' | 日期格式 |
| min | string | 否 | '2020-01-01' | 最早日期 |
| max | string | 否 | 当前日期 | 最晚日期 |
**示例**:
```json
{
"name": "createdAt",
"generator": "date",
"params": {
"format": "YYYY-MM-DD HH:mm:ss",
"min": "2024-01-01"
}
}
```
**生成结果**: 2024-01-15 14:30:22
---
### 4. 状态 (status)
**说明**: 从选项中随机选择
**参数**:
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
| ------- | -------- | ---- | -------- | ------------ |
| options | string[] | 是 | - | 状态选项列表 |
| weights | number[] | 否 | 均匀分布 | 各选项权重 |
**示例**:
```json
{
"name": "status",
"generator": "status",
"params": {
"options": ["pending", "paid", "shipped", "completed"],
"weights": [30, 40, 20, 10]
}
}
```
**生成结果**: pending, paid, shipped, completed
---
### 5. 数量 (quantity)
**说明**: 生成真实分布的数量数据
**参数**:
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
| -------- | ---------------------------------- | ---- | ----------- | -------- |
| min | number | 否 | 1 | 最小值 |
| max | number | 否 | 100 | 最大值 |
| strategy | 'realistic' \| 'bulk' \| 'uniform' | 否 | 'realistic' | 生成策略 |
**策略说明**:
| 策略 | 说明 | 适用场景 |
| --------- | --------------------------- | -------- |
| realistic | 泊松分布,大多数购买 1-3 件 | 零售订单 |
| bulk | 10-100 之间均匀分布 | 批发订单 |
| uniform | 均匀随机 | 测试数据 |
**示例**:
```json
{
"name": "quantity",
"generator": "quantity",
"params": {
"min": 1,
"max": 50,
"strategy": "realistic"
}
}
```
**生成结果**: 2, 1, 5, 3, 1
---
### 6. 评分 (rating)
**说明**: 生成真实分布的评分数据
**参数**:
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
| -------- | ------------------------------------ | ---- | ----------- | -------- |
| min | number | 否 | 1 | 最低分 |
| max | number | 否 | 5 | 最高分 |
| decimals | number | 否 | 1 | 小数位数 |
| strategy | 'realistic' \| 'strict' \| 'uniform' | 否 | 'realistic' | 生成策略 |
**策略说明**:
| 策略 | 说明 | 适用场景 |
| --------- | ------------------ | -------- |
| realistic | 偏态分布,偏向高分 | 电商平台 |
| strict | 偏态分布,偏向低分 | 严格评审 |
| uniform | 均匀随机 | 测试数据 |
**示例**:
```json
{
"name": "rating",
"generator": "rating",
"params": {
"min": 1,
"max": 5,
"decimals": 1,
"strategy": "realistic"
}
}
```
**生成结果**: 4.5, 4.8, 3.9, 5.0, 4.2
---
### 7. 折扣 (discount)
**说明**: 生成真实分布的折扣数据
**参数**:
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
| -------- | ------------------------------------------- | ---- | --------------- | -------- |
| min | number | 否 | 0.1 | 最低折扣 |
| max | number | 否 | 0.9 | 最高折扣 |
| strategy | 'psychological' \| 'clearance' \| 'uniform' | 否 | 'psychological' | 生成策略 |
**策略说明**:
| 策略 | 说明 | 适用场景 |
| ------------- | ------------------------ | -------- |
| psychological | 常见折扣点(8折、9折等) | 电商促销 |
| clearance | 大折扣区间(3-7折) | 清仓处理 |
| uniform | 均匀随机 | 测试数据 |
**示例**:
```json
{
"name": "discount",
"generator": "discount",
"params": {
"min": 0.1,
"max": 0.9,
"strategy": "psychological"
}
}
```
**生成结果**: 0.80, 0.90, 0.85, 0.95, 0.70
---
### 8. 库存 (stock)
**说明**: 生成真实分布的库存数据
**参数**:
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
| -------- | --------------------------------- | ---- | ----------- | -------- |
| min | number | 否 | 0 | 最小库存 |
| max | number | 否 | 1000 | 最大库存 |
| strategy | 'realistic' \| 'hot' \| 'uniform' | 否 | 'realistic' | 生成策略 |
**策略说明**:
| 策略 | 说明 | 适用场景 |
| --------- | ------------------------ | -------- |
| realistic | 指数分布,大多数库存较少 | 通用商品 |
| hot | 低库存区间(0-50) | 热销商品 |
| uniform | 均匀随机 | 测试数据 |
**示例**:
```json
{
"name": "stock",
"generator": "stock",
"params": {
"min": 0,
"max": 500,
"strategy": "realistic"
}
}
```
**生成结果**: 23, 8, 156, 45, 12
---
## 技术数据生成器
### 1. UUID (uuid)
**说明**: 生成 UUID
**参数**:
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
| ------- | ------------ | ---- | ------ | --------- |
| version | 'v4' \| 'v1' | 否 | 'v4' | UUID 版本 |
**示例**:
```json
{
"name": "id",
"generator": "uuid",
"params": {
"version": "v4"
}
}
```
**生成结果**: 550e8400-e29b-41d4-a716-446655440000
---
### 2. IPv4 地址 (ipv4)
**说明**: 生成 IPv4 地址
**参数**:
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
| ------- | ------- | ---- | ------ | ------------ |
| private | boolean | 否 | false | 是否私有地址 |
**示例**:
```json
{
"name": "ip",
"generator": "ipv4",
"params": {
"private": true
}
}
```
**生成结果**: 192.168.1.1, 10.0.0.1
---
### 3. URL (url)
**说明**: 生成 URL
**参数**:
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
| -------- | ----------------- | ---- | ------------- | ---- |
| protocol | 'http' \| 'https' | 否 | 'https' | 协议 |
| domain | string | 否 | 'example.com' | 域名 |
**示例**:
```json
{
"name": "website",
"generator": "url",
"params": {
"protocol": "https",
"domain": "shop.com"
}
}
```
**生成结果**: https://shop.com/product/123
---
## 基础类型生成器
### 1. 随机整数 (randomInt)
**说明**: 生成指定范围的随机整数
**参数**:
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
| ------ | ------ | ---- | ------ | ------ |
| min | number | 是 | - | 最小值 |
| max | number | 是 | - | 最大值 |
**示例**:
```json
{
"name": "age",
"generator": "randomInt",
"params": {
"min": 18,
"max": 60
}
}
```
**生成结果**: 25, 42, 18, 60
---
### 2. 随机浮点数 (randomFloat)
**说明**: 生成指定范围的随机浮点数
**参数**:
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
| -------- | ------ | ---- | ------ | -------- |
| min | number | 是 | - | 最小值 |
| max | number | 是 | - | 最大值 |
| decimals | number | 否 | 2 | 小数位数 |
**示例**:
```json
{
"name": "score",
"generator": "randomFloat",
"params": {
"min": 0,
"max": 100,
"decimals": 1
}
}
```
**生成结果**: 85.3, 42.7, 99.9
---
### 3. 随机字符串 (randomString)
**说明**: 生成指定长度的随机字符串
**参数**:
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
| ------- | ------ | ---- | -------------- | ---------- |
| length | number | 是 | - | 字符串长度 |
| charset | string | 否 | 'alphanumeric' | 字符集 |
**示例**:
```json
{
"name": "code",
"generator": "randomString",
"params": {
"length": 8,
"charset": "alphanumeric"
}
}
```
**生成结果**: aB3kL9mN, xY7zW2pQ
---
### 4. 从列表选择 (fromList)
**说明**: 从选项列表中随机选择
**参数**:
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
| ------- | -------- | ---- | -------- | -------- |
| options | any[] | 是 | - | 选项列表 |
| weights | number[] | 否 | 均匀分布 | 权重 |
**示例**:
```json
{
"name": "category",
"generator": "fromList",
"params": {
"options": ["电子产品", "服装", "食品", "图书"],
"weights": [40, 30, 20, 10]
}
}
```
**生成结果**: 电子产品, 服装, 食品
---
## 生成器选择界面
### 界面设计
```
┌─────────────────────────────────────────────────────────────────────┐
│ 选择生成器 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 搜索: [搜索生成器... ] │
│ │
│ 分类筛选 │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ 全部 │ │ 个人信息 │ │ 业务数据 │ │ 技术数据 │ │ 基础类型 │ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
│ │
│ 生成器列表 │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 个人信息 │ │
│ │ ┌─────────────────────────────────────────────────────────┐ │ │
│ │ │ 中文姓名 生成中文姓名,如张三 │ │ │
│ │ │ 邮箱地址 生成邮箱地址 │ │ │
│ │ │ 中国手机号 生成11位手机号 │ │ │
│ │ │ 身份证号 生成18位身份证号 │ │ │
│ │ │ 中文地址 生成中国地址 │ │ │
│ │ └─────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
+422
View File
@@ -0,0 +1,422 @@
# 规则管理
## 功能概览
| 功能 | 说明 | 优先级 |
| -------- | -------------------- | ------ |
| 保存规则 | 将当前配置保存为模板 | P0 |
| 规则列表 | 查看所有已保存的规则 | P0 |
| 规则搜索 | 按关键词搜索规则 | P1 |
| 规则编辑 | 修改已保存的规则 | P0 |
| 规则删除 | 删除不需要的规则 | P0 |
| 规则加载 | 一键加载规则配置 | P0 |
| 规则复制 | 复制规则配置 | P1 |
| 规则导出 | 导出为 JSON 文件 | P1 |
| 规则导入 | 从 JSON 文件导入 | P1 |
## 规则数量限制
- **最大规则数量**: 20 条
- **超出限制**: 当规则数量达到 20 条时,保存按钮置灰,提示"已达到最大规则数量"
- **解决方式**: 用户需要删除已有规则后才能保存新规则
---
## 数据结构
> **与源码对齐**:类型定义见 `src/types/testDataGenerator.ts`。规则**不**持久化生成数量与导出格式(由页面 `GenerateOptions` 状态管理)。
### 规则模板
```typescript
interface DataRule {
id: string;
name: string;
description?: string;
fields: FieldConfig[];
createdAt: number;
updatedAt: number;
lastUsedAt?: number;
useCount: number;
}
```
### 字段配置
```typescript
interface FieldConfig {
id: string;
name: string;
description?: string;
generatorId: string; // 生成器 ID,对应 lib/generators 中的 id
params: Record<string, unknown>;
required: boolean;
nullRate: number; // 空值率 0-100,仅 required=false 时生效
unique: boolean;
}
```
---
## 功能详情
### 1. 保存规则
**触发方式**: 用户点击"保存规则"按钮
**界面设计**:
```
┌─────────────────────────────────────────────────────────────────────┐
│ 保存规则 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 规则信息 │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 规则名称: │ │
│ │ ┌─────────────────────────────────────────────────────────┐ │ │
│ │ │ [电商用户数据 - 测试用 ]│ │ │
│ │ └─────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ 规则描述 (可选): │ │
│ │ ┌─────────────────────────────────────────────────────────┐ │ │
│ │ │ [用于测试用户注册功能,包含姓名、邮箱、手机号 ]│ │ │
│ │ └─────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ 规则预览 │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 字段数量: 4 个 │ │
│ │ • name (中文姓名) │ │
│ │ • email (邮箱地址) │ │
│ │ • phone (手机号) │ │
│ │ • age (年龄) │ │
│ │ │ │
│ │ 生成数量: 100 条 │ │
│ │ 数据格式: JSON │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ [保存] [取消] │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
**保存逻辑**:
1. 验证规则名称不为空
2. 检查规则数量是否达到上限(20 条)
3. 若达上限,`save()` 返回 `null`UI 应阻止或提示
4. 生成唯一 ID,写入 `createdAt`/`updatedAt``useCount` 初始为 0
5. 调用 `ruleStorage.save()`;仅当返回值非 `null` 时视为成功(`localStorage` 写入失败同样返回 `null`
---
### 2. 规则列表
**界面设计**:
```
┌─────────────────────────────────────────────────────────────────────┐
│ 规则管理 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 搜索: [搜索规则名称... ] │
│ │
│ 规则列表 (已保存 15/20 条) │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────┐ │ │
│ │ │ 电商用户数据 - 测试用 │ │ │
│ │ │ ─────────────────────────────────────────────────────── │ │ │
│ │ │ 用于测试用户注册功能,包含姓名、邮箱、手机号 │ │ │
│ │ │ 4 个字段 | 100 条 | JSON │ │ │
│ │ │ 2024-01-15 14:30 创建 | 2024-01-20 10:15 最后使用 │ │ │
│ │ │ │ │ │
│ │ │ [加载] [复制] [导出] [编辑] [删除] │ │ │
│ │ └─────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ [导入规则] [导出全部] [清空全部] │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
**列表项信息**:
- 规则名称
- 规则描述
- 字段数量、生成数量、数据格式
- 创建时间、最后使用时间
- 操作按钮
---
### 3. 规则搜索
**搜索逻辑**:
- 支持按规则名称搜索
- 支持按规则描述搜索
- 搜索为模糊匹配,不区分大小写
**实现方式**`src/utils/ruleStorage.ts`:
```typescript
import * as ruleStorage from '@/utils/ruleStorage';
function searchRules(query: string): DataRule[] {
return ruleStorage.search(query);
}
```
---
### 4. 规则编辑
**界面设计**:
```
┌─────────────────────────────────────────────────────────────────────┐
│ 编辑规则 - 电商用户数据 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 基本信息 │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 规则名称: [电商用户数据 - 测试用 ]│ │
│ │ 规则描述: [用于测试用户注册功能,包含姓名、邮箱、手机号 ]│ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ 字段配置 (可直接编辑) │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1. name [中文姓名 ▼] [配置] [删除] [上移][下移] │ │
│ │ 2. email [邮箱地址 ▼] [配置] [删除] [上移][下移] │ │
│ │ 3. phone [手机号 ▼] [配置] [删除] [上移][下移] │ │
│ │ 4. age [年龄 ▼] [配置] [删除] [上移][下移] │ │
│ │ │ │
│ │ [+ 添加字段] │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ 生成选项 │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 数量: [100] 格式: [JSON ▼] │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ [保存修改] [取消] │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
**编辑逻辑**:
1. 加载原规则配置到编辑器
2. 用户修改配置
3. 点击保存时调用 `ruleStorage.update()`;返回非 `null` 才更新 `updatedAt` 并提示成功
---
### 5. 规则删除
**删除流程**:
1. 点击删除按钮
2. 弹出确认对话框
3. 确认后删除规则
4. 刷新规则列表
**确认对话框**:
```
┌─────────────────────────────────────────┐
│ 确认删除 │
├─────────────────────────────────────────┤
│ │
│ 确定要删除规则 "电商用户数据" 吗? │
│ │
│ 此操作不可撤销。 │
│ │
│ [取消] [删除] │
│ │
└─────────────────────────────────────────┘
```
---
### 6. 规则加载
**加载流程**:
1. 点击"加载"按钮
2. 将规则配置应用到当前编辑器
3. 更新预览数据
4. 记录使用时间和次数
**实现方式**:
```typescript
function loadRule(ruleId: string): void {
const rule = ruleStorage.getById(ruleId);
if (!rule) return;
setFields(rule.fields);
ruleStorage.recordUse(ruleId);
}
```
---
### 7. 规则复制
**复制逻辑**:
1. 点击"复制"按钮
2. 创建规则的副本
3. 名称添加「(副本)」后缀(默认 `duplicate(id, '(副本)')`
4. 生成新的 ID
5. 保存为新规则
---
### 8. 规则导出
**导出界面**:
```
┌─────────────────────────────────────────────────────────────────────┐
│ 导出规则 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 选择要导出的规则: │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ ☑ 电商用户数据 - 测试用 (4个字段, 100条) │ │
│ │ ☑ 电商订单数据 - 测试用 (6个字段, 500条) │ │
│ │ ☐ 用户登录数据 - 测试用 (3个字段, 50条) │ │
│ │ │ │
│ │ [全选] [全不选] │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ 导出格式: │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ ○ 导出为单个 JSON 文件 (所有规则合并) │ │
│ │ ● 导出为多个 JSON 文件 (每个规则一个文件) │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ [导出] [取消] │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
**导出格式**`exportRules()` 返回 `DataRule[]` 的 JSON 字符串,无外层包装):
```json
[
{
"id": "rule_123456",
"name": "电商用户数据 - 测试用",
"description": "用于测试用户注册功能",
"fields": [],
"createdAt": 1704067200000,
"updatedAt": 1704067200000,
"useCount": 0
}
]
```
---
### 9. 规则导入
**导入界面**:
```
┌─────────────────────────────────────────────────────────────────────┐
│ 导入规则 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 选择导入方式: │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ○ 从文件导入 │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ 拖拽文件到这里,或 [点击选择文件] │ │ │
│ │ │ 支持格式: .json │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ○ 从剪贴板粘贴 │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ 粘贴 JSON 规则内容... │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ 预览导入内容: │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 格式验证通过 │ │
│ │ 规则数量: 2 个 │ │
│ │ • 电商用户数据 (4个字段) │ │
│ │ • 电商订单数据 (6个字段) │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ [导入] [取消] │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
**导入逻辑**:
1. 解析 JSON 文件
2. 验证格式是否正确
3. 预览导入内容
4. 确认后逐条导入
5. 生成新 ID 避免冲突
6. 处理导入结果(成功/失败数量)
---
## 存储机制
### 本地存储
使用 `localStorage`,键名 `testDataGenerator_rules`。API 为**命名导出函数**(见 `src/utils/ruleStorage.ts`):
| 函数 | 说明 |
| ---- | ---- |
| `getAll()` / `getById()` / `getByName()` | 读取 |
| `save()` / `update()` / `deleteRule()` / `duplicate()` | 写入;失败时返回 `null``false` |
| `recordUse()` | 递增 `useCount`、更新 `lastUsedAt` |
| `search()` / `getRecent()` | 搜索与最近使用 |
| `exportRules()` / `importRules()` | 导入导出 JSON 数组 |
| `clear()` | 清空全部规则 |
写入失败(如 `QuotaExceededError`)时,内部 `setAll()` 返回 `false``save`/`update` 返回 `null``deleteRule` 返回 `false`,并在控制台输出 `[ruleStorage] 保存规则失败`。调用方须检查返回值,避免误报成功。
```typescript
import * as ruleStorage from '@/utils/ruleStorage';
const saved = ruleStorage.save({ name: '示例', fields });
if (!saved) {
// 达上限或 localStorage 不可用
}
```
### 存储限制
- **规则数量限制**: 最多 20 条
- localStorage 容量: 5-10MB(浏览器限制)
- 单个规则约 1-5KB
- 预计可存储 1000-5000 个规则(但受数量限制,最多 20 条)
---
## 功能亮点
| 亮点 | 说明 |
| -------- | ------------------------------ |
| 本地存储 | 数据保存在浏览器本地,无需后端 |
| 快速复用 | 一键加载已保存的规则 |
| 导入导出 | 支持 JSON 文件导入导出 |
| 使用统计 | 记录使用次数和时间 |
| 搜索筛选 | 支持关键词搜索 |
File diff suppressed because it is too large Load Diff
+489
View File
@@ -0,0 +1,489 @@
# 界面设计
## 设计原则
1. **简洁直观** - 一目了然的布局
2. **操作便捷** - 减少点击次数
3. **实时反馈** - 配置即预览
4. **响应式** - 支持多种屏幕尺寸
---
## 页面结构
### 主页面布局
```
┌─────────────────────────────────────────────────────────────────────┐
│ 🧪 测试数据生成器 [帮助] │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────┐ ┌───────────────────────┐ │
│ │ 字段配置 │ │ 数据预览 │ │
│ │ │ │ │ │
│ │ [+ 添加字段] │ │ [实时预览数据] │ │
│ │ │ │ │ │
│ │ 字段列表... │ │ │ │
│ │ │ │ │ │
│ └─────────────────────────────────────┘ └───────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 生成选项 │ │
│ │ 数量、格式、约束... │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 操作栏 │ │
│ │ [生成] [保存规则] [规则管理] │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
---
## 组件设计
### 1. 字段列表组件
**功能**: 展示和管理所有字段
**界面**:
```
┌─────────────────────────────────────────────────────────────────────┐
│ 字段配置 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ [+ 添加字段] │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 1. name [中文姓名 ▼] [配置] [删除] [上移][下移] │ │
│ │ 2. email [邮箱地址 ▼] [配置] [删除] [上移][下移] │ │
│ │ 3. phone [手机号 ▼] [配置] [删除] [上移][下移] │ │
│ │ 4. age [年龄 ▼] [配置] [删除] [上移][下移] │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ [+ 添加字段] │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
**交互**:
- 点击字段名可编辑
- 点击生成器下拉框可切换生成器
- 点击 配置 展开参数配置
- 点击 删除 删除字段
- 点击 上移下移 调整顺序
---
### 2. 字段配置组件
**功能**: 配置单个字段的详细参数
**界面**:
```
┌─────────────────────────────────────────────────────────────────────┐
│ 字段配置 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 基础配置 │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 字段名称: [name ] │ │
│ │ 字段描述: [用户姓名 ] │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ 生成器选择 │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 分类: [个人信息 ▼] │ │
│ │ 生成器: [中文姓名 ▼] │ │
│ │ 说明: 生成中文姓名,如张三、李四 │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ 参数配置 │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 姓氏池: [百家姓前100 ▼] │ │
│ │ 名字长度: [1] ~ [2] │ │
│ │ ☑ 唯一性约束 │ │
│ │ ☑ 必填 │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ 空值率设置 (仅选填字段显示) │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 空值概率: [====|----] 50% │ │
│ │ 预设: [低 20%] [中 50%] [高 80%] │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ 预览示例 │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 张伟, 李娜, 王强, 赵敏, 刘洋 │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ [保存] [取消] │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
**交互说明**:
- **必填复选框**: 勾选后该字段必填,100% 生成数据
- **空值率设置**: 仅当选填时显示,控制该字段生成空值的概率
- **预设按钮**: 快速设置常用空值率(低/中/高)
- **空值率滑块**: 精细调节空值概率(0-100%)
---
### 3. 数据预览组件
**功能**: 实时预览生成的数据
**界面**:
```
┌─────────────────────────────────────────────────────────────────────┐
│ 数据预览 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 预览: 前 10 条 │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ { │ │
│ │ "name": "张伟", │ │
│ │ "email": "zhangwei@qq.com", │ │
│ │ "phone": "13812345678", │ │
│ │ "age": 28 │ │
│ │ }, │ │
│ │ { │ │
│ │ "name": "李娜", │ │
│ │ "email": "lina@163.com", │ │
│ │ "phone": "13987654321", │ │
│ │ "age": 35 │ │
│ │ }, │ │
│ │ ... │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ 格式切换: │
│ [JSON] [CSV] │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
**功能**:
- 实时更新预览
- 支持格式切换
- 分页显示更多数据
- 一键复制
---
### 4. 生成选项组件
**功能**: 配置生成参数
**界面**:
```
┌─────────────────────────────────────────────────────────────────────┐
│ 生成选项 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌───────────────────────┐ │
│ │ 生成数量 │ │ 数据格式 │ │ 约束条件 │ │
│ │ │ │ │ │ │ │
│ │ [100 ] │ │ ○ JSON │ │ ☑ name 唯一 │ │
│ │ [10 ▼] │ │ ○ CSV │ │ ☑ email 唯一 │ │
│ │ [100 ▼] │ │ │ │ ☑ phone 唯一 │ │
│ │ [自定义 ] │ │ │ │ ☐ age 唯一 │ │
│ │ │ │ │ │ │ │
│ └─────────────────┘ └─────────────────┘ └───────────────────────┘ │
│ │
│ 默认空值率 (选填字段) │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 空值概率: [====|----] 50% │ │
│ │ 预设: [低 20%] [中 50%] [高 80%] │ │
│ │ 说明: 未单独设置空值率的选填字段将使用此默认值 │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
---
### 5. 操作栏组件
**功能**: 顶部操作按钮
**界面**:
```
┌─────────────────────────────────────────────────────────────────────┐
│ 操作栏 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ [生成数据] [保存规则] [规则管理] [导入规则] [导出]│
│ │
└─────────────────────────────────────────────────────────────────────┘
```
---
### 6. 生成结果组件
**功能**: 展示生成结果,包含成功、警告、错误三种状态
**成功状态界面**:
```
┌─────────────────────────────────────────────────────────────────────┐
│ 生成结果 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 生成完成 │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ [成功图标] 成功生成 100 条数据,耗时 1234ms │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ 数据统计: │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ • 总条数: 100 │ │
│ │ • 成功条数: 100 │ │
│ │ • 失败条数: 0 │ │
│ │ • 文件大小: 12.5 KB │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ 数据预览: │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ [JSON 格式预览...] │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ 导出选项: │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ [复制全部] [下载 JSON] [下载 CSV] │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ [关闭] │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
**警告状态界面(部分字段失败)**:
```
┌─────────────────────────────────────────────────────────────────────┐
│ 生成结果 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 部分字段生成失败 │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ [警告图标] 有 2 个字段生成失败 │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ 失败详情: │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ [警告图标] "phone" - 重试 100 次后仍存在重复 │ │
│ │ [警告图标] "email" - 重试 100 次后仍存在重复 │ │
│ │ │ │
│ │ 提示: 这些字段的值已设置为空,您可以尝试减少生成数量 │ │
│ │ 或调整字段配置 │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ 数据统计: │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ • 总条数: 100 │ │
│ │ • 成功条数: 98 │ │
│ │ • 失败条数: 2 │ │
│ │ • 文件大小: 12.3 KB │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ 数据预览: │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ [JSON 格式预览...] │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ 导出选项: │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ [复制全部] [下载 JSON] [下载 CSV] │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ [关闭] │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
**错误状态界面(生成失败)**:
```
┌─────────────────────────────────────────────────────────────────────┐
│ 生成失败 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ [错误图标] 生成过程中发生错误 │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ 错误详情: │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 错误类型: 生成器不存在 │ │
│ │ 错误信息: 字段 "phone" 的生成器 "chinesePhone1" 不存在 │ │
│ │ 错误位置: 字段配置第 2 行 │ │
│ │ │ │
│ │ 建议操作: │ │
│ │ 1. 检查字段配置中的生成器名称是否正确 │ │
│ │ 2. 确认该生成器已正确注册 │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ [重新配置] [关闭] │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
---
### 6.1 Toast 提示组件
**功能**: 显示临时提示信息
**成功提示**:
```
┌─────────────────────────────────────┐
│ [成功图标] 数据生成成功 │
└─────────────────────────────────────┘
```
**警告提示**:
```
┌─────────────────────────────────────────────────────────────┐
│ [警告图标] 有 2 个字段生成失败:phone、email │
└─────────────────────────────────────────────────────────────┘
```
**错误提示**:
```
┌─────────────────────────────────────────────────────────────┐
│ [错误图标] 生成失败:生成器 "xxx" 不存在 │
└─────────────────────────────────────────────────────────────┘
```
---
### 6.2 Modal 提示组件
**功能**: 显示严重错误的模态框
**错误模态框**:
```
┌─────────────────────────────────────────────────────────────┐
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ [错误图标] 生成失败 │ │
│ │ │ │
│ │ 字段 "phone" 的生成器 "chinesePhone1" 不存在 │ │
│ │ │ │
│ │ 请检查字段配置中的生成器名称是否正确 │ │
│ │ │ │
│ │ ─────────────────────────────────────────────────── │ │
│ │ │ │
│ │ [ 确定 ] │ │
│ │ │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
```
---
### 7. 规则管理组件
**功能**: 管理已保存的规则
**界面**:
```
┌─────────────────────────────────────────────────────────────────────┐
│ 规则管理 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 搜索: [搜索规则名称... ] │
│ │
│ 规则列表: │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 电商用户数据 - 测试用 │ │
│ │ 用于测试用户注册功能 │ │
│ │ 4 个字段 | 100 条 | JSON │ │
│ │ 2024-01-15 创建 | 使用 12 次 │ │
│ │ [加载] [编辑] [导出] [删除] │ │
│ │ │ │
│ │ 电商订单数据 - 测试用 │ │
│ │ 用于测试订单功能 │ │
│ │ 6 个字段 | 500 条 | JSON │ │
│ │ 2024-01-16 创建 | 使用 8 次 │ │
│ │ [加载] [编辑] [导出] [删除] │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ [导入规则] [导出全部] │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
---
## 响应式设计
### 桌面端 (>1024px)
- 左右分栏布局
- 字段配置区占 60%
- 数据预览区占 40%
---
## 交互细节
### 拖拽排序
- 字段支持拖拽排序(基于 @dnd-kit/sortable
- 拖拽手柄(GripVertical 图标)位于字段左侧
- 拖拽时显示半透明效果和阴影
- 释放后立即生效,自动更新字段顺序
### 实时预览
- 配置参数时实时更新预览
- 延迟 300ms 防抖,避免频繁重渲染
- 预览最多显示 10 条示例数据
- 生成完成后切换为完整结果预览
### 虚拟列表
- 数据量超过 100 条时自动启用虚拟滚动
- 仅渲染可见区域的行,大幅减少 DOM 节点
- 行高固定 20px,支持快速滚动
---
## 主题支持
### 浅色主题
- 背景色: #ffffff
- 文字色: #333333
- 边框色: #e0e0e0
- 主题色: #3b82f6
### 深色主题
- 背景色: #1a1a1a
- 文字色: #ffffff
- 边框色: #404040
- 主题色: #60a5fa
+70
View File
@@ -0,0 +1,70 @@
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import reactHooks from 'eslint-plugin-react-hooks';
import reactPlugin from 'eslint-plugin-react';
import globals from 'globals';
export default tseslint.config(
{
ignores: ['dist', '.output', '.wxt', 'node_modules', 'eslint.config.ts', 'eslint.config.js'],
},
js.configs.recommended,
...tseslint.configs.recommended,
{
files: ['**/__tests__/**', '**/*.test.{ts,tsx}', '**/*.spec.{ts,tsx}', 'setupTests.ts'],
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': [
'warn',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
],
},
},
{
files: ['src/**/*.{ts,tsx}'],
ignores: ['**/__tests__/**', '**/*.test.{ts,tsx}', '**/*.spec.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2022, // 💡 升级至现代高频语法解析
globals: {
...globals.browser,
...globals.node,
},
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
plugins: {
react: reactPlugin,
'react-hooks': reactHooks,
},
settings: {
react: {
version: 'detect',
},
},
rules: {
...reactHooks.configs.recommended.rules,
...reactPlugin.configs.recommended.rules,
...reactPlugin.configs['jsx-runtime'].rules,
'react/prop-types': 'off',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [
'warn',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
],
'react/react-in-jsx-scope': 'off',
},
},
);
+12
View File
@@ -0,0 +1,12 @@
/* global process */
const isCI = process.env.CI === 'true';
export default {
'*.{ts,tsx,js,jsx,mjs}': [
'eslint --fix --no-warn-ignored',
...(isCI ? ['eslint --max-warnings=0'] : []),
'prettier --write',
],
'*.{json,css,scss,md}': ['prettier --write'],
};
+8333 -13910
View File
File diff suppressed because it is too large Load Diff
+70 -39
View File
@@ -1,45 +1,76 @@
{
"name": "chrome-extension-router-demo",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.1",
"@testing-library/user-event": "^13.5.0",
"date-fns": "^4.1.0",
"dexie": "^4.2.1",
"dexie-react-hooks": "^4.2.0",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-markdown": "^6.0.3",
"react-router-dom": "^6.30.2",
"react-scripts": "5.0.1",
"remark-gfm": "^1.0.0",
"web-vitals": "^2.1.4"
},
"name": "testing-tools",
"description": "A browser extension providing useful testing tools including timestamp conversion and storage management",
"private": false,
"version": "1.0.0",
"license": "MIT",
"type": "module",
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
"dev": "wxt",
"dev:firefox": "wxt -b firefox",
"build": "wxt build",
"build:firefox": "wxt build -b firefox",
"zip": "wxt zip",
"zip:firefox": "wxt zip -b firefox",
"postinstall": "wxt prepare",
"prepare": "husky",
"lint": "eslint . --max-warnings=0",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"typecheck": "tsc --noEmit"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@testing-library/dom": "^10.4.1",
"@vitest/coverage-v8": "^4.1.7",
"@webext-core/messaging": "^3.0.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dayjs": "^1.11.20",
"lucide-react": "^1.16.0",
"qr-scanner": "^1.4.2",
"qrious": "^4.0.2",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"sonner": "^2.0.7",
"tailwind-merge": "^3.6.0"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
"devDependencies": {
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/chrome": "^0.1.42",
"@types/react": "^19.2.15",
"@types/react-dom": "^19.2.3",
"@types/webextension-polyfill": "^0.12.5",
"@typescript-eslint/eslint-plugin": "^8.59.4",
"@typescript-eslint/parser": "^8.59.4",
"@vitejs/plugin-react": "^6.0.2",
"@wxt-dev/module-react": "^1.2.2",
"autoprefixer": "^10.5.0",
"eslint": "^9.39.2",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.1.1",
"globals": "^17.6.0",
"husky": "^9.1.7",
"jsdom": "^29.1.1",
"lint-staged": "^17.0.5",
"postcss": "^8.5.15",
"prettier": "^3.8.3",
"tailwindcss": "^3.4.19",
"terser": "^5.47.1",
"typescript": "^5.9.3",
"typescript-eslint": "^8.59.4",
"vitest": "^4.1.7",
"wxt": "^0.20.26"
}
}
+6
View File
@@ -0,0 +1,6 @@
import tailwindcss from 'tailwindcss';
import autoprefixer from 'autoprefixer';
export default {
plugins: [tailwindcss, autoprefixer],
};
+20
View File
@@ -0,0 +1,20 @@
# public/
静态资源目录,存放无需构建处理的文件,会被直接复制到输出目录。
## 文件说明
| 文件/目录 | 用途 |
| -------------- | -------------------------------- |
| `icon/` | 扩展图标,提供多种尺寸 |
| `icon/16.png` | 16×16 图标(工具栏) |
| `icon/32.png` | 32×32 图标 |
| `icon/48.png` | 48×48 图标(扩展管理页) |
| `icon/96.png` | 96×96 图标 |
| `icon/128.png` | 128×128 图标(Chrome Web Store |
## 注意事项
- 修改图标后需同步更新 `wxt.config.ts` 中的 manifest 配置
- 图标格式推荐使用 PNG,确保透明背景
- UI 文案不在此目录维护,见 `src/config/features.tsx` 与各页面组件
-25
View File
@@ -1,25 +0,0 @@
console.log('Background script loaded');
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
console.log('收到消息:', request);
if (request.action === 'copy') {
console.log('开始复制文本:', request.text);
navigator.clipboard.writeText(request.text)
.then(() => {
console.log('复制成功');
sendResponse({success: true});
})
.catch((err) => {
console.error('复制失败:', err);
sendResponse({success: false, error: err.message});
});
return true; // 保持消息端口开放以支持异步响应
}
// 对于未知的操作,也返回响应
sendResponse({success: false, error: '未知操作'});
return false;
});
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 932 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

-43
View File
@@ -1,43 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

-25
View File
@@ -1,25 +0,0 @@
{
"manifest_version": 3,
"name": "React Router Chrome Extension",
"version": "1.0",
"description": "A Chrome Extension with React Router: User List + Actions Pages",
"permissions": [
"storage",
"clipboardWrite"
],
"background": {
"service_worker": "background.js"
},
"icons": {
"16": "favicon.ico",
"48": "favicon.ico",
"128": "favicon.ico"
},
"action": {
"default_popup": "index.html"
},
"options_ui": {
"page": "index.html",
"open_in_tab": true
}
}
-3
View File
@@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
-1021
View File
File diff suppressed because it is too large Load Diff
-104
View File
@@ -1,104 +0,0 @@
import {HashRouter as Router, Routes, Route, NavLink} from 'react-router-dom';
import {useState, useEffect} from 'react';
import TimestampPage from './pages/TimestampPage';
// import ElectronicWoodenFishPage from "./pages/ElectronicWoodenFishPage";
import './App.css';
// 导航项配置数组,便于后续添加
// 为了测试折叠功能,我们添加更多导航项
const navItems = [
{path: '/', label: '时间戳', element: <TimestampPage/>},
// {path: '/dzmy', label: '电子木鱼', element: <ElectronicWoodenFishPage/>},
];
function App() {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [isMobile, setIsMobile] = useState(false);
const [visibleItems, setVisibleItems] = useState(navItems.length);
// 检测屏幕尺寸变化
useEffect(() => {
const handleResize = () => {
const width = window.innerWidth;
setIsMobile(width < 768);
// 根据屏幕宽度决定显示多少个导航项
if (width >= 768) {
setVisibleItems(navItems.length); // 大屏幕显示所有
} else if (width >= 480) {
// 中等屏幕:如果导航项超过3个,显示3个,否则显示全部
setVisibleItems(Math.min(3, navItems.length));
} else {
// 小屏幕:如果导航项超过2个,显示2个,否则显示全部
setVisibleItems(Math.min(2, navItems.length));
}
};
handleResize(); // 初始调用
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
// 计算哪些导航项应该显示,哪些应该折叠
const visibleNavItems = navItems.slice(0, visibleItems);
const collapsedNavItems = navItems.slice(visibleItems);
return (<Router>
<div className="app">
<nav className="nav">
<div className="nav-content">
<ul className={`nav-list ${isMenuOpen ? 'open' : ''}`}>
{visibleNavItems.map((item) => (
<li key={item.path}>
<NavLink
to={item.path}
className={({isActive}) => isActive ? "nav-link active" : "nav-link"}
onClick={() => isMobile && setIsMenuOpen(false)}
>
{item.label}
</NavLink>
</li>
))}
{/* 折叠的导航项 */}
{collapsedNavItems.length > 0 && (
<li className="nav-collapse-item">
<div className={`nav-collapse-content ${isMenuOpen ? 'show' : ''}`}>
{isMenuOpen && collapsedNavItems.map((item) => (
<NavLink
key={item.path}
to={item.path}
className={({isActive}) => isActive ? "nav-link active" : "nav-link"}
onClick={() => setIsMenuOpen(false)}
>
{item.label}
</NavLink>
))}
</div>
<button
className="nav-toggle"
onClick={() => setIsMenuOpen(!isMenuOpen)}
aria-label={isMenuOpen ? "收起菜单" : "展开菜单"}
>
<span className="nav-toggle-icon">{isMenuOpen ? '×' : '☰'}</span>
{collapsedNavItems.length > 0 && !isMenuOpen && (
<span className="nav-collapse-count">+{collapsedNavItems.length}</span>
)}
</button>
</li>
)}
</ul>
</div>
</nav>
<Routes>
{navItems.map((item) => (
<Route key={item.path} path={item.path} element={item.element}/>
))}
</Routes>
</div>
</Router>);
}
export default App;
-8
View File
@@ -1,8 +0,0 @@
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});
+24
View File
@@ -0,0 +1,24 @@
# src/
源码目录,存放全局样式定义。
## 文件说明
| 文件 | 用途 |
| ----------- | ----------------- |
| `index.css` | 全局 CSS 入口文件 |
## index.css
全局样式入口,包含:
- **Tailwind 指令**`@tailwind base/components/utilities`
- **shadcn/ui CSS 变量**:定义 `--background``--primary``--destructive``--card``--muted``--accent``--border``--ring` 等语义化颜色变量
- **主题色值**`:root`(亮色)和 `.dark`(暗色)两套完整的颜色定义
- **圆角变量**`--radius` 定义全局圆角大小
## 修改注意事项
- 修改 CSS 变量会影响所有使用 shadcn/ui 语义化 token 的组件
- 新增颜色变量需同时在 `:root``.dark` 中定义
- 避免在组件中硬编码颜色值,应使用 CSS 变量或 Tailwind 的语义化类名
Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

-110
View File
@@ -1,110 +0,0 @@
import {useState, useCallback} from "react";
const CopyButton = ({
text = '要复制的文本',
buttonText = '复制文本',
className = 'action-btn',
}) => {
const [btnText, setBtnText] = useState(buttonText);
const [copyStatus, setCopyStatus] = useState('');
// 重置按钮状态的函数
const resetButton = useCallback(() => {
setBtnText(buttonText);
setCopyStatus('');
}, [buttonText]);
// 复制成功的处理
const handleCopySuccess = useCallback(() => {
setCopyStatus('success');
setBtnText('复制成功!');
// 2秒后恢复
setTimeout(() => {
resetButton();
}, 2000);
}, [text, resetButton]);
// 复制失败的处理
const handleCopyError = useCallback(() => {
setCopyStatus('error');
// 2秒后恢复
setTimeout(() => {
resetButton();
}, 2000);
}, [resetButton]);
const handleCopy = async () => {
try {
console.log('开始复制:', text);
// 首先尝试直接使用navigator.clipboard(在popup页面中可能可用)
try {
console.log('尝试直接使用navigator.clipboard复制');
await navigator.clipboard.writeText(text.toString());
console.log('直接复制成功');
handleCopySuccess();
return;
} catch (directError) {
console.log('直接复制失败,尝试使用Chrome扩展API:', directError);
}
// 如果直接复制失败,尝试使用Chrome扩展API
if (chrome && chrome.runtime && chrome.runtime.sendMessage) {
console.log('使用Chrome扩展API复制');
// 使用Chrome扩展API复制
chrome.runtime.sendMessage({
action: 'copy',
text: text
}, (response) => {
console.log('收到background响应:', response, 'lastError:', chrome.runtime.lastError);
// 检查是否有运行时错误
if (chrome.runtime.lastError) {
console.error('Chrome运行时错误:', chrome.runtime.lastError.message);
handleCopyError();
return;
}
// 检查响应
if (response && response.success) {
console.log('通过background复制成功');
handleCopySuccess();
} else {
console.error('通过background复制失败:', response?.error);
handleCopyError();
}
});
} else {
console.error('没有可用的复制方法');
handleCopyError();
}
} catch (err) {
console.error('复制过程中发生错误:', err);
handleCopyError();
}
};
return (
<button
onClick={handleCopy}
className={className}
style={{
backgroundColor: copyStatus === 'success' ? '#4CAF50' :
copyStatus === 'error' ? '#f44336' : '',
color: copyStatus ? 'white' : '',
transition: 'all 0.3s ease',
padding: '8px 16px',
border: 'none',
borderRadius: '8px',
cursor: 'pointer'
}}
>
{btnText}
</button>
);
};
export default CopyButton;
+65
View File
@@ -0,0 +1,65 @@
import React, { useEffect, useRef, useState } from 'react';
import { Check, Copy } from 'lucide-react';
import { copyTextToClipboard } from '@/utils/clipboard';
import { Button, type ButtonProps } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { toast } from 'sonner';
interface CopyButtonProps extends Omit<ButtonProps, 'children' | 'onClick'> {
text: string;
tooltip?: string;
}
export const CopyButton: React.FC<CopyButtonProps> = ({
text,
tooltip,
variant = 'ghost',
size = 'icon',
className,
...props
}) => {
const [copied, setCopied] = useState(false);
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
return () => {
if (timerRef.current) clearTimeout(timerRef.current);
};
}, []);
const handleCopy = async (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
if (!text) {
toast.error('无内容可复制');
return;
}
const success = await copyTextToClipboard(text);
if (success) {
toast.success('已复制到剪贴板');
setCopied(true);
if (timerRef.current) clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => setCopied(false), 1500);
} else {
toast.error('复制失败');
}
};
return (
<Button
type="button"
onClick={handleCopy}
title={tooltip ?? '复制'}
aria-label={tooltip ?? '复制'}
variant={variant}
size={size}
className={cn(className, copied && 'text-emerald-500')}
{...props}
>
{copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
</Button>
);
};
export default CopyButton;
@@ -0,0 +1,194 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { act, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
// unmock the globally-mocked component so we test the real implementation
vi.unmock('@/components/CopyButton');
vi.mock('@/utils/clipboard', () => ({
copyTextToClipboard: vi.fn(),
}));
vi.mock('sonner', () => ({
toast: {
success: vi.fn(),
error: vi.fn(),
},
}));
import { CopyButton } from '@/components/CopyButton';
import { copyTextToClipboard } from '@/utils/clipboard';
import { toast } from 'sonner';
const mockedCopy = vi.mocked(copyTextToClipboard);
const mockedToast = vi.mocked(toast);
describe('CopyButton', () => {
beforeEach(() => {
vi.useFakeTimers({ shouldAdvanceTime: true });
vi.clearAllMocks();
});
afterEach(() => {
vi.useRealTimers();
});
it('复制成功时调用 copyTextToClipboard 并传入正确 text', async () => {
mockedCopy.mockResolvedValue(true);
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
render(<CopyButton text="hello world" />);
await user.click(screen.getByRole('button'));
expect(mockedCopy).toHaveBeenCalledWith('hello world');
expect(mockedToast.success).toHaveBeenCalled();
});
it('复制成功后图标切换为 Check1.5 秒后恢复', async () => {
mockedCopy.mockResolvedValue(true);
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
render(<CopyButton text="test" />);
// 点击后复制成功,按钮获得 emerald 样式(说明切到了 Check 状态)
await user.click(screen.getByRole('button'));
await waitFor(() => {
expect(screen.getByRole('button').className).toContain('text-emerald');
});
// 1.5 秒后样式恢复
act(() => {
vi.advanceTimersByTime(1500);
});
await waitFor(() => {
expect(screen.getByRole('button').className).not.toContain('text-emerald');
});
});
it('复制空文本时弹出 error toast', async () => {
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
render(<CopyButton text="" />);
await user.click(screen.getByRole('button'));
expect(mockedCopy).not.toHaveBeenCalled();
expect(mockedToast.error).toHaveBeenCalled();
});
it('复制失败时弹出 error toast', async () => {
mockedCopy.mockResolvedValue(false);
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
render(<CopyButton text="something" />);
await user.click(screen.getByRole('button'));
expect(mockedCopy).toHaveBeenCalledWith('something');
expect(mockedToast.error).toHaveBeenCalled();
});
// ==================== 新增测试 ====================
it('初始渲染时显示 Copy 图标且无 emerald 样式', () => {
render(<CopyButton text="initial" />);
const button = screen.getByRole('button');
expect(button.className).not.toContain('text-emerald');
// 通过 aria-label 确认按钮存在,图标由 lucide 渲染为 svg
expect(button).toHaveAttribute('aria-label');
});
it('自定义 tooltip 会覆盖默认 title 和 aria-label', () => {
render(<CopyButton text="tooltip-test" tooltip="自定义提示" />);
const button = screen.getByRole('button');
expect(button).toHaveAttribute('title', '自定义提示');
expect(button).toHaveAttribute('aria-label', '自定义提示');
});
it('className 被正确透传到按钮', () => {
render(<CopyButton text="class-test" className="my-custom-class" />);
const button = screen.getByRole('button');
expect(button.className).toContain('my-custom-class');
});
it('点击事件阻止冒泡', async () => {
mockedCopy.mockResolvedValue(true);
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
const parentClick = vi.fn();
render(
<div onClick={parentClick}>
<CopyButton text="stop-propagation" />
</div>,
);
await user.click(screen.getByRole('button'));
expect(mockedCopy).toHaveBeenCalled();
expect(parentClick).not.toHaveBeenCalled();
});
it('组件卸载时清除定时器,不触发状态更新警告', async () => {
mockedCopy.mockResolvedValue(true);
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
const { unmount } = render(<CopyButton text="unmount-test" />);
await user.click(screen.getByRole('button'));
// 在 1.5 秒超时到期前卸载组件
act(() => {
vi.advanceTimersByTime(500);
});
// 卸载不应抛出 "Can't perform a React state update on an unmounted component" 警告
expect(() => unmount()).not.toThrow();
// 前进剩余时间,确认没有异常
act(() => {
vi.advanceTimersByTime(2000);
});
});
it('快速连续点击不会创建多个重叠定时器', async () => {
mockedCopy.mockResolvedValue(true);
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
render(<CopyButton text="rapid-click" />);
const button = screen.getByRole('button');
// 快速点击 3 次
await user.click(button);
await user.click(button);
await user.click(button);
// copyTextToClipboard 应该被调用 3 次(每次点击都执行)
expect(mockedCopy).toHaveBeenCalledTimes(3);
// 但 setTimeout 相关的 clearTimeout + setTimeout 组合应正常工作
// advance 1.5 秒后,copied 状态应恢复为 false
act(() => {
vi.advanceTimersByTime(1500);
});
await waitFor(() => {
expect(button.className).not.toContain('text-emerald');
});
});
it('其他 button props 通过 ...props 透传', () => {
render(<CopyButton text="props-test" data-testid="copy-btn" disabled id="copy-button-id" />);
const button = screen.getByRole('button');
expect(button).toHaveAttribute('data-testid', 'copy-btn');
expect(button).toBeDisabled();
expect(button).toHaveAttribute('id', 'copy-button-id');
});
});
-198
View File
@@ -1,198 +0,0 @@
import {formatWithDate, formatWithZone, TimezoneOptions} from "../utils/timeUtils";
import {useState, useCallback} from "react";
/**
* 日期时间转时间戳组件
*
* 功能特性:
* 1. 将日期时间字符串转换为时间戳
* 2. 支持多种时区选择
* 3. 支持毫秒和秒单位切换
* 4. 提供输入验证和错误提示
* 5. 实时单位转换
*
* @component
* @example
* ```jsx
* <DatetimeToTimestamp />
* ```
*
* @returns {JSX.Element} 日期时间转时间戳组件
*/
// 常用时区列表
const TIME_ZONE_LIST = [
'America/New_York',
'America/Chicago',
'America/Denver',
'America/Los_Angeles',
'America/Anchorage',
'America/Honolulu',
'Europe/London',
'Europe/Paris',
'Europe/Berlin',
'Europe/Moscow',
'Asia/Tokyo',
'Asia/Shanghai',
'Asia/Hong_Kong',
'Asia/Singapore',
'Asia/Dubai',
'Asia/Kolkata',
'Australia/Sydney',
'Pacific/Auckland',
];
// 时间戳单位选项
const TIMESTAMP_UNITS = [
{value: 'milliseconds', label: '毫秒(ms)'},
{value: 'seconds', label: '秒(s)'},
];
export function DatetimeToTimestamp() {
/** @type {[string, function]} 输入的日期时间字符串 */
const [dateValue, setDateValue] = useState(() => formatWithZone(Date.now()));
/** @type {[string, function]} 选择的时区 */
const [selectedZone, setSelectedZone] = useState('Asia/Shanghai');
/** @type {[string, function]} 转换结果 */
const [result, setResult] = useState('');
/** @type {[string, function]} 时间戳单位 ('milliseconds' | 'seconds') */
const [unit, setUnit] = useState('milliseconds');
/** @type {[string, function]} 错误信息 */
const [error, setError] = useState('');
/**
* 转换日期时间为时间戳
* @type {function(): void}
*/
const handleConvertDatetimeToTimestamp = useCallback(() => {
try {
setError('');
const timestamp = formatWithDate(dateValue, selectedZone);
if (isNaN(timestamp)) {
setError('无效的日期时间格式');
setResult('');
return;
}
const finalResult = unit === 'milliseconds'
? timestamp
: Math.floor(timestamp / 1000);
setResult(finalResult.toString());
} catch (err) {
setError('转换失败,请检查输入格式');
setResult('');
}
}, [dateValue, selectedZone, unit]);
/**
* 处理日期时间输入变化
* @type {function(React.ChangeEvent<HTMLInputElement>): void}
*/
const handleDateChange = useCallback((e) => {
setDateValue(e.target.value);
setError(''); // 清除错误信息
}, []);
/**
* 处理时区选择变化
* @type {function(React.ChangeEvent<HTMLSelectElement>): void}
*/
const handleZoneChange = useCallback((e) => {
setSelectedZone(e.target.value);
}, []);
/**
* 处理时间戳单位变化
* @type {function(React.ChangeEvent<HTMLSelectElement>): void}
*/
const handleUnitChange = useCallback((e) => {
const newUnit = e.target.value;
setUnit(newUnit);
// 如果已有结果,重新计算
if (result) {
const currentResult = parseInt(result, 10);
if (!isNaN(currentResult)) {
const newResult = newUnit === 'milliseconds'
? currentResult * 1000
: Math.floor(currentResult / 1000);
setResult(newResult.toString());
}
}
}, [result]);
return (
<div className="datetime-converter">
<h2 className="converter-title">日期时间转时间戳</h2>
<div className="converter-form">
<div className="input-group">
<input
type="text"
placeholder="输入日期时间 (如: 2024-01-01 12:00:00)"
value={dateValue}
className="datetime-input"
onChange={handleDateChange}
aria-label="输入要转换的日期时间"
title="支持格式: YYYY-MM-DD HH:mm:ss"
/>
<select
value={selectedZone}
className="timezone-select"
onChange={handleZoneChange}
aria-label="选择时区"
>
<TimezoneOptions zones={TIME_ZONE_LIST}/>
</select>
</div>
{error && (
<div className="error-message" role="alert">
{error}
</div>
)}
<div className="action-group">
<button
className="converter-btn action-btn"
onClick={handleConvertDatetimeToTimestamp}
aria-label="转换日期时间为时间戳"
>
转换
</button>
</div>
<div className="result-group">
<input
type="text"
placeholder="转换结果"
value={result}
className="result-input"
readOnly
aria-label="转换结果"
/>
<select
value={unit}
className="unit-select"
onChange={handleUnitChange}
aria-label="选择时间戳单位"
>
{TIMESTAMP_UNITS.map(({value, label}) => (
<option key={value} value={value}>
{label}
</option>
))}
</select>
</div>
</div>
</div>
);
}
+30
View File
@@ -0,0 +1,30 @@
import React from 'react';
import { cn } from '@/lib/utils';
export interface EmptyPlaceholderProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
messageClassName?: string;
}
const CONTAINER_CLASSES =
'rounded-xl bg-muted/30 border border-dashed border-border/80 text-center flex flex-col items-center justify-center select-none p-8 min-h-[120px]';
const MESSAGE_CLASSES =
'text-xs font-semibold text-muted-foreground/80 tracking-wide max-w-[240px] leading-relaxed';
export default function EmptyPlaceholder({
children,
className,
messageClassName,
...props
}: EmptyPlaceholderProps) {
return (
<div className={cn(CONTAINER_CLASSES, className)} {...props}>
{typeof children === 'string' || typeof children === 'number' ? (
<p className={cn(MESSAGE_CLASSES, messageClassName)}>{children}</p>
) : (
children
)}
</div>
);
}
+56
View File
@@ -0,0 +1,56 @@
import { Component, ErrorInfo, ReactNode } from 'react';
import { ErrorFallback } from '@/components/ErrorFallback';
interface Props {
children: ReactNode;
}
interface State {
hasError: boolean;
error: Error | null;
}
class ErrorBoundary extends Component<Props, State> {
state: State = {
hasError: false,
error: null,
};
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Uncaught error:', error, errorInfo);
}
componentDidUpdate(prevProps: Props) {
if (this.state.hasError && prevProps.children !== this.props.children) {
this.setState({ hasError: false, error: null });
}
}
private handleReset = () => {
window.location.reload();
};
render() {
if (this.state.hasError) {
return (
<ErrorFallback
variant="app"
title="糟糕,出了点问题"
description="应用遇到了一些意外错误。您可以尝试刷新页面或重置应用。"
error={this.state.error}
actionLabel="刷新应用"
onAction={this.handleReset}
/>
);
}
return this.props.children;
}
}
export { ErrorBoundary };
export default ErrorBoundary;
+96
View File
@@ -0,0 +1,96 @@
import { AlertCircle, RefreshCw } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
export interface ErrorFallbackProps {
title: string;
description: string;
error: Error | null;
actionLabel: string;
onAction: () => void;
variant?: 'app' | 'page';
showStack?: boolean;
className?: string;
}
export function ErrorFallback({
title,
description,
error,
actionLabel,
onAction,
variant = 'page',
showStack = false,
className,
}: ErrorFallbackProps) {
const isApp = variant === 'app';
const errorText = error ? (showStack ? error.stack || error.toString() : error.toString()) : null;
return (
<div
className={cn(
'flex flex-col items-center justify-center',
isApp ? 'mt-16 mx-auto max-w-md' : 'flex-1 p-6 min-h-[300px] fade-in-zoom-95',
className,
)}
>
<div
className={cn(
'p-6 text-center rounded-xl border border-destructive/20 bg-destructive/5 shadow-sm',
!isApp && 'max-w-md w-full',
)}
>
<div
className={cn(
'flex items-center justify-center rounded-full bg-destructive/10 text-destructive mx-auto mb-4',
isApp ? 'h-16 w-16' : 'h-12 w-12',
)}
>
<AlertCircle className={isApp ? 'h-8 w-8' : 'h-6 w-6'} />
</div>
{isApp ? (
<h2 className="text-xl font-extrabold text-destructive mb-2">{title}</h2>
) : (
<h3 className="text-base font-semibold text-foreground mb-1.5">{title}</h3>
)}
<p
className={
isApp ? 'text-sm text-muted-foreground mb-6' : 'text-xs text-muted-foreground mb-5'
}
>
{description}
</p>
{errorText && (
<div
className={cn(
'rounded-lg bg-zinc-950 dark:bg-zinc-900 text-left border border-border/40',
isApp ? 'mb-6 p-4 max-h-[200px] overflow-auto' : 'mb-5 p-3 max-h-40 overflow-y-auto',
)}
>
<pre
className={cn(
'font-mono whitespace-pre-wrap break-all text-zinc-200 selection:bg-zinc-700',
isApp ? 'text-xs' : 'text-[11px] leading-relaxed',
)}
>
{errorText}
</pre>
</div>
)}
<Button
variant="destructive"
size={isApp ? 'default' : 'sm'}
onClick={onAction}
className={isApp ? 'rounded-lg font-bold shadow-sm' : 'font-medium shadow-sm'}
>
<RefreshCw className={isApp ? 'mr-2 h-4 w-4' : 'mr-1.5 h-3.5 w-3.5'} />
{actionLabel}
</Button>
</div>
</div>
);
}
+58
View File
@@ -0,0 +1,58 @@
import { Component, ErrorInfo, ReactNode } from 'react';
import { ErrorFallback } from '@/components/ErrorFallback';
interface Props {
children: ReactNode;
resetKey?: string | number;
}
interface State {
hasError: boolean;
error: Error | null;
}
class PageErrorBoundary extends Component<Props, State> {
state: State = {
hasError: false,
error: null,
};
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Uncaught error in page:', error, errorInfo);
}
componentDidUpdate(prevProps: Props) {
if (this.state.hasError && prevProps.resetKey !== this.props.resetKey) {
this.setState({ hasError: false, error: null });
}
}
private handleRetry = () => {
this.setState({ hasError: false, error: null });
};
render() {
if (this.state.hasError) {
return (
<ErrorFallback
variant="page"
title="该功能运行异常"
description="该页面在加载或渲染时遇到了内部脚本错误。您可以尝试重试,或者通过导航菜单切换到其他工具。"
error={this.state.error}
actionLabel="重新尝试"
onAction={this.handleRetry}
showStack
/>
);
}
return this.props.children;
}
}
export { PageErrorBoundary };
export default PageErrorBoundary;
+78
View File
@@ -0,0 +1,78 @@
/**
* PageSkeleton 组件 - 页面加载骨架屏
*
* 用于 Suspense fallback 和初始加载状态,提供平滑的视觉过渡
* 避免白屏闪烁,减少布局偏移
*/
interface PageSkeletonProps {
/** 骨架屏类型 */
variant?: 'dashboard' | 'tool';
}
/**
* 仪表盘卡片骨架屏
*/
function DashboardCardSkeleton() {
return (
<div className="rounded-xl border border-border p-5 h-[100px]">
<div className="flex justify-between items-start">
<div className="flex gap-3 items-center">
<div className="w-10 h-10 rounded-lg bg-muted animate-pulse" />
<div>
<div className="w-24 h-5 bg-muted rounded animate-pulse" />
<div className="w-32 h-3.5 bg-muted rounded animate-pulse mt-1.5" />
</div>
</div>
<div className="w-3 h-3 rounded-full bg-muted animate-pulse" />
</div>
</div>
);
}
/**
* 工具页面骨架屏
*/
function ToolPageSkeleton() {
return (
<div className="p-5">
{/* 标题区域 */}
<div className="w-44 h-7 bg-muted rounded animate-pulse mb-4" />
{/* 输入区域 */}
<div className="w-full h-[120px] bg-muted rounded-xl animate-pulse mb-4" />
{/* 控制栏 */}
<div className="flex gap-2 mb-4">
<div className="w-24 h-9 bg-muted rounded-lg animate-pulse" />
<div className="w-20 h-9 bg-muted rounded-lg animate-pulse" />
<div className="flex-1" />
<div className="w-24 h-9 bg-muted rounded-lg animate-pulse" />
</div>
{/* 结果区域 */}
<div className="w-full h-[160px] bg-muted rounded-xl animate-pulse" />
</div>
);
}
/**
* 页面加载骨架屏
*
* @param props - PageSkeletonProps
* @returns 骨架屏 JSX 元素
*/
export default function PageSkeleton({ variant = 'dashboard' }: PageSkeletonProps) {
if (variant === 'tool') {
return <ToolPageSkeleton />;
}
return (
<div className="grid grid-cols-1 sm:grid-cols-[repeat(auto-fill,minmax(300px,1fr))] auto-rows-fr gap-4 p-4">
{Array.from({ length: 6 }).map((_, index) => (
<DashboardCardSkeleton key={index} />
))}
</div>
);
}
PageSkeleton.displayName = 'PageSkeleton';
+27
View File
@@ -0,0 +1,27 @@
# components/
通用业务组件目录,存放跨页面复用的 UI 组件,与具体工具页面解耦。
## 组件列表
| 组件 | 用途 |
| ----------------------- | ------------------------------------------------------------------------------ |
| `RouterContainer.tsx` | 路由容器,根据当前路由动态渲染对应页面组件,集成错误边界和骨架屏 |
| `SwitchButtonGroup.tsx` | 通用切换按钮组,支持 `small/medium/large` 三种尺寸,用于页面子模式切换 |
| `EmptyPlaceholder.tsx` | 虚线边框空状态占位,统一工具页「暂无结果」提示样式 |
| `TextInputArea.tsx` | 增强文本输入区域,支持校验规则、工具栏操作、字符计数、清空 |
| `CopyButton.tsx` | 一键复制按钮:复制成功后 1.5s 内切换为 Check 图标并应用 `text-emerald-500`;空内容/失败时 `toast` 提示 |
| `ImageUploader.tsx` | 图片上传组件,支持拖拽上传、文件选择和预览 |
| `QrCodePreview.tsx` | 二维码预览组件,展示生成的二维码图片,提供复制和下载操作 |
| `DecodeResultPaper.tsx` | Base64 解码结果展示面板,显示 MIME 类型、文件大小、文件名输入和下载按钮 |
| `GlobalSnackbar.tsx` | 全局消息提示组件 + Context Provider,支持受控/Hook/全局单例三种使用方式 |
| `ErrorBoundary.tsx` | 全局错误边界(类组件),捕获子组件树 JS 错误并展示友好错误页面 |
| `PageErrorBoundary.tsx` | 页面级错误边界,适配 shadcn 暗黑模式,支持 `resetKey` 自动恢复 |
| `PageSkeleton.tsx` | 页面骨架屏,提供 `dashboard``tool` 两种变体,用于 Suspense fallback |
## 使用约定
- 优先使用 `components/ui/` 下的 shadcn/ui 基础组件
- 组件使用 `cn()` 合并 Tailwind 类名,支持 `className` 透传
- 需要 memo 优化的组件使用 `React.memo` + `displayName`
- 需要 ref 转发的组件使用 `React.forwardRef` + `displayName`
+73
View File
@@ -0,0 +1,73 @@
import { getFeatureByKey } from '@/config/features';
import { loadPage } from '@/config/pageLoaders/index';
import { useRouter } from '@/providers/RouterProvider';
import type { PageType } from '@/types/storage';
import { type ComponentType, useEffect, useState } from 'react';
import PageErrorBoundary from '@/components/PageErrorBoundary';
import PageSkeleton from '@/components/PageSkeleton';
import { cn } from '@/lib/utils';
import { AlertTriangle } from 'lucide-react';
function LoadedPage({ pageKey }: { pageKey: PageType }) {
const [Page, setPage] = useState<ComponentType | null>(null);
useEffect(() => {
let cancelled = false;
loadPage(pageKey)
.then((mod) => {
if (!cancelled) {
setPage(() => mod.default);
}
})
.catch((err) => {
console.error('[Router Page Load Error]', err);
});
return () => {
cancelled = true;
};
}, [pageKey]);
if (!Page) {
return <PageSkeleton variant={pageKey === 'dashboard' ? 'dashboard' : 'tool'} />;
}
return <Page />;
}
export default function RouterContainer() {
const { currentPage } = useRouter();
const animationClass =
currentPage === 'dashboard' ? 'page-transition-dashboard' : 'page-transition-enter';
const currentFeature = getFeatureByKey(currentPage);
return (
<div
key={currentPage}
className={cn(
'flex-1 flex flex-col overflow-x-hidden overflow-y-auto',
'motion-reduce:transition-none',
animationClass,
)}
>
<PageErrorBoundary resetKey={currentPage}>
{currentFeature ? (
<LoadedPage key={currentPage} pageKey={currentPage} />
) : (
<div className="flex-1 flex flex-col items-center justify-center p-8 text-center fade-in-300">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-destructive/10 text-destructive mb-4">
<AlertTriangle className="h-6 w-6" />
</div>
<h3 className="text-sm font-semibold text-foreground"></h3>
<p className="text-xs text-muted-foreground mt-1 max-w-[240px]">
</p>
</div>
)}
</PageErrorBoundary>
</div>
);
}
+65
View File
@@ -0,0 +1,65 @@
import React from 'react';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
export interface SwitchOption<T extends string | number = string> {
value: T;
label: React.ReactNode;
}
export interface SwitchButtonGroupProps<T extends string | number = string> extends Omit<
React.HTMLAttributes<HTMLDivElement>,
'onChange'
> {
value: T;
options: SwitchOption<T>[];
onChange: (value: T) => void;
size?: 'small' | 'medium' | 'large';
buttonClassName?: string;
}
const SIZE_CLASSES = {
small: 'text-xs h-8 px-2 py-1 rounded-md',
medium: 'text-sm h-9 px-3 py-1.5 rounded-md',
large: 'text-base h-11 px-4 py-2 rounded-lg',
} as const;
const SELECTED_CLASSES = 'bg-background text-foreground shadow-sm font-semibold fade-in-zoom-95';
const UNSELECTED_CLASSES = 'hover:bg-background/50 hover:text-foreground/80';
export default function SwitchButtonGroup<T extends string | number = string>({
value,
options,
onChange,
size = 'medium',
className,
buttonClassName,
...props
}: SwitchButtonGroupProps<T>) {
return (
<div
className={cn(
'inline-flex w-full items-center justify-center rounded-lg bg-muted text-muted-foreground p-1',
className,
)}
{...props}
>
{options.map((option) => (
<Button
key={option.value}
type="button"
variant="ghost"
onClick={() => onChange(option.value)}
className={cn(
'flex-1 transition-all',
SIZE_CLASSES[size],
value === option.value ? SELECTED_CLASSES : UNSELECTED_CLASSES,
buttonClassName,
)}
>
{option.label}
</Button>
))}
</div>
);
}
+306
View File
@@ -0,0 +1,306 @@
import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react';
import { X } from 'lucide-react';
import { cn } from '@/lib/utils';
import { toast } from 'sonner';
import { CopyButton } from '@/components/CopyButton';
import { Button } from './ui/button';
export type ValidateRule = {
validator: (value: string) => boolean;
message: string;
};
export type ToolbarAction = {
key: string;
label: string;
icon?: React.ReactNode;
position?: 'top' | 'bottom';
type?: 'primary' | 'default' | 'danger';
disabled?: boolean | ((value: string) => boolean);
onClick: (value: string, helpers: { clear: () => void; setError: (msg: string) => void }) => void;
};
export interface TextInputAreaProps extends Omit<
React.TextareaHTMLAttributes<HTMLTextAreaElement>,
'onChange'
> {
value?: string;
defaultValue?: string;
/** 值变化回调,返回最新的字符串内容 */
onChange?: (value: string) => void;
minRows?: number;
maxRows?: number;
showCount?: boolean;
showClear?: boolean;
allowCopy?: boolean;
rules?: ValidateRule[];
validateTrigger?: 'onBlur' | 'onChange' | 'onAction';
actions?: ToolbarAction[];
topExtra?: React.ReactNode;
title?: string;
externalError?: string;
onClear?: () => void;
}
function ActionButton({
action,
value,
globalDisabled,
onAction,
}: {
action: ToolbarAction;
value: string;
globalDisabled: boolean;
onAction: (action: ToolbarAction) => void;
}) {
const isBtnDisabled =
typeof action.disabled === 'function' ? action.disabled(value) : (action.disabled ?? false);
const variantClasses = {
primary: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
danger: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
default:
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
};
return (
<button
type="button"
onClick={() => onAction(action)}
disabled={isBtnDisabled || globalDisabled}
className={cn(
'inline-flex items-center justify-center rounded-md text-xs font-medium transition-colors h-7 px-2.5',
'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring',
'disabled:pointer-events-none disabled:opacity-50',
variantClasses[action.type || 'default'],
)}
>
{action.icon && <span className="mr-1.5 h-3.5 w-3.5 flex items-center">{action.icon}</span>}
{action.label}
</button>
);
}
const TextInputArea = forwardRef<HTMLTextAreaElement, TextInputAreaProps>((props, ref) => {
const {
value: controlledValue,
defaultValue = '',
onChange,
placeholder: placeholderProp,
disabled = false,
readOnly = false,
autoFocus = false,
minRows = 4,
maxRows = 12,
maxLength,
className,
showCount = false,
showClear = true,
allowCopy = false,
rules = [],
validateTrigger = 'onAction',
actions = [],
topExtra,
title,
externalError,
onClear,
...restProps
} = props;
const internalRef = useRef<HTMLTextAreaElement | null>(null);
const [internalValue, setInternalValue] = useState(defaultValue);
const [error, setError] = useState<string>('');
const placeholder = placeholderProp ?? '请输入文本';
const isControlled = controlledValue !== undefined;
const value = isControlled ? controlledValue : internalValue;
const displayError = externalError ?? error;
useImperativeHandle(ref, () => internalRef.current as HTMLTextAreaElement);
const adjustHeight = useCallback(() => {
const textArea = internalRef.current;
if (!textArea) return;
textArea.style.height = 'auto';
const computedMin = minRows * 24;
const computedMax = maxRows * 24;
const nextHeight = Math.max(textArea.scrollHeight, computedMin);
textArea.style.height = `${Math.min(nextHeight, computedMax)}px`;
}, [minRows, maxRows]);
React.useEffect(() => {
adjustHeight();
}, [value, adjustHeight]);
const validate = useCallback(
(val: string, trigger?: string): boolean => {
if (validateTrigger !== trigger && trigger) return true;
for (const rule of rules) {
if (!rule.validator(val)) {
setError(rule.message);
return false;
}
}
setError('');
return true;
},
[rules, validateTrigger],
);
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const newVal = e.target.value;
if (maxLength && newVal.length > maxLength) {
const msg = `内容不能超过 ${maxLength} 个字符`;
setError(msg);
toast.warning(msg);
return;
}
if (!isControlled) setInternalValue(newVal);
onChange?.(newVal);
if (error) setError('');
if (validateTrigger === 'onChange') validate(newVal, 'onChange');
};
const handleBlur = () => {
if (validateTrigger === 'onBlur') validate(value, 'onBlur');
};
const handleClear = useCallback(() => {
if (!isControlled) setInternalValue('');
onChange?.('');
setError('');
internalRef.current?.focus();
toast.success('已清空');
onClear?.();
}, [isControlled, onChange, onClear]);
const handleAction = useCallback(
(action: ToolbarAction) => {
const isDisabled =
typeof action.disabled === 'function' ? action.disabled(value) : action.disabled;
if (isDisabled || disabled) return;
if (validateTrigger === 'onAction' && !validate(value, 'onAction')) return;
action.onClick(value, {
clear: handleClear,
setError,
});
},
[value, disabled, validate, validateTrigger, handleClear],
);
const topActions = actions.filter((a) => a.position !== 'bottom');
const bottomActions = actions.filter((a) => a.position === 'bottom');
const hasTopBar = title || showCount || topActions.length > 0 || topExtra;
const hasBottomBar = allowCopy || showClear || bottomActions.length > 0;
return (
<div className={cn('w-full flex flex-col gap-1.5', className)}>
{hasTopBar && (
<div className="flex items-center justify-between px-0.5">
<div className="flex items-center gap-2">
{title && <span className="text-xs font-semibold text-muted-foreground">{title}</span>}
{topExtra}
</div>
<div className="flex items-center gap-1.5">
{topActions.map((action) => (
<ActionButton
key={action.key}
action={action}
value={value}
globalDisabled={disabled}
onAction={handleAction}
/>
))}
{showCount && (
<span className="text-xs text-muted-foreground tabular-nums">
{value.length}
{maxLength ? ` / ${maxLength}` : ''}
</span>
)}
</div>
</div>
)}
<div
className={cn(
'rounded-md border border-input bg-background shadow-sm transition-all focus-within:ring-1 focus-within:ring-ring focus-within:border-input overflow-hidden',
displayError &&
'border-destructive focus-within:ring-destructive focus-within:border-destructive',
)}
>
<textarea
ref={internalRef}
value={value}
onChange={handleChange}
onBlur={handleBlur}
disabled={disabled}
autoFocus={autoFocus}
readOnly={readOnly}
placeholder={placeholder}
className="w-full bg-transparent px-4 py-3 font-mono text-sm leading-relaxed text-foreground placeholder:text-muted-foreground/50 focus:outline-none resize-none border-0 block"
{...restProps}
/>
{hasBottomBar && (
<div className="flex h-10 items-center justify-between px-4 bg-muted/30 border-t border-border/50">
<div className="flex items-center gap-1.5 min-w-0">
{bottomActions.map((action) => (
<ActionButton
key={action.key}
action={action}
value={value}
globalDisabled={disabled}
onAction={handleAction}
/>
))}
</div>
<div className="flex items-center gap-1.5 ml-auto shrink-0">
{allowCopy && value && (
<CopyButton
text={value}
tooltip="复制内容"
variant="ghost"
size="iconSm"
className="text-muted-foreground hover:text-foreground"
/>
)}
{showClear && value && !disabled && !readOnly && (
<Button
type="button"
onClick={handleClear}
aria-label="清空"
variant="ghost"
size="iconSm"
className="text-muted-foreground hover:text-destructive hover:bg-destructive/10"
>
<X className="h-4 w-4" />
</Button>
)}
</div>
</div>
)}
</div>
{displayError && (
<p className="text-xs font-medium text-destructive px-0.5 fade-in-slide-top-1">
{displayError}
</p>
)}
</div>
);
});
TextInputArea.displayName = 'TextInputArea';
export default TextInputArea;
-147
View File
@@ -1,147 +0,0 @@
import {useEffect, useState, useRef, useCallback} from "react";
import CopyButton from "./CopyButton";
/**
* 时间戳显示和执行组件
*
* 功能特性:
* 1. 实时显示当前时间戳(毫秒/秒)
* 2. 支持毫秒和秒单位切换
* 3. 支持启动/停止时间戳自动更新
* 4. 提供复制时间戳功能
* 5. 响应式设计和良好的可访问性
*
* @component
* @example
* ```jsx
* <TimestampExecution />
* ```
*
* @returns {JSX.Element} 时间戳组件
*/
export function TimestampExecution() {
/** @type {[number, function]} 当前时间戳(毫秒)和更新函数 */
const [currentTimestamp, setCurrentTimestamp] = useState(() => Math.floor(Date.now()));
/** @type {[boolean, function]} 是否显示毫秒(true=毫秒,false=秒) */
const [showMilliseconds, setShowMilliseconds] = useState(true);
/** @type {[boolean, function]} 时间戳是否正在自动更新 */
const [isRunningTimestamp, setIsRunningTimestamp] = useState(true);
/** @type {React.RefObject<NodeJS.Timeout | null>} 定时器引用,用于清理 */
const timerRef = useRef(null);
/**
* 计算显示的时间戳值
* @type {number}
*/
const displayTimestamp = showMilliseconds
? currentTimestamp
: Math.floor(currentTimestamp / 1000);
/**
* 计算单位文本
* @type {string}
*/
const unitText = showMilliseconds ? '毫秒' : '秒';
/**
* 定时更新时间戳的副作用
* 根据 isRunningTimestamp 和 showMilliseconds 控制定时器的启停和间隔
*/
useEffect(() => {
// 清除之前的定时器
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
// 如果需要运行,创建新的定时器
if (isRunningTimestamp) {
const interval = showMilliseconds ? 100 : 1000;
timerRef.current = setInterval(() => {
setCurrentTimestamp(Math.floor(Date.now()));
}, interval);
}
// 清理函数
return () => {
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
};
}, [isRunningTimestamp, showMilliseconds]);
/**
* 切换时间戳显示单位(毫秒/秒)
* @type {function(): void}
*/
const toggleUnit = useCallback(() => {
setShowMilliseconds(prev => !prev);
}, []);
/**
* 切换时间戳自动更新状态(启动/停止)
* @type {function(): void}
*/
const toggleTimestamp = useCallback(() => {
setIsRunningTimestamp(prev => !prev);
}, []);
/**
* 切换单位按钮的辅助文本
* @type {string}
*/
const unitButtonLabel = showMilliseconds ? '切换为秒显示' : '切换为毫秒显示';
/**
* 启动/停止按钮的辅助文本
* @type {string}
*/
const toggleButtonLabel = isRunningTimestamp ? '停止时间戳自动更新' : '开始时间戳自动更新';
/**
* 启动/停止按钮的显示文本
* @type {string}
*/
const toggleButtonText = isRunningTimestamp ? '停止' : '开始';
return (
<div className="timestamp-container">
<div className="timestamp-display">
<span className="timestamp-value">{displayTimestamp}</span>
<span className="timestamp-unit">{unitText}</span>
</div>
<div className="timestamp-controls">
<button
type="button"
className="timestamp-btn action-btn"
onClick={toggleUnit}
aria-label={unitButtonLabel}
title={unitButtonLabel}
>
切换单位
</button>
<CopyButton
text={currentTimestamp.toString()}
buttonText="复制时间戳"
aria-label="复制当前时间戳到剪贴板"
/>
<button
type="button"
className={`timestamp-btn ${isRunningTimestamp ? 'stop-btn' : 'action-btn'}`}
onClick={toggleTimestamp}
aria-label={toggleButtonLabel}
title={toggleButtonLabel}
>
{toggleButtonText}
</button>
</div>
</div>
);
}
-186
View File
@@ -1,186 +0,0 @@
import {useState, useCallback} from "react";
import {formatWithZone, TimezoneOptions} from "../utils/timeUtils";
/**
* 时间戳转日期时间组件
*
* 功能特性:
* 1. 将时间戳转换为日期时间字符串
* 2. 支持多种时区选择
* 3. 支持毫秒和秒单位切换
* 4. 提供输入验证和错误提示
*
* @component
* @example
* ```jsx
* <TimestampToDatetime />
* ```
*
* @returns {JSX.Element} 时间戳转日期时间组件
*/
// 常用时区列表
const TIME_ZONE_LIST = [
'America/New_York',
'America/Chicago',
'America/Denver',
'America/Los_Angeles',
'America/Anchorage',
'America/Honolulu',
'Europe/London',
'Europe/Paris',
'Europe/Berlin',
'Europe/Moscow',
'Asia/Tokyo',
'Asia/Shanghai',
'Asia/Hong_Kong',
'Asia/Singapore',
'Asia/Dubai',
'Asia/Kolkata',
'Australia/Sydney',
'Pacific/Auckland',
];
// 时间戳单位选项
const TIMESTAMP_UNITS = [
{value: 'milliseconds', label: '毫秒(ms)'},
{value: 'seconds', label: '秒(s)'},
];
export function TimestampToDatetime() {
/** @type {[string, function]} 输入的时间戳值 */
const [timestampValue, setTimestampValue] = useState(Date.now());
/** @type {[string, function]} 转换结果 */
const [timestampResult, setTimestampResult] = useState('');
/** @type {[string, function]} 时间戳单位 ('milliseconds' | 'seconds') */
const [unit, setUnit] = useState('milliseconds');
/** @type {[string, function]} 选择的时区 */
const [selectedZone, setSelectedZone] = useState('Asia/Shanghai');
/** @type {[string, function]} 错误信息 */
const [error, setError] = useState('');
/**
* 转换时间戳为日期时间
* @type {function(): void}
*/
const handleConvertTimestampToDate = useCallback(() => {
try {
setError('');
if (!timestampValue) {
setError('请输入时间戳');
setTimestampResult('');
return;
}
const numericValue = Number(timestampValue);
if (isNaN(numericValue)) {
setError('无效的时间戳格式');
setTimestampResult('');
return;
}
const result = formatWithZone(numericValue, selectedZone, unit);
setTimestampResult(result);
} catch (err) {
setError('转换失败,请检查输入格式');
setTimestampResult('');
}
}, [timestampValue, selectedZone, unit]);
/**
* 处理时间戳输入变化
* @type {function(React.ChangeEvent<HTMLInputElement>): void}
*/
const handleInputChange = useCallback((e) => {
setTimestampValue(e.target.value);
setError(''); // 清除错误信息
}, []);
/**
* 处理时区选择变化
* @type {function(React.ChangeEvent<HTMLSelectElement>): void}
*/
const handleZoneChange = useCallback((e) => {
setSelectedZone(e.target.value);
}, []);
/**
* 处理时间戳单位变化
* @type {function(React.ChangeEvent<HTMLSelectElement>): void}
*/
const handleUnitChange = useCallback((e) => {
setUnit(e.target.value);
}, []);
return (
<div className="datetime-converter">
<h2 className="converter-title">时间戳转日期时间</h2>
<div className="converter-form">
<div className="input-group">
<input
type="number"
placeholder="输入时间戳 (如: 1704067200000)"
value={timestampValue}
className="datetime-input"
onChange={handleInputChange}
aria-label="输入要转换的时间戳"
title="支持毫秒或秒为单位的时间戳"
/>
<select
value={unit}
className="unit-select"
onChange={handleUnitChange}
aria-label="选择时间戳单位"
>
{TIMESTAMP_UNITS.map(({value, label}) => (
<option key={value} value={value}>
{label}
</option>
))}
</select>
</div>
{error && (
<div className="error-message" role="alert">
{error}
</div>
)}
<div className="action-group">
<button
className="converter-btn action-btn"
onClick={handleConvertTimestampToDate}
aria-label="转换时间戳为日期时间"
>
转换
</button>
</div>
<div className="result-group">
<input
type="text"
placeholder="转换结果"
value={timestampResult}
className="result-input"
readOnly
aria-label="转换结果"
/>
<select
value={selectedZone}
className="timezone-select"
onChange={handleZoneChange}
aria-label="选择时区"
>
<TimezoneOptions zones={TIME_ZONE_LIST}/>
</select>
</div>
</div>
</div>
)
}
-18
View File
@@ -1,18 +0,0 @@
const TodoList = ({todoContentList = []}) => {
const todoLi = todoContentList?.map((todo, index) => {
return (
<li key={index}>{todo}</li>
)
})
return (
<div className="todo-list">
<ul>
{todoLi}
</ul>
</div>
);
};
export default TodoList;
-309
View File
@@ -1,309 +0,0 @@
/* Markdown Editor Styles */
.markdown-editor {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
}
.toolbar {
background-color: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 15px;
margin-bottom: 20px;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
gap: 15px;
}
.toolbar-section {
display: flex;
align-items: center;
gap: 10px;
}
.toolbar-label {
font-weight: 600;
color: #495057;
font-size: 0.95rem;
white-space: nowrap;
}
.format-buttons {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.format-button {
padding: 6px 12px;
background-color: #fff;
border: 1px solid #ced4da;
border-radius: 8px;
font-size: 0.85rem;
font-weight: 600;
color: #495057;
cursor: pointer;
transition: all 0.2s ease;
min-width: 36px;
text-align: center;
}
.format-button:hover {
background-color: #e9ecef;
border-color: #adb5bd;
transform: translateY(-1px);
}
.format-button:active {
transform: translateY(0);
}
.action-button {
padding: 8px 16px;
border: none;
border-radius: 8px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.clear-button {
background-color: #dc3545;
color: white;
}
.clear-button:hover {
background-color: #c82333;
}
.reset-button {
background-color: #6c757d;
color: white;
}
.reset-button:hover {
background-color: #5a6268;
}
@media (max-width: 768px) {
.toolbar {
flex-direction: column;
align-items: stretch;
gap: 12px;
}
.toolbar-section {
flex-direction: column;
align-items: stretch;
}
.format-buttons {
justify-content: center;
}
.toolbar-label {
text-align: center;
margin-bottom: 5px;
}
}
.editor-container {
margin-bottom: 20px;
}
@media (max-width: 768px) {
.editor-container {
grid-template-columns: 1fr;
gap: 20px;
}
}
.input-section,
.preview-section {
display: flex;
flex-direction: column;
}
.input-section label,
.preview-section label {
margin-bottom: 10px;
font-weight: 600;
color: #444;
font-size: 1.1rem;
}
.markdown-input {
box-sizing: border-box;
width: 100%;
min-height: 400px;
padding: 15px;
border: 2px solid #ddd;
border-radius: 8px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 14px;
line-height: 1.5;
resize: vertical;
transition: border-color 0.2s ease;
background-color: #f9f9f9;
}
.markdown-input:focus {
outline: none;
border-color: #4a90e2;
background-color: #fff;
box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.1);
}
.markdown-input::placeholder {
color: #999;
}
.markdown-preview {
box-sizing: border-box;
width: 100%;
min-height: 400px;
max-height: 600px;
padding: 15px;
border: 2px solid #ddd;
border-radius: 8px;
overflow-y: auto;
background-color: #fff;
line-height: 1.6;
}
.markdown-preview h1,
.markdown-preview h2,
.markdown-preview h3,
.markdown-preview h4,
.markdown-preview h5,
.markdown-preview h6 {
margin-top: 1.5em;
margin-bottom: 0.5em;
font-weight: 600;
line-height: 1.25;
}
.markdown-preview h1 {
font-size: 2em;
border-bottom: 2px solid #eaeaea;
padding-bottom: 0.3em;
}
.markdown-preview h2 {
font-size: 1.5em;
border-bottom: 1px solid #eaeaea;
padding-bottom: 0.3em;
}
.markdown-preview p {
margin: 0 0 1em 0;
}
.markdown-preview ul,
.markdown-preview ol {
padding-left: 2em;
margin: 0 0 1em 0;
}
.markdown-preview li {
margin-bottom: 0.5em;
}
.markdown-preview code {
padding: 0.2em 0.4em;
margin: 0;
font-size: 85%;
background-color: rgba(27, 31, 35, 0.05);
border-radius: 8px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
}
.markdown-preview pre {
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: #f6f8fa;
border-radius: 8px;
margin: 0 0 1em 0;
}
.markdown-preview pre code {
padding: 0;
background-color: transparent;
border-radius: 8px;
}
.markdown-preview blockquote {
padding: 0 1em;
color: #6a737d;
border-left: 0.25em solid #dfe2e5;
margin: 0 0 1em 0;
}
.markdown-preview table {
border-collapse: collapse;
margin: 0 0 1em 0;
width: 100%;
}
.markdown-preview table th,
.markdown-preview table td {
padding: 6px 13px;
border: 1px solid #dfe2e5;
}
.markdown-preview table th {
font-weight: 600;
background-color: #f6f8fa;
}
.markdown-preview table tr:nth-child(2n) {
background-color: #f6f8fa;
}
.markdown-preview a {
color: #0366d6;
text-decoration: none;
}
.markdown-preview a:hover {
text-decoration: underline;
}
.markdown-preview img {
max-width: 100%;
height: auto;
}
.markdown-preview hr {
height: 0.25em;
padding: 0;
margin: 24px 0;
background-color: #e1e4e8;
border: 0;
}
/* Scrollbar styling */
.markdown-preview::-webkit-scrollbar {
width: 8px;
}
.markdown-preview::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 8px;
}
.markdown-preview::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 8px;
}
.markdown-preview::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
-51
View File
@@ -1,51 +0,0 @@
import './TodoMarkdownEditor.css';
import ReactMarkdown from "react-markdown";
import {useState, useRef} from "react";
const TodoMarkdownEditor = () => {
const [markdown, setMarkdown] = useState('# Hello, world!\n\nThis is a simple paragraph with some **bold** text.');
const textareaRef = useRef(null);
const handleClear = () => {
setMarkdown('');
};
const handleReset = () => {
setMarkdown('# Hello, world!\n\nThis is a simple paragraph with some **bold** text.');
};
return (<div className="markdown-editor">
<div className="editor-container">
<div className="preview-section">
<label>预览区</label>
<div className="markdown-preview">
<ReactMarkdown>{markdown}</ReactMarkdown>
</div>
</div>
<div className="input-section">
<label htmlFor="markdown-input">编辑区</label>
<textarea
id="markdown-input"
ref={textareaRef}
placeholder="输入Markdown内容..."
className="markdown-input"
value={markdown}
onChange={(e) => setMarkdown(e.target.value)}
/>
</div>
<div className="toolbar">
<div className="toolbar-section">
<button className="action-button clear-button" onClick={handleClear}>
清空
</button>
<button className="action-button reset-button" onClick={handleReset}>
重置
</button>
</div>
</div>
</div>
</div>);
};
export default TodoMarkdownEditor;
@@ -0,0 +1,155 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { act, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
// unmock the globally-mocked component so we test the real implementation
vi.unmock('@/components/CopyButton');
vi.mock('@/utils/clipboard', () => ({
copyTextToClipboard: vi.fn(),
}));
vi.mock('sonner', () => ({
toast: {
success: vi.fn(),
error: vi.fn(),
},
}));
import { CopyButton } from '@/components/CopyButton';
import { copyTextToClipboard } from '@/utils/clipboard';
import { toast } from 'sonner';
const mockedCopy = vi.mocked(copyTextToClipboard);
const mockedToast = vi.mocked(toast);
describe('CopyButton', () => {
beforeEach(() => {
vi.useFakeTimers({ shouldAdvanceTime: true });
vi.clearAllMocks();
});
afterEach(() => {
vi.useRealTimers();
});
it('复制成功时调用 copyTextToClipboard 并传入正确 text', async () => {
mockedCopy.mockResolvedValue(true);
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
render(<CopyButton text="hello world" />);
await user.click(screen.getByRole('button'));
expect(mockedCopy).toHaveBeenCalledWith('hello world');
expect(mockedToast.success).toHaveBeenCalled();
});
it('复制空文本时弹出 error toast', async () => {
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
render(<CopyButton text="" />);
await user.click(screen.getByRole('button'));
expect(mockedCopy).not.toHaveBeenCalled();
expect(mockedToast.error).toHaveBeenCalled();
});
it('复制失败时弹出 error toast', async () => {
mockedCopy.mockResolvedValue(false);
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
render(<CopyButton text="something" />);
await user.click(screen.getByRole('button'));
expect(mockedCopy).toHaveBeenCalledWith('something');
expect(mockedToast.error).toHaveBeenCalled();
});
it('初始渲染时带有默认 aria-label', () => {
render(<CopyButton text="initial" />);
expect(screen.getByRole('button')).toHaveAttribute('aria-label', '复制');
});
it('自定义 tooltip 会覆盖默认 title 和 aria-label', () => {
render(<CopyButton text="tooltip-test" tooltip="自定义提示" />);
const button = screen.getByRole('button');
expect(button).toHaveAttribute('title', '自定义提示');
expect(button).toHaveAttribute('aria-label', '自定义提示');
});
it('className 被正确透传到按钮', () => {
render(<CopyButton text="class-test" className="my-custom-class" />);
const button = screen.getByRole('button');
expect(button.className).toContain('my-custom-class');
});
it('点击事件阻止冒泡', async () => {
mockedCopy.mockResolvedValue(true);
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
const parentClick = vi.fn();
render(
<div onClick={parentClick}>
<CopyButton text="stop-propagation" />
</div>,
);
await user.click(screen.getByRole('button'));
expect(mockedCopy).toHaveBeenCalled();
expect(parentClick).not.toHaveBeenCalled();
});
it('组件卸载时清除定时器,不触发状态更新警告', async () => {
mockedCopy.mockResolvedValue(true);
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
const { unmount } = render(<CopyButton text="unmount-test" />);
await user.click(screen.getByRole('button'));
act(() => {
vi.advanceTimersByTime(500);
});
expect(() => unmount()).not.toThrow();
act(() => {
vi.advanceTimersByTime(2000);
});
});
it('快速连续点击不会创建多个重叠定时器', async () => {
mockedCopy.mockResolvedValue(true);
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
render(<CopyButton text="rapid-click" />);
const button = screen.getByRole('button');
await user.click(button);
await user.click(button);
await user.click(button);
expect(mockedCopy).toHaveBeenCalledTimes(3);
act(() => {
vi.advanceTimersByTime(1500);
});
});
it('其他 button props 通过 ...props 透传', () => {
render(<CopyButton text="props-test" data-testid="copy-btn" disabled id="copy-button-id" />);
const button = screen.getByRole('button');
expect(button).toHaveAttribute('data-testid', 'copy-btn');
expect(button).toBeDisabled();
expect(button).toHaveAttribute('id', 'copy-button-id');
});
});
@@ -0,0 +1,48 @@
import { describe, expect, it } from 'vitest';
import { render, screen } from '@testing-library/react';
import EmptyPlaceholder from '@/components/EmptyPlaceholder';
describe('EmptyPlaceholder 组件', () => {
it('应渲染字符串提示文本', () => {
render(<EmptyPlaceholder></EmptyPlaceholder>);
expect(screen.getByText('请输入内容')).toBeInTheDocument();
});
it('应应用规范空状态容器样式', () => {
const { container } = render(<EmptyPlaceholder></EmptyPlaceholder>);
const placeholder = container.firstChild;
expect(placeholder).toHaveClass(
'rounded-xl',
'bg-muted/30',
'border-dashed',
'border-border/80',
'min-h-[120px]',
);
});
it('应支持 className 自定义容器样式', () => {
const { container } = render(
<EmptyPlaceholder className="flex-1 min-h-[320px]"></EmptyPlaceholder>,
);
expect(container.firstChild).toHaveClass('flex-1', 'min-h-[320px]');
});
it('应支持 messageClassName 自定义文本样式', () => {
render(<EmptyPlaceholder messageClassName="text-sm max-w-none"></EmptyPlaceholder>);
expect(screen.getByText('提示')).toHaveClass('text-sm', 'max-w-none');
});
it('应支持 ReactNode 类型的 children', () => {
render(
<EmptyPlaceholder>
<span data-testid="custom-content"></span>
</EmptyPlaceholder>,
);
expect(screen.getByTestId('custom-content')).toBeInTheDocument();
});
});
@@ -0,0 +1,102 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import { ErrorBoundary } from '@/components/ErrorBoundary';
// 用于触发错误的测试子组件
function ThrowError({ message }: { message: string }): never {
throw new Error(message);
}
// 正常渲染的子组件
function NormalComponent({ text }: { text: string }) {
return <div data-testid="normal-content">{text}</div>;
}
describe('ErrorBoundary', () => {
beforeEach(() => {
// 抑制测试中故意抛出的错误日志
vi.spyOn(console, 'error').mockImplementation(() => {});
});
it('正常渲染子组件', () => {
render(
<ErrorBoundary>
<NormalComponent text="正常内容" />
</ErrorBoundary>,
);
expect(screen.getByTestId('normal-content')).toHaveTextContent('正常内容');
});
it('子组件抛出错误时显示错误 UI', () => {
render(
<ErrorBoundary>
<ThrowError message="测试错误" />
</ErrorBoundary>,
);
expect(screen.getByText('糟糕,出了点问题')).toBeInTheDocument();
expect(screen.getByText(/测试错误/)).toBeInTheDocument();
});
it('children 变化时重置错误状态', async () => {
const { rerender } = render(
<ErrorBoundary>
<ThrowError message="初始错误" />
</ErrorBoundary>,
);
expect(screen.getByText('糟糕,出了点问题')).toBeInTheDocument();
// 切换到正常子组件
rerender(
<ErrorBoundary>
<NormalComponent text="恢复后的内容" />
</ErrorBoundary>,
);
await waitFor(() => {
expect(screen.getByTestId('normal-content')).toHaveTextContent('恢复后的内容');
});
expect(screen.queryByText('糟糕,出了点问题')).not.toBeInTheDocument();
});
it('相同的 children 不重置错误状态', () => {
const { rerender } = render(
<ErrorBoundary>
<ThrowError message="相同子组件错误" />
</ErrorBoundary>,
);
expect(screen.getByText('糟糕,出了点问题')).toBeInTheDocument();
// 用相同的 children rerender
rerender(
<ErrorBoundary>
<ThrowError message="相同子组件错误" />
</ErrorBoundary>,
);
expect(screen.getByText('糟糕,出了点问题')).toBeInTheDocument();
});
it('错误 UI 包含刷新按钮', () => {
const reloadMock = vi.fn();
Object.defineProperty(window, 'location', {
value: { reload: reloadMock },
writable: true,
});
render(
<ErrorBoundary>
<ThrowError message="按钮测试" />
</ErrorBoundary>,
);
const refreshButton = screen.getByRole('button', { name: /刷新应用/ });
expect(refreshButton).toBeInTheDocument();
refreshButton.click();
expect(reloadMock).toHaveBeenCalledTimes(1);
});
});
@@ -0,0 +1,131 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import { PageErrorBoundary } from '@/components/PageErrorBoundary';
// 用于触发错误的测试子组件
function ThrowError({ message }: { message: string }): never {
throw new Error(message);
}
// 正常渲染的子组件
function NormalComponent({ text }: { text: string }) {
return <div data-testid="normal-content">{text}</div>;
}
describe('PageErrorBoundary', () => {
beforeEach(() => {
// 抑制测试中故意抛出的错误日志
vi.spyOn(console, 'error').mockImplementation(() => {});
});
it('正常渲染子组件', () => {
render(
<PageErrorBoundary>
<NormalComponent text="正常内容" />
</PageErrorBoundary>,
);
expect(screen.getByTestId('normal-content')).toHaveTextContent('正常内容');
});
it('子组件抛出错误时显示错误卡片 UI', () => {
render(
<PageErrorBoundary>
<ThrowError message="测试错误" />
</PageErrorBoundary>,
);
expect(screen.getByText('该功能运行异常')).toBeInTheDocument();
expect(screen.getByText(/测试错误/)).toBeInTheDocument();
});
it('点击重试按钮后恢复', async () => {
const { rerender } = render(
<PageErrorBoundary>
<ThrowError message="可恢复错误" />
</PageErrorBoundary>,
);
expect(screen.getByText('该功能运行异常')).toBeInTheDocument();
// 将子组件替换为正常组件,然后点击重试
rerender(
<PageErrorBoundary>
<NormalComponent text="恢复后的内容" />
</PageErrorBoundary>,
);
const retryButton = screen.getByRole('button', { name: /重新尝试/ });
retryButton.click();
await waitFor(() => {
expect(screen.getByTestId('normal-content')).toHaveTextContent('恢复后的内容');
});
expect(screen.queryByText('该功能运行异常')).not.toBeInTheDocument();
});
it('resetKey 变化时自动重置错误状态', async () => {
const { rerender } = render(
<PageErrorBoundary resetKey="page-a">
<ThrowError message="页面 A 错误" />
</PageErrorBoundary>,
);
expect(screen.getByText('该功能运行异常')).toBeInTheDocument();
// 切换 resetKey,同时提供正常子组件
rerender(
<PageErrorBoundary resetKey="page-b">
<NormalComponent text="页面 B 内容" />
</PageErrorBoundary>,
);
await waitFor(() => {
expect(screen.getByTestId('normal-content')).toHaveTextContent('页面 B 内容');
});
expect(screen.queryByText('该功能运行异常')).not.toBeInTheDocument();
});
it('resetKey 不变时保持错误状态', () => {
const { rerender } = render(
<PageErrorBoundary resetKey="page-a">
<ThrowError message="初始错误" />
</PageErrorBoundary>,
);
expect(screen.getByText('该功能运行异常')).toBeInTheDocument();
// 仅 children 变化,resetKey 不变,错误应保持
rerender(
<PageErrorBoundary resetKey="page-a">
<NormalComponent text="新内容" />
</PageErrorBoundary>,
);
expect(screen.getByText('该功能运行异常')).toBeInTheDocument();
});
it('错误 UI 包含重试按钮', () => {
render(
<PageErrorBoundary>
<ThrowError message="按钮测试" />
</PageErrorBoundary>,
);
const retryButton = screen.getByRole('button', { name: /重新尝试/ });
expect(retryButton).toBeInTheDocument();
});
it('错误信息以 monospace 格式显示', () => {
render(
<PageErrorBoundary>
<ThrowError message="格式化测试" />
</PageErrorBoundary>,
);
const errorText = screen.getByText(/格式化测试/);
expect(errorText).toBeInTheDocument();
expect(errorText.tagName.toLowerCase()).toBe('pre');
});
});
@@ -0,0 +1,64 @@
import { describe, expect, it } from 'vitest';
import { render } from '@testing-library/react';
import PageSkeleton from '@/components/PageSkeleton';
describe('PageSkeleton 组件', () => {
describe('渲染测试', () => {
it('默认应渲染 dashboard 骨架屏', () => {
const { container } = render(<PageSkeleton />);
// dashboard 骨架屏包含 6 个卡片
const cards = container.querySelectorAll('.rounded-xl');
expect(cards.length).toBe(6);
});
it('variant 为 dashboard 时应渲染仪表盘卡片骨架', () => {
const { container } = render(<PageSkeleton variant="dashboard" />);
// 每个卡片有 2 个骨架元素(图标、文本),6 个卡片共 12 个
const cards = container.querySelectorAll('.rounded-xl');
expect(cards.length).toBe(6);
});
it('variant 为 tool 时应渲染工具页面骨架', () => {
const { container } = render(<PageSkeleton variant="tool" />);
// tool 骨架屏包含标题、输入区、控制栏 3 个按钮、结果区
const skeletons = container.querySelectorAll('.animate-pulse');
expect(skeletons.length).toBeGreaterThan(0);
});
});
describe('布局结构测试', () => {
it('dashboard 骨架屏应使用 grid 布局', () => {
const { container } = render(<PageSkeleton variant="dashboard" />);
const gridContainer = container.firstChild as HTMLElement;
expect(gridContainer).toHaveClass('grid');
});
it('tool 骨架屏应有内边距', () => {
const { container } = render(<PageSkeleton variant="tool" />);
const toolContainer = container.firstChild as HTMLElement;
expect(toolContainer).toHaveClass('p-5');
});
});
describe('骨架屏元素测试', () => {
it('dashboard 骨架屏应包含圆角和边框样式', () => {
const { container } = render(<PageSkeleton variant="dashboard" />);
// 获取第一个卡片容器
const card = container.querySelector('.rounded-xl.border');
expect(card).toBeInTheDocument();
});
it('tool 骨架屏应包含动画脉冲效果', () => {
const { container } = render(<PageSkeleton variant="tool" />);
const skeletons = container.querySelectorAll('.animate-pulse');
expect(skeletons.length).toBeGreaterThan(0);
});
});
});
@@ -0,0 +1,81 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { render } from '@testing-library/react';
import RouterContainer from '@/components/RouterContainer';
import { RouterProvider } from '@/providers/RouterProvider';
import type { PageType } from '@/types/storage';
import React from 'react';
const mockRouterValue = {
currentPage: 'dashboard' as PageType,
visiblePages: ['dashboard', 'timestamp'] as PageType[],
pageOrder: ['timestamp'] as PageType[],
navigateTo: vi.fn(),
syncNavigation: vi.fn(),
goHome: vi.fn(),
setVisiblePages: vi.fn(),
setPageOrder: vi.fn(),
recentlyUsedTools: [] as PageType[],
isLoaded: true,
};
vi.mock('@/providers/RouterProvider', () => ({
useRouter: () => mockRouterValue,
RouterProvider: ({ children }: { children: React.ReactNode }) => children,
}));
describe('RouterContainer 组件', () => {
beforeEach(() => {
vi.clearAllMocks();
});
const renderWithProvider = (ui: React.ReactElement) => {
return render(<RouterProvider>{ui}</RouterProvider>);
};
describe('渲染测试', () => {
it('mount 后应直接渲染页面结构(不等待 storage 加载)', () => {
mockRouterValue.currentPage = 'dashboard';
const { container } = renderWithProvider(<RouterContainer />);
expect(container.querySelector('.page-transition-dashboard')).toBeInTheDocument();
});
});
describe('动画类测试', () => {
it('在 dashboard 页面应应用 dashboard 动画类', () => {
mockRouterValue.currentPage = 'dashboard';
renderWithProvider(<RouterContainer />);
const box = document.querySelector('.page-transition-dashboard');
expect(box).toBeInTheDocument();
});
it('在非 dashboard 页面应应用 enter 动画类', () => {
mockRouterValue.currentPage = 'timestamp';
renderWithProvider(<RouterContainer />);
const box = document.querySelector('.page-transition-enter');
expect(box).toBeInTheDocument();
});
});
describe('路由处理测试', () => {
it('currentPage 变化时应更新', () => {
const { rerender } = renderWithProvider(<RouterContainer />);
mockRouterValue.currentPage = 'timestamp';
rerender(<RouterProvider>{<RouterContainer />}</RouterProvider>);
const box = document.querySelector('.page-transition-enter');
expect(box).toBeInTheDocument();
});
});
describe('页面级错误隔离', () => {
it('PageErrorBoundary 应包裹页面内容', () => {
mockRouterValue.currentPage = 'dashboard';
const { container } = renderWithProvider(<RouterContainer />);
// 验证 RouterContainer 的 Box 结构存在
const routerBox = container.querySelector('.page-transition-dashboard');
expect(routerBox).toBeInTheDocument();
});
});
});
@@ -0,0 +1,141 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { fireEvent, render, screen } from '@testing-library/react';
import { StorageCleanerConfirm } from '@/pages/StorageCleaner/components/StorageCleanerConfirm';
import type { StorageCleanerOptions } from '@/types/storage';
import React from 'react';
const storageOnChangedMock = { addListener: vi.fn(), removeListener: vi.fn() };
(globalThis as any).chrome = { storage: { onChanged: storageOnChangedMock } };
(globalThis as any).browser = { storage: { onChanged: storageOnChangedMock } };
describe('StorageCleanerConfirm 组件', () => {
const mockOnClose = vi.fn();
const mockOnConfirm = vi.fn();
beforeEach(() => {
vi.clearAllMocks();
});
const defaultOptions: StorageCleanerOptions = {
localStorage: true,
sessionStorage: true,
indexedDB: true,
cookies: true,
cacheStorage: true,
serviceWorkers: true,
};
const renderComponent = (props?: Partial<React.ComponentProps<typeof StorageCleanerConfirm>>) => {
return render(
<StorageCleanerConfirm
open={true}
onClose={mockOnClose}
onConfirm={mockOnConfirm}
options={defaultOptions}
{...props}
/>,
);
};
describe('渲染测试', () => {
it('open 为 true 时应渲染对话框', () => {
renderComponent();
expect(screen.getByRole('heading', { name: /确认清理/ })).toBeInTheDocument();
});
it('应显示警告信息', () => {
renderComponent();
expect(screen.getByText(/不可撤销/)).toBeInTheDocument();
});
it('应将选中的选项显示为标签', () => {
renderComponent();
expect(screen.getByText(/Local Storage/)).toBeInTheDocument();
expect(screen.getByText(/Session Storage/)).toBeInTheDocument();
expect(screen.getByText(/Cookies/)).toBeInTheDocument();
});
it('应显示取消和确认按钮', () => {
renderComponent();
expect(screen.getByRole('button', { name: /取消/ })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /确认清理/ })).toBeInTheDocument();
});
});
describe('交互测试', () => {
it('点击取消时应调用 onClose', () => {
renderComponent();
fireEvent.click(screen.getByRole('button', { name: /取消/ }));
expect(mockOnClose).toHaveBeenCalledTimes(1);
expect(mockOnConfirm).not.toHaveBeenCalled();
});
it('点击确认时应调用 onConfirm', () => {
renderComponent();
fireEvent.click(screen.getByRole('button', { name: /确认清理/ }));
expect(mockOnConfirm).toHaveBeenCalledTimes(1);
expect(mockOnClose).not.toHaveBeenCalled();
});
});
describe('选项过滤测试', () => {
it('应仅显示选中的选项', () => {
const partialOptions: StorageCleanerOptions = {
localStorage: true,
sessionStorage: false,
indexedDB: true,
cookies: false,
cacheStorage: false,
serviceWorkers: false,
};
renderComponent({ options: partialOptions });
expect(screen.getByText(/Local Storage/)).toBeInTheDocument();
expect(screen.getByText(/IndexedDB/)).toBeInTheDocument();
expect(screen.queryByText(/Session Storage/)).not.toBeInTheDocument();
expect(screen.queryByText(/Cookies$/)).not.toBeInTheDocument();
});
it('应处理空选项', () => {
const emptyOptions: StorageCleanerOptions = {
localStorage: false,
sessionStorage: false,
indexedDB: false,
cookies: false,
cacheStorage: false,
serviceWorkers: false,
};
renderComponent({ options: emptyOptions });
const chips = screen.queryAllByRole('button');
expect(chips.length).toBeGreaterThanOrEqual(2);
});
});
describe('对话框行为测试', () => {
it('open 为 false 时不应渲染', () => {
renderComponent({ open: false });
expect(screen.queryByRole('heading', { name: /确认清理/ })).not.toBeInTheDocument();
});
it('应使用不同选项渲染', () => {
const customOptions: StorageCleanerOptions = {
localStorage: false,
sessionStorage: true,
indexedDB: false,
cookies: true,
cacheStorage: false,
serviceWorkers: false,
};
renderComponent({ options: customOptions });
expect(screen.getByText(/Session Storage/)).toBeInTheDocument();
expect(screen.getByText(/Cookies/)).toBeInTheDocument();
});
});
});
@@ -0,0 +1,121 @@
import { describe, expect, it, vi } from 'vitest';
import { fireEvent, render, screen } from '@testing-library/react';
import SwitchButtonGroup from '@/components/SwitchButtonGroup';
describe('SwitchButtonGroup 组件', () => {
const options = [
{ value: 'a', label: '选项A' },
{ value: 'b', label: '选项B' },
];
it('应渲染所有选项按钮', () => {
render(<SwitchButtonGroup value="a" options={options} onChange={vi.fn()} />);
expect(screen.getByRole('button', { name: /选项A/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /选项B/i })).toBeInTheDocument();
});
it('应高亮当前选中的按钮', () => {
render(<SwitchButtonGroup value="a" options={options} onChange={vi.fn()} />);
const buttonA = screen.getByRole('button', { name: /选项A/i });
const buttonB = screen.getByRole('button', { name: /选项B/i });
expect(buttonA).toHaveClass('bg-background', 'text-foreground', 'shadow-sm');
expect(buttonB).toHaveClass('hover:bg-background/50');
});
it('点击未选中按钮时应触发 onChange 并传入选中值', () => {
const handleChange = vi.fn();
render(<SwitchButtonGroup value="a" options={options} onChange={handleChange} />);
fireEvent.click(screen.getByRole('button', { name: /选项B/i }));
expect(handleChange).toHaveBeenCalledTimes(1);
expect(handleChange).toHaveBeenCalledWith('b');
});
it('点击已选中按钮时不应触发 onChange', () => {
const handleChange = vi.fn();
render(<SwitchButtonGroup value="a" options={options} onChange={handleChange} />);
fireEvent.click(screen.getByRole('button', { name: /选项A/i }));
expect(handleChange).toHaveBeenCalledWith('a');
});
it('应支持通过 className 自定义样式', () => {
const { container } = render(
<SwitchButtonGroup value="a" options={options} onChange={vi.fn()} className="custom-group" />,
);
const group = container.firstChild;
expect(group).toHaveClass('custom-group');
});
it('应支持 size 属性', () => {
render(<SwitchButtonGroup value="a" options={options} onChange={vi.fn()} size="small" />);
const button = screen.getByRole('button', { name: /选项A/i });
expect(button).toHaveClass('text-xs');
});
it('应支持 buttonSx 自定义按钮样式', () => {
render(<SwitchButtonGroup value="a" options={options} onChange={vi.fn()} />);
const button = screen.getByRole('button', { name: /选项A/i });
expect(button).toBeInTheDocument();
});
it('应支持 ReactNode 类型的 label', () => {
const nodeOptions = [{ value: 'x', label: <span data-testid="custom-label"></span> }];
render(<SwitchButtonGroup value="x" options={nodeOptions} onChange={vi.fn()} />);
expect(screen.getByTestId('custom-label')).toBeInTheDocument();
});
it('默认按钮样式应禁止文字换行', () => {
render(<SwitchButtonGroup value="a" options={options} onChange={vi.fn()} />);
const button = screen.getByRole('button', { name: /选项A/i });
expect(button).toHaveClass('whitespace-nowrap');
});
it('buttonSx 传入时应覆盖默认换行样式', () => {
render(<SwitchButtonGroup value="a" options={options} onChange={vi.fn()} />);
const button = screen.getByRole('button', { name: /选项A/i });
expect(button).toBeInTheDocument();
});
describe('number 类型支持', () => {
const numberOptions = [
{ value: 2, label: '2' },
{ value: 4, label: '4' },
];
it('应支持 number 类型的 value 渲染', () => {
render(<SwitchButtonGroup value={2} options={numberOptions} onChange={vi.fn()} />);
expect(screen.getByRole('button', { name: /^2$/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /^4$/i })).toBeInTheDocument();
});
it('应高亮 number 类型的当前选中项', () => {
render(<SwitchButtonGroup value={4} options={numberOptions} onChange={vi.fn()} />);
const button2 = screen.getByRole('button', { name: /^2$/i });
const button4 = screen.getByRole('button', { name: /^4$/i });
expect(button2).toHaveClass('hover:bg-background/50');
expect(button4).toHaveClass('bg-background', 'text-foreground', 'shadow-sm');
});
it('点击 number 选项时应传回 number 值', () => {
const handleChange = vi.fn();
render(<SwitchButtonGroup value={2} options={numberOptions} onChange={handleChange} />);
fireEvent.click(screen.getByRole('button', { name: /^4$/i }));
expect(handleChange).toHaveBeenCalledTimes(1);
expect(handleChange).toHaveBeenCalledWith(4);
});
});
});
@@ -0,0 +1,489 @@
import { describe, expect, it, vi } from 'vitest';
import { fireEvent, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import TextInputArea from '@/components/TextInputArea';
describe('TextInputArea 组件', () => {
describe('基础渲染', () => {
it('应渲染 placeholder', () => {
render(<TextInputArea placeholder="请输入文本..." />);
expect(screen.getByPlaceholderText('请输入文本...')).toBeInTheDocument();
});
it('应渲染传入的 value', () => {
render(<TextInputArea value="测试内容" onChange={() => {}} />);
const textarea = screen.getByRole('textbox');
expect(textarea).toHaveValue('测试内容');
});
it('默认显示清空按钮', () => {
render(<TextInputArea value="有内容" onChange={() => {}} />);
expect(screen.getByRole('button', { name: '清空' })).toBeInTheDocument();
});
it('无内容时清空按钮应隐藏', () => {
render(<TextInputArea value="" onChange={() => {}} />);
expect(screen.queryByRole('button', { name: '清空' })).not.toBeInTheDocument();
});
it('disabled 时清空按钮应隐藏', () => {
render(<TextInputArea value="内容" onChange={() => {}} disabled />);
expect(screen.queryByTitle('清空')).not.toBeInTheDocument();
});
it('readOnly 时清空按钮应隐藏', () => {
render(<TextInputArea value="内容" onChange={() => {}} readOnly />);
expect(screen.queryByTitle('清空')).not.toBeInTheDocument();
});
it('showClear=false 时不显示清空按钮', () => {
render(<TextInputArea value="内容" onChange={() => {}} showClear={false} />);
expect(screen.queryByTitle('清空')).not.toBeInTheDocument();
});
});
describe('受控模式', () => {
it('输入时触发 onChange', () => {
const handleChange = vi.fn();
render(<TextInputArea value="" onChange={handleChange} />);
const textarea = screen.getByRole('textbox');
fireEvent.change(textarea, { target: { value: '新内容' } });
expect(handleChange).toHaveBeenCalledWith('新内容');
});
it('清空按钮触发 onChange("")', () => {
const handleChange = vi.fn();
render(<TextInputArea value="内容" onChange={handleChange} />);
fireEvent.click(screen.getByRole('button', { name: '清空' }));
expect(handleChange).toHaveBeenCalledWith('');
});
});
describe('非受控模式', () => {
it('defaultValue 应显示初始值', () => {
render(<TextInputArea defaultValue="初始值" />);
expect(screen.getByRole('textbox')).toHaveValue('初始值');
});
it('输入后应更新内部值', () => {
render(<TextInputArea defaultValue="" />);
const textarea = screen.getByRole('textbox');
fireEvent.change(textarea, { target: { value: '新内容' } });
expect(textarea).toHaveValue('新内容');
});
it('清空按钮应清空内容', () => {
render(<TextInputArea defaultValue="内容" />);
fireEvent.click(screen.getByRole('button', { name: '清空' }));
expect(screen.getByRole('textbox')).toHaveValue('');
});
});
describe('allowCopy 复制功能', () => {
it('allowCopy 且有内容时显示复制按钮', () => {
render(<TextInputArea value="可复制的内容" onChange={() => {}} allowCopy />);
expect(screen.getByRole('button', { name: '复制内容' })).toBeInTheDocument();
});
it('allowCopy 但无内容时隐藏复制按钮', () => {
render(<TextInputArea value="" onChange={() => {}} allowCopy />);
expect(screen.queryByRole('button', { name: '复制内容' })).not.toBeInTheDocument();
});
it('allowCopy=false 时不显示复制按钮', () => {
render(<TextInputArea value="内容" onChange={() => {}} />);
expect(
screen.queryByRole('button', { name: 'textInputArea.copyContent' }),
).not.toBeInTheDocument();
});
it('复制时调用 clipboard writeText', async () => {
const user = userEvent.setup();
const writeTextSpy = vi.spyOn(navigator.clipboard, 'writeText').mockResolvedValue(undefined);
render(<TextInputArea value="测试" onChange={() => {}} allowCopy />);
await user.click(screen.getByRole('button', { name: '复制内容' }));
expect(writeTextSpy).toHaveBeenCalledWith('测试');
});
it('复制失败时调用 clipboard writeText 并捕获错误', async () => {
const user = userEvent.setup();
const writeTextSpy = vi
.spyOn(navigator.clipboard, 'writeText')
.mockRejectedValue(new Error('失败'));
render(<TextInputArea value="测试" onChange={() => {}} allowCopy />);
await user.click(screen.getByRole('button', { name: '复制内容' }));
expect(writeTextSpy).toHaveBeenCalledWith('测试');
});
});
describe('showCount 字符计数', () => {
it('显示当前字符数', () => {
render(<TextInputArea value="hello" onChange={() => {}} showCount />);
expect(screen.getByText('5')).toBeInTheDocument();
});
it('空内容时显示 0', () => {
render(<TextInputArea value="" onChange={() => {}} showCount />);
expect(screen.getByText('0')).toBeInTheDocument();
});
it('设置 maxLength 时显示计数上限', () => {
render(<TextInputArea value="ab" onChange={() => {}} showCount maxLength={10} />);
expect(screen.getByText('2 / 10')).toBeInTheDocument();
});
});
describe('maxLength', () => {
it('超出 maxLength 的输入应被截断', () => {
const handleChange = vi.fn();
render(<TextInputArea value="" onChange={handleChange} maxLength={5} />);
const textarea = screen.getByRole('textbox');
fireEvent.change(textarea, { target: { value: '123456' } });
expect(handleChange).not.toHaveBeenCalledWith('123456');
expect(screen.getByText('内容不能超过 5 个字符')).toBeInTheDocument();
});
it('未超出 maxLength 的输入应正常触发', () => {
const handleChange = vi.fn();
render(<TextInputArea value="" onChange={handleChange} maxLength={5} />);
const textarea = screen.getByRole('textbox');
fireEvent.change(textarea, { target: { value: '123' } });
expect(handleChange).toHaveBeenCalledWith('123');
});
});
describe('验证规则', () => {
it('onChange 触发时验证失败应设置 error', () => {
render(
<TextInputArea
value=""
onChange={() => {}}
validateTrigger="onChange"
rules={[{ validator: (v) => v.length >= 3, message: '至少3个字符' }]}
/>,
);
const textarea = screen.getByRole('textbox');
fireEvent.change(textarea, { target: { value: 'ab' } });
expect(screen.getByText('至少3个字符')).toBeInTheDocument();
});
it('onBlur 触发时验证失败应设置 error', () => {
render(
<TextInputArea
value="ab"
onChange={() => {}}
validateTrigger="onBlur"
rules={[{ validator: (v) => v.length >= 3, message: '至少3个字符' }]}
/>,
);
const textarea = screen.getByRole('textbox');
fireEvent.blur(textarea);
expect(screen.getByText('至少3个字符')).toBeInTheDocument();
});
it('验证通过不应显示错误', () => {
render(
<TextInputArea
value="abc"
onChange={() => {}}
validateTrigger="onChange"
rules={[{ validator: (v) => v.length >= 3, message: '至少3个字符' }]}
/>,
);
const textarea = screen.getByRole('textbox');
fireEvent.change(textarea, { target: { value: 'abcd' } });
expect(screen.queryByText('至少3个字符')).not.toBeInTheDocument();
});
it('onAction 触发时验证失败应阻止 action 执行', () => {
const handleAction = vi.fn();
render(
<TextInputArea
value="ab"
onChange={() => {}}
validateTrigger="onAction"
rules={[{ validator: (v) => v.length >= 3, message: '至少3个字符' }]}
actions={[
{
key: 'test',
label: '执行',
onClick: handleAction,
},
]}
/>,
);
fireEvent.click(screen.getByText('执行'));
expect(handleAction).not.toHaveBeenCalled();
expect(screen.getByText('至少3个字符')).toBeInTheDocument();
});
});
describe('操作栏 actions', () => {
it('应渲染顶部操作按钮', () => {
render(
<TextInputArea
value="内容"
onChange={() => {}}
actions={[{ key: 'top-action', label: '顶部操作', onClick: vi.fn() }]}
/>,
);
expect(screen.getByText('顶部操作')).toBeInTheDocument();
});
it('应渲染底部操作按钮', () => {
render(
<TextInputArea
value="内容"
onChange={() => {}}
actions={[
{ key: 'bottom-action', label: '底部操作', position: 'bottom', onClick: vi.fn() },
]}
/>,
);
expect(screen.getByText('底部操作')).toBeInTheDocument();
});
it('点击操作按钮触发 onClick', () => {
const handleClick = vi.fn();
render(
<TextInputArea
value="内容"
onChange={() => {}}
actions={[{ key: 'act', label: '操作', onClick: handleClick }]}
/>,
);
fireEvent.click(screen.getByText('操作'));
expect(handleClick).toHaveBeenCalledWith(
'内容',
expect.objectContaining({
clear: expect.any(Function),
setError: expect.any(Function),
}),
);
});
it('disabled 为 true 时按钮应禁用', () => {
render(
<TextInputArea
value=""
onChange={() => {}}
actions={[{ key: 'act', label: '操作', onClick: vi.fn(), disabled: true }]}
/>,
);
expect(screen.getByText('操作')).toBeDisabled();
});
it('disabled 为函数且返回 true 时按钮应禁用', () => {
render(
<TextInputArea
value=""
onChange={() => {}}
actions={[{ key: 'act', label: '操作', onClick: vi.fn(), disabled: (v) => !v }]}
/>,
);
expect(screen.getByText('操作')).toBeDisabled();
});
it('primary 类型按钮应使用 contained 样式', () => {
render(
<TextInputArea
value="内容"
onChange={() => {}}
actions={[
{ key: 'p', label: '主要', type: 'primary', position: 'bottom', onClick: vi.fn() },
]}
/>,
);
const btn = screen.getByText('主要');
expect(btn).toHaveClass('bg-primary', 'text-primary-foreground');
});
});
describe('title', () => {
it('应渲染 title', () => {
render(<TextInputArea title="输入区域" value="" onChange={() => {}} />);
expect(screen.getByText('输入区域')).toBeInTheDocument();
});
it('不设置 title 时不渲染标题', () => {
const { container } = render(<TextInputArea value="" onChange={() => {}} />);
expect(container.querySelector('.text-muted-foreground')).not.toBeInTheDocument();
});
});
describe('disabled 和 readOnly', () => {
it('disabled 时输入框应禁用', () => {
render(<TextInputArea value="内容" onChange={() => {}} disabled />);
expect(screen.getByRole('textbox')).toBeDisabled();
});
it('readOnly 时输入框应只读', () => {
render(<TextInputArea value="内容" onChange={() => {}} readOnly />);
expect(screen.getByRole('textbox')).not.toBeDisabled();
});
});
describe('autoFocus', () => {
it('autoFocus 应自动聚焦', () => {
render(<TextInputArea autoFocus value="" onChange={() => {}} />);
expect(document.activeElement).toBe(screen.getByRole('textbox'));
});
});
describe('复制功能', () => {
it('复制成功时调用 clipboard writeText', async () => {
const user = userEvent.setup();
const writeTextSpy = vi.spyOn(navigator.clipboard, 'writeText').mockResolvedValue(undefined);
render(<TextInputArea value="测试" onChange={() => {}} allowCopy />);
await user.click(screen.getByRole('button', { name: '复制内容' }));
expect(writeTextSpy).toHaveBeenCalledWith('测试');
});
});
describe('onClear 回调', () => {
it('点击清空按钮时应调用 onClear', () => {
const handleClear = vi.fn();
render(<TextInputArea value="内容" onChange={() => {}} onClear={handleClear} />);
fireEvent.click(screen.getByRole('button', { name: '清空' }));
expect(handleClear).toHaveBeenCalledOnce();
});
it('不传 onClear 时清空按钮应正常工作', () => {
render(<TextInputArea defaultValue="内容" />);
fireEvent.click(screen.getByRole('button', { name: '清空' }));
expect(screen.getByRole('textbox')).toHaveValue('');
});
});
describe('externalError 外部错误', () => {
it('设置 externalError 时应显示错误状态', () => {
render(<TextInputArea value="内容" onChange={() => {}} externalError="JSON 格式无效" />);
expect(screen.getByText('JSON 格式无效')).toBeInTheDocument();
});
it('externalError 为空时应隐藏错误状态', () => {
const { rerender } = render(
<TextInputArea value="内容" onChange={() => {}} externalError="错误" />,
);
expect(screen.getByText('错误')).toBeInTheDocument();
rerender(<TextInputArea value="内容" onChange={() => {}} externalError="" />);
expect(screen.queryByText('错误')).not.toBeInTheDocument();
});
it('externalError 优先级高于内部验证错误', () => {
render(
<TextInputArea
value="ab"
onChange={() => {}}
externalError="外部错误"
validateTrigger="onChange"
rules={[{ validator: (v) => v.length >= 3, message: '内部验证错误' }]}
/>,
);
expect(screen.getByText('外部错误')).toBeInTheDocument();
expect(screen.queryByText('内部验证错误')).not.toBeInTheDocument();
});
it('externalError 清除后应显示内部验证错误', () => {
const { rerender } = render(
<TextInputArea
value="ab"
onChange={() => {}}
externalError="外部错误"
validateTrigger="onChange"
rules={[{ validator: (v) => v.length >= 3, message: '至少3个字符' }]}
/>,
);
expect(screen.getByText('外部错误')).toBeInTheDocument();
rerender(
<TextInputArea
value="ab"
onChange={() => {}}
validateTrigger="onChange"
rules={[{ validator: (v) => v.length >= 3, message: '至少3个字符' }]}
/>,
);
expect(screen.queryByText('外部错误')).not.toBeInTheDocument();
const textarea = screen.getByRole('textbox');
fireEvent.change(textarea, { target: { value: 'a' } });
expect(screen.getByText('至少3个字符')).toBeInTheDocument();
});
});
describe('autoResize', () => {
it('autoResize=true 时设置 minRows/maxRows', () => {
const { container } = render(
<TextInputArea value="" onChange={() => {}} minRows={3} maxRows={8} />,
);
const textarea = container.querySelector('textarea');
expect(textarea).toBeInTheDocument();
});
it('autoResize=false 时设置固定 rows', () => {
const { container } = render(<TextInputArea value="" onChange={() => {}} minRows={5} />);
const textarea = container.querySelector('textarea');
expect(textarea).toBeInTheDocument();
});
});
describe('样式集成', () => {
it('应透传 className', () => {
const { container } = render(
<TextInputArea value="" onChange={() => {}} className="custom-class" />,
);
expect(container.firstChild).toHaveClass('custom-class');
});
});
});
+40
View File
@@ -0,0 +1,40 @@
# components/ui/
shadcn/ui 基础原子组件目录,基于 Radix UI 原语 + Tailwind CSS 实现。
## 组件列表
| 组件 | 用途 |
| -------------- | -------------------------------------------------------------------------------------------------- |
| `button.tsx` | 按钮组件,支持 `default/destructive/outline/secondary/ghost/link` 变体和 `default/sm/lg/icon` 尺寸 |
| `input.tsx` | 标准输入框,统一的 ring/focus 样式 |
| `select.tsx` | 下拉选择组件,包含 Trigger、Content、Item 等子组件 |
| `dialog.tsx` | 对话框组件,包含 Overlay、Content、Header、Footer、Title、Description |
| `checkbox.tsx` | 复选框组件 |
| `label.tsx` | 标签组件 |
| `switch.tsx` | 开关组件 |
| `badge.tsx` | 徽章组件,支持 `default/secondary/destructive/outline` 变体 |
## 使用方式
```tsx
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
```
## 添加新组件
使用 shadcn/ui CLI 添加新组件:
```bash
npx shadcn-ui@latest add <component-name>
```
组件配置在项目根目录的 `components.json` 中定义。
+32
View File
@@ -0,0 +1,32 @@
import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const badgeVariants = cva(
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
{
variants: {
variant: {
default: 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
secondary:
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
destructive:
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
outline: 'text-foreground',
},
},
defaultVariants: {
variant: 'default',
},
},
);
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
}
export { Badge, badgeVariants };
+49
View File
@@ -0,0 +1,49 @@
import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const buttonVariants = cva(
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10',
iconSm: 'h-7 w-7',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
);
},
);
Button.displayName = 'Button';
export { Button, buttonVariants };
+26
View File
@@ -0,0 +1,26 @@
import * as React from 'react';
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
import { Check } from 'lucide-react';
import { cn } from '@/lib/utils';
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
'grid place-content-center peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator className="grid place-content-center text-current">
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox };
+109
View File
@@ -0,0 +1,109 @@
import * as React from 'react';
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { X } from 'lucide-react';
import { cn } from '@/lib/utils';
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
'fixed inset-0 z-50 bg-black/80 dark:bg-black/60 transition-opacity duration-200 data-[state=closed]:opacity-0 data-[state=open]:opacity-100',
className,
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
interface DialogContentProps extends React.ComponentPropsWithoutRef<
typeof DialogPrimitive.Content
> {
showCloseButton?: boolean;
}
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
DialogContentProps
>(({ className, children, showCloseButton = true, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-white dark:bg-gray-900 p-6 shadow-lg transition-all duration-200 data-[state=closed]:scale-95 data-[state=open]:scale-100 data-[state=closed]:opacity-0 data-[state=open]:opacity-100 sm:rounded-lg',
className,
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)} {...props} />
);
DialogHeader.displayName = 'DialogHeader';
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
{...props}
/>
);
DialogFooter.displayName = 'DialogFooter';
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn('text-lg font-semibold leading-none tracking-tight', className)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
};
+21
View File
@@ -0,0 +1,21 @@
import * as React from 'react';
import { cn } from '@/lib/utils';
const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
ref={ref}
{...props}
/>
);
},
);
Input.displayName = 'Input';
export { Input };
+19
View File
@@ -0,0 +1,19 @@
import * as React from 'react';
import * as LabelPrimitive from '@radix-ui/react-label';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const labelVariants = cva(
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
);
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
));
Label.displayName = LabelPrimitive.Root.displayName;
export { Label };
+150
View File
@@ -0,0 +1,150 @@
import * as React from 'react';
import * as SelectPrimitive from '@radix-ui/react-select';
import { Check, ChevronDown, ChevronUp } from 'lucide-react';
import { cn } from '@/lib/utils';
const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn('flex cursor-default items-center justify-center py-1', className)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn('flex cursor-default items-center justify-center py-1', className)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
));
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = 'popper', ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md transition-all duration-200 data-[state=closed]:scale-95 data-[state=open]:scale-100 data-[state=closed]:opacity-0 data-[state=open]:opacity-100',
position === 'popper' &&
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
className,
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
'p-1',
position === 'popper' &&
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]',
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn('py-1.5 pl-8 pr-2 text-sm font-semibold', className)}
{...props}
/>
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn('-mx-1 my-1 h-px bg-muted', className)}
{...props}
/>
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
};
+26
View File
@@ -0,0 +1,26 @@
import { Toaster as Sonner } from 'sonner';
import { useThemeMode } from '@/providers/ThemeModeProvider';
type ToasterProps = React.ComponentProps<typeof Sonner>;
export function Toaster(props: ToasterProps) {
const { resolvedMode } = useThemeMode();
return (
<Sonner
theme={resolvedMode}
className="toaster group"
position="bottom-center"
toastOptions={{
classNames: {
toast:
'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',
description: 'group-[.toast]:text-muted-foreground',
actionButton: 'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',
cancelButton: 'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground',
},
}}
{...props}
/>
);
}
+27
View File
@@ -0,0 +1,27 @@
import * as React from 'react';
import * as SwitchPrimitives from '@radix-ui/react-switch';
import { cn } from '@/lib/utils';
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
className,
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
'pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0',
)}
/>
</SwitchPrimitives.Root>
));
Switch.displayName = SwitchPrimitives.Root.displayName;
export { Switch };
@@ -0,0 +1,87 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
const pageLoadTracker = vi.hoisted(() => ({
loaded: [] as string[],
}));
vi.mock('@/pages/Dashboard', () => {
pageLoadTracker.loaded.push('Dashboard');
return { default: () => null };
});
vi.mock('@/pages/Timestamp', () => {
pageLoadTracker.loaded.push('Timestamp');
return { default: () => null };
});
vi.mock('@/pages/StorageCleaner', () => {
pageLoadTracker.loaded.push('StorageCleaner');
return { default: () => null };
});
vi.mock('@/pages/QrCode', () => {
pageLoadTracker.loaded.push('QrCode');
return { default: () => null };
});
vi.mock('@/pages/TextStatistics', () => {
pageLoadTracker.loaded.push('TextStatistics');
return { default: () => null };
});
vi.mock('@/pages/Jwt', () => {
pageLoadTracker.loaded.push('Jwt');
return { default: () => null };
});
vi.mock('@/pages/JsonTools', () => {
pageLoadTracker.loaded.push('JsonTools');
return { default: () => null };
});
vi.mock('@/pages/Base64Converter', () => {
pageLoadTracker.loaded.push('Base64Converter');
return { default: () => null };
});
vi.mock('@/pages/RightClickRestorer', () => {
pageLoadTracker.loaded.push('RightClickRestorer');
return { default: () => null };
});
vi.mock('@/pages/TestDataGenerator', () => {
pageLoadTracker.loaded.push('TestDataGenerator');
return { default: () => null };
});
describe('features 懒加载', () => {
beforeEach(() => {
pageLoadTracker.loaded.length = 0;
vi.resetModules();
});
it('仅导入工具函数时不应加载任何页面模块', async () => {
const { getAllFeatureKeys, getDefaultPageOrder } = await import('@/config/features');
getAllFeatureKeys();
getDefaultPageOrder();
expect(pageLoadTracker.loaded).toEqual([]);
});
it('访问 FEATURES 元数据时不应加载任何页面模块', async () => {
const { FEATURES } = await import('@/config/features');
expect(FEATURES).toHaveLength(10);
expect(pageLoadTracker.loaded).toEqual([]);
});
it('loadPage 应仅加载对应页面模块', async () => {
const { loadPage } = await import('@/config/pageLoaders/index');
await loadPage('dashboard');
expect(pageLoadTracker.loaded).toEqual(['Dashboard']);
});
it('loadPage 切换页面时不应加载无关页面模块', async () => {
const { loadPage } = await import('@/config/pageLoaders/index');
await loadPage('timestamp');
expect(pageLoadTracker.loaded).toEqual(['Timestamp']);
expect(pageLoadTracker.loaded).not.toContain('QrCode');
expect(pageLoadTracker.loaded).not.toContain('TestDataGenerator');
});
});
+125
View File
@@ -0,0 +1,125 @@
import { describe, expect, it } from 'vitest';
import {
FEATURES,
getAllFeatureKeys,
getDefaultPageOrder,
getDefaultVisibleFeatureKeys,
getFeatureByKey,
} from '@/config/features';
describe('features', () => {
describe('FEATURES', () => {
it('应该有10个功能定义', () => {
expect(FEATURES).toHaveLength(10);
});
it('应该有每个功能的所有必需属性', () => {
FEATURES.forEach((feature) => {
expect(feature).toHaveProperty('key');
expect(feature).toHaveProperty('label');
expect(feature).toHaveProperty('description');
expect(feature).toHaveProperty('defaultVisible');
expect(feature).not.toHaveProperty('component');
expect(typeof feature.key).toBe('string');
expect(typeof feature.label).toBe('string');
expect(typeof feature.description).toBe('string');
expect(typeof feature.defaultVisible).toBe('boolean');
if (feature.key !== 'dashboard') {
expect(feature).toHaveProperty('icon');
expect(feature).toHaveProperty('themeColorKey');
expect(typeof feature.themeColorKey).toBe('string');
}
});
});
it('应该有每个功能的唯一key', () => {
const keys = FEATURES.map((f) => f.key);
const uniqueKeys = new Set(keys);
expect(uniqueKeys.size).toBe(keys.length);
});
});
describe('getFeatureByKey', () => {
it('应该返回dashboard功能', () => {
const feature = getFeatureByKey('dashboard');
expect(feature).toBeDefined();
expect(feature?.key).toBe('dashboard');
expect(feature?.label).toBe('仪表盘');
});
it('应该返回时间戳功能', () => {
const feature = getFeatureByKey('timestamp');
expect(feature).toBeDefined();
expect(feature?.key).toBe('timestamp');
expect(feature?.label).toBe('时间戳');
expect(feature?.themeColorKey).toBeDefined();
});
it('应该返回存储清理功能', () => {
const feature = getFeatureByKey('storageCleaner');
expect(feature).toBeDefined();
expect(feature?.key).toBe('storageCleaner');
expect(feature?.label).toBe('存储清理');
});
it('应该返回undefined用于无效的key', () => {
const feature = getFeatureByKey('invalid' as any);
expect(feature).toBeUndefined();
});
});
describe('getDefaultVisibleFeatureKeys', () => {
it('应该返回仅可见的功能', () => {
const visibleKeys = getDefaultVisibleFeatureKeys();
visibleKeys.forEach((key) => {
const feature = getFeatureByKey(key);
expect(feature?.defaultVisible).toBe(true);
});
});
it('应该包含仪表盘、时间戳、存储清理、二维码', () => {
const visibleKeys = getDefaultVisibleFeatureKeys();
expect(visibleKeys).toContain('dashboard');
expect(visibleKeys).toContain('timestamp');
expect(visibleKeys).toContain('storageCleaner');
expect(visibleKeys).toContain('qrCode');
});
});
describe('getAllFeatureKeys', () => {
it('应该返回所有功能key', () => {
const allKeys = getAllFeatureKeys();
expect(allKeys).toHaveLength(10);
expect(allKeys).toContain('dashboard');
expect(allKeys).toContain('timestamp');
expect(allKeys).toContain('storageCleaner');
expect(allKeys).toContain('qrCode');
expect(allKeys).toContain('textStatistics');
expect(allKeys).toContain('jwt');
expect(allKeys).toContain('jsonTools');
expect(allKeys).toContain('base64Converter');
expect(allKeys).toContain('rightClickRestorer');
expect(allKeys).toContain('testDataGenerator');
});
});
describe('getDefaultPageOrder', () => {
it('应该排除仪表盘从页面顺序', () => {
const pageOrder = getDefaultPageOrder();
expect(pageOrder).not.toContain('dashboard');
});
it('应该包含时间戳、存储清理、二维码在页面顺序', () => {
const pageOrder = getDefaultPageOrder();
expect(pageOrder).toContain('timestamp');
expect(pageOrder).toContain('storageCleaner');
expect(pageOrder).toContain('qrCode');
});
it('应该有9个项目在页面顺序', () => {
const pageOrder = getDefaultPageOrder();
expect(pageOrder).toHaveLength(9);
});
});
});
+133
View File
@@ -0,0 +1,133 @@
import type { ComponentType } from 'react';
import type { LucideProps } from 'lucide-react';
import type { PageType } from '@/types/storage';
import {
Clock,
Database,
QrCode,
FileText,
Key,
GitCompareArrows,
ArrowLeftRight,
MousePointerClick,
FileSpreadsheet,
} from 'lucide-react';
export type PaletteColorKey = 'primary' | 'success' | 'warning' | 'error' | 'secondary' | 'info';
export interface FeatureConfig {
key: PageType;
label: string;
description: string;
themeColorKey?: PaletteColorKey;
icon?: ComponentType<LucideProps>;
defaultVisible: boolean;
}
export const FEATURES: FeatureConfig[] = [
{
key: 'dashboard',
label: '仪表盘',
description: '',
defaultVisible: true,
},
{
key: 'timestamp',
label: '时间戳',
description: 'Unix 毫秒数转换与格式化',
themeColorKey: 'primary',
icon: Clock,
defaultVisible: true,
},
{
key: 'storageCleaner',
label: '存储清理',
description: '清理缓存、Cookies 及本地存储',
themeColorKey: 'warning',
icon: Database,
defaultVisible: true,
},
{
key: 'qrCode',
label: '二维码工具',
description: '生成当前选中的 URL 的二维码',
themeColorKey: 'success',
icon: QrCode,
defaultVisible: true,
},
{
key: 'textStatistics',
label: '文本统计',
description: '实时分析文本字符、单词及字节',
themeColorKey: 'secondary',
icon: FileText,
defaultVisible: true,
},
{
key: 'jwt',
label: 'JWT 解析',
description: 'JSON Web Token 解码与查看',
themeColorKey: 'info',
icon: Key,
defaultVisible: true,
},
{
key: 'jsonTools',
label: 'JSON 工具',
description: '差异比较、格式化、YAML/TOML 转换及压缩',
themeColorKey: 'primary',
icon: GitCompareArrows,
defaultVisible: true,
},
{
key: 'base64Converter',
label: 'Base64 转换器',
description: '文本、文件与图像的 Base64 编码转换',
themeColorKey: 'info',
icon: ArrowLeftRight,
defaultVisible: true,
},
{
key: 'rightClickRestorer',
label: '右键恢复',
description: '检测并恢复被网站禁用的浏览器右键菜单',
themeColorKey: 'success',
icon: MousePointerClick,
defaultVisible: true,
},
{
key: 'testDataGenerator',
label: '测试数据生成器',
description: '自定义规则批量生成测试数据',
themeColorKey: 'warning',
icon: FileSpreadsheet,
defaultVisible: true,
},
];
export function getFeatureByKey(key: PageType): FeatureConfig | undefined {
return FEATURES.find((f) => f.key === key);
}
export function getDefaultVisibleFeatureKeys(): PageType[] {
return FEATURES.filter((f) => f.defaultVisible).map((f) => f.key);
}
export function getAllFeatureKeys(): PageType[] {
return FEATURES.map((f) => f.key);
}
export function getDefaultPageOrder(): PageType[] {
return FEATURES.filter((f) => f.key !== 'dashboard').map((f) => f.key);
}
export function getEntryPointType(): 'popup' | 'sidepanel' | 'tab' {
const pathname = window.location.pathname;
if (pathname.includes('sidepanel')) {
return 'sidepanel';
}
if (new URLSearchParams(window.location.search).get('mode') === 'tab') {
return 'tab';
}
return 'popup';
}
+9
View File
@@ -0,0 +1,9 @@
export type { FeatureConfig, PaletteColorKey } from '@/config/featureMeta';
export {
FEATURES,
getAllFeatureKeys,
getDefaultPageOrder,
getDefaultVisibleFeatureKeys,
getEntryPointType,
getFeatureByKey,
} from '@/config/featureMeta';
@@ -0,0 +1,5 @@
import type { ComponentType } from 'react';
export default function loadBase64Converter(): Promise<{ default: ComponentType }> {
return import('@/pages/Base64Converter');
}
+5
View File
@@ -0,0 +1,5 @@
import type { ComponentType } from 'react';
export default function loadDashboard(): Promise<{ default: ComponentType }> {
return import('@/pages/Dashboard');
}
+45
View File
@@ -0,0 +1,45 @@
import type { ComponentType } from 'react';
import type { PageType } from '@/types/storage';
type PageModule = { default: ComponentType };
let pagesStylesLoaded = false;
function ensurePagesStyles(): Promise<unknown> {
if (pagesStylesLoaded) {
return Promise.resolve();
}
pagesStylesLoaded = true;
return import('@/styles/pages.css');
}
export function loadPage(key: PageType): Promise<PageModule> {
return ensurePagesStyles().then(() => {
switch (key) {
case 'dashboard':
return import('@/config/pageLoaders/dashboard').then((m) => m.default());
case 'timestamp':
return import('@/config/pageLoaders/timestamp').then((m) => m.default());
case 'storageCleaner':
return import('@/config/pageLoaders/storageCleaner').then((m) => m.default());
case 'qrCode':
return import('@/config/pageLoaders/qrCode').then((m) => m.default());
case 'textStatistics':
return import('@/config/pageLoaders/textStatistics').then((m) => m.default());
case 'jwt':
return import('@/config/pageLoaders/jwt').then((m) => m.default());
case 'jsonTools':
return import('@/config/pageLoaders/jsonTools').then((m) => m.default());
case 'base64Converter':
return import('@/config/pageLoaders/base64Converter').then((m) => m.default());
case 'rightClickRestorer':
return import('@/config/pageLoaders/rightClickRestorer').then((m) => m.default());
case 'testDataGenerator':
return import('@/config/pageLoaders/testDataGenerator').then((m) => m.default());
default: {
const _exhaustive: never = key;
return Promise.reject(new Error(`Unknown page: ${String(_exhaustive)}`));
}
}
});
}
+5
View File
@@ -0,0 +1,5 @@
import type { ComponentType } from 'react';
export default function loadJsonTools(): Promise<{ default: ComponentType }> {
return import('@/pages/JsonTools');
}
+5
View File
@@ -0,0 +1,5 @@
import type { ComponentType } from 'react';
export default function loadJwt(): Promise<{ default: ComponentType }> {
return import('@/pages/Jwt');
}
+5
View File
@@ -0,0 +1,5 @@
import type { ComponentType } from 'react';
export default function loadQrCode(): Promise<{ default: ComponentType }> {
return import('@/pages/QrCode');
}
@@ -0,0 +1,5 @@
import type { ComponentType } from 'react';
export default function loadRightClickRestorer(): Promise<{ default: ComponentType }> {
return import('@/pages/RightClickRestorer');
}
+5
View File
@@ -0,0 +1,5 @@
import type { ComponentType } from 'react';
export default function loadStorageCleaner(): Promise<{ default: ComponentType }> {
return import('@/pages/StorageCleaner');
}
@@ -0,0 +1,5 @@
import type { ComponentType } from 'react';
export default function loadTestDataGenerator(): Promise<{ default: ComponentType }> {
return import('@/pages/TestDataGenerator');
}
+5
View File
@@ -0,0 +1,5 @@
import type { ComponentType } from 'react';
export default function loadTextStatistics(): Promise<{ default: ComponentType }> {
return import('@/pages/TextStatistics');
}
+5
View File
@@ -0,0 +1,5 @@
import type { ComponentType } from 'react';
export default function loadTimestamp(): Promise<{ default: ComponentType }> {
return import('@/pages/Timestamp');
}

Some files were not shown because too many files have changed in this diff Show More