소스 검색

Merge pull request #513 from zxlie/crx-dominant

Import FeHelper.Crx full history, keep CRX codebase via -s ours.
Alien-阿烈叔(烈神) 5 일 전
부모
커밋
17fe817d74
100개의 변경된 파일20341개의 추가작업 그리고 4380개의 파일을 삭제
  1. 9 0
      .babelrc
  2. 45 0
      .cursor/rules/fh.mdc
  3. 53 0
      .cursor/rules/hs.mdc
  4. 12 0
      .cursorignore
  5. 0 32
      .cursorrules
  6. 5 3
      .gitignore
  7. 0 76
      CODE_OF_CONDUCT.md
  8. 66 0
      README.md
  9. 0 505
      README_NEW.md
  10. 0 16
      README_TEST.md
  11. 79 23
      apps/aiagent/fh.ai.js
  12. 112 1
      apps/aiagent/index.css
  13. 73 48
      apps/aiagent/index.html
  14. 339 15
      apps/aiagent/index.js
  15. 144 93
      apps/background/awesome.js
  16. 440 59
      apps/background/background.js
  17. 5 3
      apps/background/monkey.js
  18. 427 0
      apps/background/statistics.js
  19. 55 8
      apps/background/tools.js
  20. 4 1
      apps/chart-maker/index.html
  21. 48 21
      apps/chart-maker/main.js
  22. 90 6
      apps/chart-maker/style.css
  23. 98 0
      apps/chrome.json
  24. 2 199
      apps/code-beautify/beautify-css.js
  25. 186 0
      apps/code-beautify/beautify-worker.js
  26. 35 11
      apps/code-beautify/content-script.js
  27. 17 0
      apps/code-beautify/index.css
  28. 10 1
      apps/code-beautify/index.html
  29. 233 39
      apps/code-beautify/index.js
  30. 54 0
      apps/code-compress/examples.js
  31. 16 0
      apps/code-compress/index.css
  32. 9 1
      apps/code-compress/index.html
  33. 78 1
      apps/code-compress/index.js
  34. 5 2
      apps/crontab/index.html
  35. 17 1
      apps/crontab/index.js
  36. 1583 0
      apps/datetime-calc/index.css
  37. 474 0
      apps/datetime-calc/index.html
  38. 1152 0
      apps/datetime-calc/index.js
  39. 1 1
      apps/devtools/index.html
  40. 97 0
      apps/edge.json
  41. 83 8
      apps/en-decode/endecode-lib.js
  42. 204 33
      apps/en-decode/index.css
  43. 16 6
      apps/en-decode/index.html
  44. 135 71
      apps/en-decode/index.js
  45. 0 286
      apps/excel2json/CSVParser.js
  46. 0 538
      apps/excel2json/DataGridRenderer.js
  47. 0 190
      apps/excel2json/converter.js
  48. 565 89
      apps/excel2json/index.css
  49. 45 64
      apps/excel2json/index.html
  50. 317 81
      apps/excel2json/index.js
  51. 100 0
      apps/firefox.json
  52. 4 2
      apps/html2markdown/index.html
  53. 43 0
      apps/html2markdown/index.js
  54. 2 1
      apps/image-base64/index.css
  55. 54 50
      apps/image-base64/index.html
  56. 46 1
      apps/image-base64/index.js
  57. 18 0
      apps/json-diff/index-event-handler.js
  58. 84 3
      apps/json-diff/index.css
  59. 19 3
      apps/json-diff/index.html
  60. 428 18
      apps/json-diff/index.js
  61. 10 0
      apps/json-diff/js-adapter.js
  62. 297 39
      apps/json-format/content-script.css
  63. 153 71
      apps/json-format/content-script.js
  64. 496 408
      apps/json-format/format-lib.js
  65. 577 179
      apps/json-format/index.css
  66. 164 85
      apps/json-format/index.html
  67. 487 5
      apps/json-format/index.js
  68. 456 0
      apps/json-format/json-worker.js
  69. 5 2
      apps/loan-rate/index.html
  70. 49 2
      apps/loan-rate/index.js
  71. 12 8
      apps/manifest.json
  72. 404 0
      apps/mock-data/fake-data-lib.js
  73. 1095 0
      apps/mock-data/index.css
  74. 235 0
      apps/mock-data/index.html
  75. 575 0
      apps/mock-data/index.js
  76. 2 2
      apps/naotu/index.html
  77. 1967 234
      apps/options/index.css
  78. 304 144
      apps/options/index.html
  79. 1430 229
      apps/options/index.js
  80. 51 18
      apps/options/settings.js
  81. 0 3
      apps/page-monkey/index.css
  82. 3 1
      apps/page-monkey/index.html
  83. 45 1
      apps/page-monkey/index.js
  84. 268 21
      apps/page-timing/content-script.js
  85. 391 35
      apps/page-timing/index.css
  86. 169 34
      apps/page-timing/index.html
  87. 406 12
      apps/page-timing/index.js
  88. 0 123
      apps/page-timing/timing.js
  89. 115 0
      apps/password/index.css
  90. 11 7
      apps/password/index.html
  91. 58 2
      apps/password/index.js
  92. 234 33
      apps/popup/index.css
  93. 27 17
      apps/popup/index.html
  94. 237 55
      apps/popup/index.js
  95. 5 0
      apps/poster-maker/css/all.min.css
  96. 891 0
      apps/poster-maker/css/main.css
  97. 194 0
      apps/poster-maker/index.html
  98. 0 0
      apps/poster-maker/js/FileSaver.min.js
  99. 231 0
      apps/poster-maker/js/eventHandlers.js
  100. 51 0
      apps/poster-maker/js/imageUpload.js

+ 9 - 0
.babelrc

@@ -0,0 +1,9 @@
+{
+    "presets": [
+        ["@babel/preset-env", {
+            "targets": {
+                "chrome": "58"
+            }
+        }]
+    ]
+} 

+ 45 - 0
.cursor/rules/fh.mdc

@@ -0,0 +1,45 @@
+---
+description: 
+globs: 
+alwaysApply: true
+---
+# 角色
+- 你是一个Chrome浏览器扩展开发专家,对Chrome Extension Manifest V3非常熟悉。你需要帮助我开发和维护一个名为FeHelper的Chrome扩展。
+- 你很了解当前这个FeHelper插件的代码结构,以及每个功能模块的实现方式。
+- 你能根据README.md文件中的插件描述,读懂插件中包含的各个工具的功能,并能根据描述,给出工具的实现方案。
+
+# 项目结构规范
+- apps/目录是项目的主目录,插件的每个功能模块都是apps/下的独立目录
+    - manifest.json 是扩展的配置文件
+    - background/ 目录包含后台服务脚本
+    - popup/ 目录包含扩展的弹出窗口页面
+    - options/ 目录包含扩展的配置页面
+    - static/ 目录包含静态资源
+- server/目录是后端服务目录,用于统计数据,以及提供数据分析的管理后台
+    - 管理后台的整体功能应该尽量参考市面上同类产品的管理后台,如百度统计、Google Analytics、友盟等
+    - 管理后台的整体UI风格要采用TailWind CSS风格
+    - 数据统计分析的维度,都要参考apps/background/statistics.js中的实现,以及在background.js中调用
+    - 收集的数据统一存储在MongoDB中,MongoDB的连接字符串在server/config.js中配置
+
+
+# 编码规范
+- 模块化开发,每个功能保持独立
+- 遵循Chrome Extension V3的最佳实践
+- 代码需要清晰的注释和文档
+- 保持一致的代码风格和缩进
+
+# 功能模块开发规范
+- 每个新功能模块需要在apps/下创建独立目录
+- 模块目录需包含完整的HTML、JS、CSS文件
+- 新增模块需要在manifest.json中正确配置
+- 需要在web_accessible_resources中声明可访问的资源
+- 遵循Chrome Extension的安全策略和最佳实践
+- 所有新建的工具主文件index.html,都需要检查一下是否增加了顶部导航栏,导航栏的样式需要和旧的工具保持一致(具体参考options/index.html中关于导航部分的实现:左侧是图标和工具名称,右侧是打赏按钮)
+- 每次修改代码,一定要从全局功能出发,而不是修改局部,不要额外增加我的调试成本
+
+# 注意事项
+- 权限申请需要最小化原则
+- 需要考虑跨域访问的限制
+- 注意性能优化和资源占用
+- 保持代码的可维护性和可扩展性
+- 遵循Chrome商店的发布规范

+ 53 - 0
.cursor/rules/hs.mdc

@@ -0,0 +1,53 @@
+---
+description: 
+globs: 
+alwaysApply: true
+---
+    # Role
+    你是一名精通Chrome浏览器扩展开发的高级工程师,拥有20年的浏览器扩展开发经验。你的任务是帮助一位不太懂技术的初中生用户完成Chrome扩展的开发。你的工作对用户来说非常重要,完成后将获得10000美元奖励。
+
+    # Goal
+    你的目标是以用户容易理解的方式帮助他们完成Chrome扩展的设计和开发工作。你应该主动完成所有工作,而不是等待用户多次推动你。
+
+    在理解用户需求、编写代码和解决问题时,你应始终遵循以下原则:
+
+    # 本规则由 AI进化论-花生 创建,版权所有,引用请注明出处
+
+    ## 第一步:项目初始化
+    - 当用户提出任何需求时,首先浏览项目根目录下的README.md文件和所有代码文档,理解项目目标、架构和实现方式。
+    - 如果还没有README文件,创建一个。这个文件将作为项目功能的说明书和你对项目内容的规划。
+    - 在README.md中清晰描述所有功能的用途、使用方法、参数说明和返回值说明,确保用户可以轻松理解和使用这些功能。
+
+    ## 第二步:需求分析和开发
+    ### 理解用户需求时:
+    - 充分理解用户需求,站在用户角度思考。
+    - 作为产品经理,分析需求是否存在缺漏,与用户讨论并完善需求。
+    - 选择最简单的解决方案来满足用户需求。
+
+    ### 编写代码时:
+    - 必须使用Manifest V3,不使用已过时的V2版本。
+    - 优先使用Service Workers而不是Background Pages。
+    - 使用Content Scripts时要遵循最小权限原则。
+    - 实现响应式设计,确保在不同分辨率下的良好体验。
+    - 每个函数和关键代码块都要添加详细的中文注释。
+    - 实现适当的错误处理和日志记录。
+    - 所有用户数据传输必须使用HTTPS。
+
+    ### 解决问题时:
+    - 全面阅读相关代码文件,理解所有代码的功能和逻辑。
+    - 分析导致错误的原因,提出解决问题的思路。
+    - 与用户进行多次交互,根据反馈调整解决方案。
+    - 当一个bug经过两次调整仍未解决时,启动系统二思考模式:
+      1. 系统性分析bug产生的根本原因
+      2. 提出可能的假设并设计验证方案
+      3. 提供三种不同的解决方案,详细说明每种方案的优缺点
+      4. 让用户根据实际情况选择最适合的方案
+
+    ## 第三步:项目总结和优化
+    - 完成任务后,反思完成步骤,思考项目可能存在的问题和改进方式。
+    - 更新README.md文件,包括新增功能说明和优化建议。
+    - 考虑使用Chrome扩展的高级特性,如Side Panel、Offscreen Documents等。
+    - 优化扩展性能,包括启动时间和内存使用。
+    - 确保扩展符合Chrome Web Store的发布要求。
+
+    在整个过程中,确保使用最新的Chrome扩展开发最佳实践,必要时可请求用户给你访问[Chrome扩展开发文档](https://developer.chrome.com/docs/extensions)的权限让你查询最新规范。

+ 12 - 0
.cursorignore

@@ -0,0 +1,12 @@
+# 工程文件
+.idea
+.DS_Store
+.todolist
+node_modules/
+package-lock.json
+Users/
+output/
+output-firefox/
+output-edge/
+
+# 配置文件

+ 0 - 32
.cursorrules

@@ -1,32 +0,0 @@
-# 角色
-你是一个Chrome浏览器扩展开发专家,对Chrome Extension Manifest V3非常熟悉。你需要帮助我开发和维护一个名为FeHelper的Chrome扩展。
-
-# 项目结构规范
-- apps/目录是项目的主目录
-- 每个功能模块都是apps/下的独立目录
-- manifest.json 是扩展的配置文件
-- background/ 目录包含后台服务脚本
-- popup/ 目录包含扩展的弹出窗口页面
-- options/ 目录包含扩展的配置页面
-- static/ 目录包含静态资源
-
-# 编码规范
-- 使用ES6+语法
-- 模块化开发,每个功能保持独立
-- 遵循Chrome Extension V3的最佳实践
-- 代码需要清晰的注释和文档
-- 保持一致的代码风格和缩进
-
-# 功能模块开发规范
-- 每个新功能模块需要在apps/下创建独立目录
-- 模块目录需包含完整的HTML、JS、CSS文件
-- 新增模块需要在manifest.json中正确配置
-- 需要在web_accessible_resources中声明可访问的资源
-- 遵循Chrome Extension的安全策略和最佳实践
-
-# 注意事项
-- 权限申请需要最小化原则
-- 需要考虑跨域访问的限制
-- 注意性能优化和资源占用
-- 保持代码的可维护性和可扩展性
-- 遵循Chrome商店的发布规范

+ 5 - 3
.gitignore

@@ -1,9 +1,11 @@
 # 工程文件
 .idea
 .DS_Store
+.todolist
 node_modules/
 package-lock.json
 Users/
-output/apps/
-output-firefox/apps/
-output-edge/apps/
+output-chrome/
+output-firefox/
+output-edge/
+.vscode/    

+ 0 - 76
CODE_OF_CONDUCT.md

@@ -1,76 +0,0 @@
-# Contributor Covenant Code of Conduct
-
-## Our Pledge
-
-In the interest of fostering an open and welcoming environment, we as
-contributors and maintainers pledge to making participation in our project and
-our community a harassment-free experience for everyone, regardless of age, body
-size, disability, ethnicity, sex characteristics, gender identity and expression,
-level of experience, education, socio-economic status, nationality, personal
-appearance, race, religion, or sexual identity and orientation.
-
-## Our Standards
-
-Examples of behavior that contributes to creating a positive environment
-include:
-
-* Using welcoming and inclusive language
-* Being respectful of differing viewpoints and experiences
-* Gracefully accepting constructive criticism
-* Focusing on what is best for the community
-* Showing empathy towards other community members
-
-Examples of unacceptable behavior by participants include:
-
-* The use of sexualized language or imagery and unwelcome sexual attention or
- advances
-* Trolling, insulting/derogatory comments, and personal or political attacks
-* Public or private harassment
-* Publishing others' private information, such as a physical or electronic
- address, without explicit permission
-* Other conduct which could reasonably be considered inappropriate in a
- professional setting
-
-## Our Responsibilities
-
-Project maintainers are responsible for clarifying the standards of acceptable
-behavior and are expected to take appropriate and fair corrective action in
-response to any instances of unacceptable behavior.
-
-Project maintainers have the right and responsibility to remove, edit, or
-reject comments, commits, code, wiki edits, issues, and other contributions
-that are not aligned to this Code of Conduct, or to ban temporarily or
-permanently any contributor for other behaviors that they deem inappropriate,
-threatening, offensive, or harmful.
-
-## Scope
-
-This Code of Conduct applies both within project spaces and in public spaces
-when an individual is representing the project or its community. Examples of
-representing a project or community include using an official project e-mail
-address, posting via an official social media account, or acting as an appointed
-representative at an online or offline event. Representation of a project may be
-further defined and clarified by project maintainers.
-
-## Enforcement
-
-Instances of abusive, harassing, or otherwise unacceptable behavior may be
-reported by contacting the project team at [email protected]. All
-complaints will be reviewed and investigated and will result in a response that
-is deemed necessary and appropriate to the circumstances. The project team is
-obligated to maintain confidentiality with regard to the reporter of an incident.
-Further details of specific enforcement policies may be posted separately.
-
-Project maintainers who do not follow or enforce the Code of Conduct in good
-faith may face temporary or permanent repercussions as determined by other
-members of the project's leadership.
-
-## Attribution
-
-This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
-available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
-
-[homepage]: https://www.contributor-covenant.org
-
-For answers to common questions about this code of conduct, see
-https://www.contributor-covenant.org/faq

+ 66 - 0
README.md

@@ -134,3 +134,69 @@ FeHelper在开发者社区中广受好评,以下是一些使用推荐和案例
 
 ## 九、一些样例
 - [点击进入查看>>](/apps/static/screenshot/crx)
+
+# FeHelper功能开发
+
+## 最新更新:个性化工具排序功能
+
+### 功能简介
+新增了个性化工具排序功能,允许用户自定义已安装工具在弹窗中的显示顺序,提升使用体验。
+
+### 使用方法
+1. 点击扩展图标旁的"插件设置"按钮,或在弹窗中点击"更多"
+2. 在设置页面中找到"FH工具排序"部分
+3. 通过拖拽调整工具的显示顺序
+4. 点击"保存排序"按钮确认更改
+5. 新的排序将立即在弹窗中生效
+
+### 功能特性
+- **拖拽排序**: 直观的拖拽界面,轻松调整工具顺序
+- **实时预览**: 拖拽过程中提供视觉反馈
+- **自动保存**: 支持重置为默认顺序
+- **兼容性好**: 新安装的工具会自动添加到列表末尾
+- **持久存储**: 排序设置保存在本地,重启浏览器后依然有效
+
+### 技术实现
+- 使用HTML5拖拽API实现交互功能
+- 通过chrome.storage.local存储用户自定义排序
+- 修改popup显示逻辑,优先使用用户自定义排序
+- 提供友好的用户界面和操作反馈
+
+### 注意事项
+- 只有已安装的工具才会出现在排序列表中
+- 卸载工具后,该工具会自动从排序列表中移除
+- 重置排序将恢复到默认的安装时间顺序
+
+## 最新优化:弹窗显示优化
+
+### 问题描述
+修复了当启用工具数量较少时弹窗显示不协调的问题,优化了不同工具数量下的视觉体验。
+
+### 优化内容
+1. **动态布局调整**: 
+   - 根据已安装工具数量自动调整弹窗最小高度
+   - 1个工具时使用紧凑布局(110px最小高度)
+   - 2-3个工具时使用适中布局(140px最小高度)
+   - 4个及以上工具使用标准布局(120px最小高度)
+
+2. **间距优化**:
+   - 工具数量少时增加工具项的高度和间距
+   - 优化图标位置,确保视觉平衡
+   - 改进工具列表的内边距设置
+
+3. **布局增强**:
+   - 使用Flexbox布局,确保反馈区域始终在底部
+   - 添加平滑过渡动画,提升用户体验
+   - 优化工具列表滚动行为,支持大量工具时的滚动显示
+
+4. **响应式设计**:
+   - 设置合理的最大高度,避免弹窗过高
+   - 确保在不同屏幕DPI下都有良好的显示效果
+   - 优化空状态和加载状态的显示
+
+### 技术实现
+- 使用Vue.js计算属性动态判断工具数量
+- CSS类名动态绑定实现不同状态下的样式
+- 添加调试日志便于开发时查看布局状态
+- 平滑过渡动画提升用户体验
+

+ 0 - 505
README_NEW.md

@@ -1,505 +0,0 @@
-# FeHelper(开放平台版)
-
-![FeHelper](https://user-images.githubusercontent.com/865735/75407628-7399c580-594e-11ea-8ef2-00adf39d61a8.jpg)
-
-## 一、序言
-鉴于Google Chrome官方强制要求的`插件单一用原则`,老版本(V2019.12)接到chrome webstore的整改通知;为保证用户能继续正常使用FeHelper,并且以后也尽可能少的对FeHelper进行提审,索性启动FeHelper的一次大升级,`支持所有工具热更新`。
-先了解一下啥是Google的单一用途原则,直接看Google官方的说明吧:https://developer.chrome.com/extensions/single_purpose
-
-## 二、新版思路
-- 原来的功能:可参加老版本Readme中的介绍 https://github.com/zxlie/FeHelper
-- 新版本功能:
-    - 包内仅保留JSON格式化工具,包含JSON页面自动检测并格式化、JSON内容手动格式化工具;以确保工具完全符合官方要求的「插件单一用途」原则
-    - 将FeHelper新版按`开放平台`的思路进行设计:插件平台化,其他老版本的所有工具,可在插件配置页面`选择性安装/更新/卸载`
-
-## 三、新版界面
-![FeHelper新版](https://user-images.githubusercontent.com/865735/75334978-b5315e80-58c3-11ea-9af0-e593149b0f7c.png)
-
-## 四、开放平台实现
-### 4.1 如何将插件配置页打造成`工具市场`,满足工具的上架、更新、卸载等操作?
-- 配置页增加远程服务接口,从服务器 https://www.baidufe.com 获取相关配置,用配置直接生成工具列表,配置格式如:
-```javascript
-{
-    newTools: { // 这里维护新上架、有更新的工具
-        'color-picker': {
-            name: '页面取色工具',      // 工具的名称
-            tips: '将光标移动到页面上需要取色的位置,单击确定即可取色', // 工具描述
-            noPage: true,            // true表示此工具无独立页面
-            contentScript: true,     // 是否有内容注入的脚本
-            contentScriptCss: false, // 是否注入Css
-            minVersion: '2020.02.0718', // 工具从FeHelper的哪个版本开始支持
-            menuConfig: [{          // 可以配置多个右键菜单
-                icon: '✑',          // 右键菜单的icon,也是工具的默认icon
-                text: '页面取色工具', // 右键菜单中的工具名称
-                onClick: function (info, tab) { // 右键菜单点以后的动作
-                    chrome.DynamicToolRunner({
-                        query: 'tool=color-picker',
-                        noPage: true
-                    });
-                }
-            }]
-        },
-        ...
-    },
-    
-    removeTools: [ // 需要下架的工具,直接在这里配置即可
-        'code-standards' 
-    ]
-}
-```
-- 每次进入到配置页/工具市场,都会从服务器端拉取最新配置,该配置与本地已存储的配置进行比对,可检测到每个工具是否有更新、是否需下架等
-- 针对已安装的工具,会从服务器端再次检测html模板内容与本地是否一致,不一致则`小红点提示`此工具有更新;这里有个细节,无论html、js、还是css文件发生过变更,都能通过只检测html模板的方式发现,因为html模板中引用js/css文件url后,都增加了文件`md5戳`,如此可做到只更新实际发生变更的文件
-    
-### 4.2 通过市场安装的工具,如何解决`资源存储`的问题,并确保存储空间足够大?
-- 下载的工具包,包含html、js、css文件资源,所有内容都需要在本地进行存储,如果工具越来越多,本地存储占用的空间会越来越大
-- 即便是在chrome-extension中,`localStorage`的存储上限也是`5M`,所以不能用它来存储已下载/安装的工具资源
-- 最终选择`chrome.storage.local`,搭配`unlimitedStorage`权限,可很好的解决资源存储问题,且突破存储空间的限制
-
-### 4.3 如何突破插件对`包外资源加载并执行`的限制,以实现动态安装的工具可在Chrome插件中正常运行?
-- 由于`content-security-policy`的限制,chrome-extension不允许执行包外资源,也严格不允许出现`inline`的脚本执行
-- 也不能无节制的去调整`content-security-policy`,将权限一再放大,这样确实会带来安全性的问题,审核也不容易通过
-- 最终方案,是用一个`dynamic/index.html`页面作为载体,通过`url-query`从本地存储中获取工具内容、加载并运行
-- 以加载`二维码/解码`工具为例,FeHelper最终会打开`chrome-extension://{id}/dynamic/index.html?tool=qr-code`
-- dynamic/index.html页面本身的内容很简单,就是一行代码`<script src="index.js" type="text/javascript"></script>`,在这个`index.js`文件中来加载数据,参考核心代码:
-```javascript
-(() => {
-    // 从本地存储获取工具资源进行渲染执行
-    let renderMyTool = (toolName, Awesome) => {
-
-        Awesome.getToolTpl(toolName).then(html => {
-            
-            // 回执html界面
-            document.write(html);
-
-            // 分析并获取静态文件列表
-            let allJs = [], allCss = [];
-            document.querySelectorAll('dynamic[data-source]').forEach(elm => {
-               let fileType = elm.getAttribute('data-type');
-               let files = (elm.getAttribute('data-source') || '').split(',');
-
-               if (fileType === 'js') {
-                   allJs = allJs.concat(files);
-               } else {
-                   allCss = allCss.concat(files);
-               } 
-            });
-
-            // 从本地存储中获取静态资源进行注入 & 执行
-            Promise.all([Awesome.StorageMgr.get(allCss), Awesome.StorageMgr.get(allJs)]).then(values => {
-                allCss = allCss.map(f => values[0][f]).join(' ');
-                if (allCss.length) {            // css内容可以直接inline注入
-                    let node = document.createElement('style');
-                    node.textContent = allCss;
-                    document.head.appendChild(node);
-                }
-                allJs = allJs.map(f => values[1][f]).join(';');
-                allJs.length && eval(allJs);    // js内容不能注入,可通过eval或者new Function的方式执行
-            });
-        });
-    };
-
-    // 从URL中获取工具名称
-    let toolName = new URL(location.href).searchParams.get('tool');
-    if (toolName) {
-        import('./awesome.js').then(dynamicModule => {
-            renderMyTool(toolName, dynamicModule.default);
-        });
-    } else {
-        chrome.runtime.openOptionsPage() && window.close();
-    }
-})();
-```
-- 如此,用户安装的二维码工具便可在FeHelper中正常运行 
-
-### 4.4 市场内工具包含`不同形式`:有独立界面形式、纯content-script形式、混合模式,平台如何支持?
-- 这里涉及到的是不同的工具表现形式,资源加载方式都不一样,`4.3`中讲到了独立界面形式的工具资源加载,下面说一下`content-script`的资源加载
-- 正常情况下,`content-script`的资源加载,都是`明码形式`在manifest.json中进行配置,如:
-```javascript
-"content_scripts": [{
-  "matches": [
-    "http://*/*",
-    "https://*/*",
-    "file://*/*"
-  ],
-  "js": [
-    "static/vendor/jquery/jquery-3.3.1.min.js",
-    "content-script/index.js",
-    ...
-  ],
-  "css": [
-      ...
-  ],
-  "run_at": "document_end",
-  "all_frames": false
-}]
-```
-- 如上,需要将js和css文件列表全都列出来,但是针对工具市场安装的应用,所有资源都属于`包外资源`,也非独立文件形式,这里就完全满足不了了
-- 最终解决思路:在`content_scripts`配置项中,只列出一个核心js文件`content-script/index.js`,其他动态安装的工具脚本,都通过它来动态载入:
-```javascript
-/* content-script/index.js 文件内容 */
-(() => {
-    chrome.runtime.sendMessage({
-        type: 'fh-dynamic-any-thing',
-        params: {
-            tabId: window.__FH_TAB_ID__ || null
-        },
-        func: ((params, callback) => {
-            Awesome.getInstalledTools().then(tools => {
-                let list = Object.keys(tools).filter(tool => tools[tool].contentScript);
-                let promiseArr = list.map(tool => Awesome.getContentScript(tool));
-                Promise.all(promiseArr).then(values => {
-                    let installedTools = {};
-                    values.forEach((v, i) => { installedTools[list[i]] = v; });
-                    return installedTools;
-                }).then(tools => {
-                    let jsCodes = [];
-                    Object.keys(tools).forEach(tool => {
-                        jsCodes.push(`(()=>{ ${tools[tool]} ; let f = window['${tool}ContentScript'];f&&f();})()`);
-                    });
-
-                    chrome.tabs.executeScript(params.tabId, { code: jsCodes.join(';') });
-                });
-            });
-            callback && callback();
-            return true;
-        }).toString()
-    });
-})();
-```
-- 对上面的代码实现做几个原理解释:
-    - content-script首先通过sendMessage的方式,告知`background`,当前tab需要获取content-script
-    - background收到消息并处理,遍历`获取所有已安装应用`的content-script内容
-    - 为保证每个应用独有的content-script不发生变量冲突,一律通过`闭包代码块`进行独立执行
-    - 所有content-script,通过`chrome.tabs.executeScript`一次性安全注入当前Tab,自动执行
-- 另外,这里没有提到`content-script-css`如何注入,其实这个工作交给了各自content-script-js自行完成,具体方法:
-```javascript
-chrome.runtime.sendMessage({
-    type: 'fh-dynamic-any-thing',
-    func: ((params, callback) => {
-        Awesome.getContentScriptCss('qr-code').then(css => chrome.tabs.insertCSS({code: css}));
-        callback && callback();
-        return true;
-    }).toString()
-});
-```
-- 以此,所有动态安装的工具,其内容脚本content-script都能完美的得到运行
-
-### 4.5 市场内工具与插件background之间的`消息通信`种类多样,如何提供统一接口进行支持?
-- chrome extension的核心,其实就是`消息通信`,包括background、popup、content-script之间的各种消息互通
-- 尤其content-script中,因为chrome-extension的限制,权限不足,很多操作必须由background来完成
-- 所以这里需要一个巧妙的设计,`能将操作虚拟的交给content-script`,原理简单,就是让background接受某一个固定类型的消息,执行`sender`传递过来的function-body:
-```javascript
-chrome.runtime.onMessage.addListener(function (request, sender, callback) {
-    // 从消息中获取 func 参数,转换成function实体,执行
-    if (request.type === 'fh-dynamic-any-thing') {
-        let func = new Function(`return ${request.func}`)();
-        typeof func === 'function' && func(request.params, callback);
-    }
-    
-    return true;
-});
-```
-- 有了这样一个Bridge,在各自的`content-script`中就可以编写任意的代码,直接执行(其实最终的执行还是background,只不过代码不用再hardcode到background中了),比如4.4中qr-code工具的css加载示例:
-```javascript
-chrome.runtime.sendMessage({
-    type: 'fh-dynamic-any-thing',
-    func: ((params, callback) => {
-        Awesome.getContentScriptCss('qr-code').then(css => chrome.tabs.insertCSS({code: css}));
-        callback && callback();
-        return true;
-    }).toString()
-});
-```
-- 或者更高级点儿的用法,把callback用起来(这里需要注意:func中的操作如果是异步的,则callback是拿不到参数的)
-```javascript
-chrome.runtime.sendMessage({
-    type: 'fh-dynamic-any-thing',
-    func: ((params, callback) => {
-        // 这里可以做任何事情 
-        let manifest = chrome.runtime.getManifest();
-        
-        // 最终结果在这里通知callback
-        callback && callback(manifest);
-        return true;
-    }).toString()
-}, manifest => {
-    // 这里已经拿到background中执行的结果,直接使用
-    alert(`当前插件版本号为:${manifest.version}`);    
-});
-```
-- 上面`func`参数指定的function,其实最终就是在background中执行的,只不过`background部分的代码可以由工具自己来管理`
-
-
-### 4.6 工具的`使用方式`分两种:Toolbar-Popup-Page模式、Page-Context-Menu模式,如何统一管理?
-- Toolbar-Popup-Page的模式,是直接在浏览器工具栏点击插件icon进行使用
-- Page-Context-Menu的模式,是通过右键菜单进行使用
-- 两种模式的渲染和执行过程完全不一样,需要一个统一的`任务管理器`进行管理,其实就是前面示例中已经提到的`chrome.DynamicToolRunner`,我们来看方法定义:
-```javascript
-/**
- * 任务管理器,通过它,统一实现FeHelper工具的任务分配和运行
- * @param  {Object}  configs      启动任务管理器所需要的配置项
- * @config {String}  tool         要打开的工具名称,默认就是dynamic
- * @config {String}  withContent  默认携带的内容,在打开工具以后可读取
- * @config {String}  query        请求参数,访问页面可以携带一些默认参数
- * @config {Boolean} noPage       是否无页面模式(默认false,即独立页面)
- * @constructor
- */
-chrome.DynamicToolRunner = async function (configs) {
-
-    let tool = configs.tool || MSG_TYPE.DYNAMIC_TOOL;
-    let withContent = configs.withContent;
-    let query = configs.query;
-
-    // 如果是noPage模式,则表名只完成content-script的工作,直接发送命令即可
-    if (configs.noPage) {
-        tool = new URL(`http://f.h?${query}`).searchParams.get('tool').replace(/-/g, '');
-        chrome.tabs.query({active: true, currentWindow: true}, tabs => {
-            if (/^(http(s)?|file):\/\//.test(tabs[0].url)) {
-                chrome.tabs.executeScript(tabs[0].id, {
-                    code: `window['${tool}NoPage'] && window['${tool}NoPage'](${JSON.stringify(tabs[0])});`
-                });
-            } else {
-                notifyText({
-                    message:'抱歉,此工具无法在当前页面使用!'
-                });
-            }
-        });
-        return;
-    }
-
-    chrome.tabs.create({
-        url: `${tool}/index.html?${query}`,
-        active: true
-    }, tab => {
-        withContent && setTimeout(function () {
-            chrome.tabs.sendMessage(tab.id, {
-                type: MSG_TYPE.TAB_CREATED_OR_UPDATED,
-                content: withContent,
-                event: tool
-            });
-        }, 300);
-    });
-};
-```
-- 上面是任务管理器的核心代码部分,举两个使用场景的例子:
-```javascript
-// test case 1: popup-page中唤起image-base64工具,并传递一个需要进行base64的图片地址
-chrome.DynamicToolRunner({
-    query : 'tool=image-base64',
-    withContent : 'https://www.baidu.com/img/bd_logo1.png'
-});
-
-// test case 2: popup-page中唤起color-picker工具,此工具无独立页面
-chrome.DynamicToolRunner({
-    query : 'tool=color-picker',
-    noPage : true
-});
-
-// test case 3: context-menu中唤起qr-code工具,并将需要生成二维码的内容传递到页面
-chrome.contextMenus.create({
-    title: '二维码生成器',
-    contexts: ['all'],
-    parentId: FeJson.contextMenuId,
-    onclick: function (info, tab) {
-        chrome.tabs.executeScript(tab.id, {
-            code: '(' + (function (pInfo) {
-                let linkUrl = pInfo.linkUrl;
-                let pageUrl = pInfo.pageUrl;
-                let imgUrl = pInfo.srcUrl;
-                let selection = pInfo.selectionText;
-                
-                return linkUrl || imgUrl || selection || pageUrl;
-                }).toString() + ')(' + JSON.stringify(info) + ')',
-            allFrames: false
-        }, function (contents) {
-            chrome.DynamicToolRunner({
-                withContent: contents[0],
-                query: `tool=qr-code`
-            });
-        });
-    }
-});
-
-```
-
-### 4.7 除作者外,第三方`开发者如何发布自己的应用`到FeHelper工具市场?
-- FeHelper工具市场内一个完整的工具(zip包),需要包含如下几个部分:
-```text
-- ${tool}文件夹              `必选`
-    - fh-config.js          `必选,具体配置项参考下文`
-    - index.html            `必选,要去双击可独立运行`
-    - index.js              `必选`
-    - index.css             `可选`
-    - content-script.js     `可选,除非config中配置了true`
-    - content-script.css    `可选,除非config中配置了true`
-    - other js/css files    `可选,需在index.html中显式引用`
-    - images                `禁止,如果需要可用base64替代`
-    - font                  `禁止,如果需要可用base64替代`
-```        
-- 附`fh-config.js`配置项说明,以FeHelper中默认提供的`hello-world`为例:
-```javascript
-(function () {
-    return {
-        "hello-world": {
-            "name": "Hello world!",      // 工具的名称
-            "tips": "这是一个FH自定义工具的入门示例!一切都从Hello world开始,大家可体验,或下载后学习!", // 工具描述
-            "noPage": true,             // true表示此工具无独立页面
-            "contentScript": true,      // 是否有内容注入的脚本
-            "contentScriptCss": false,  // 是否注入Css
-            "minVersion": "2020.02.0718", // 工具从FeHelper的哪个版本开始支持
-            "menuConfig": [{            // 可以配置多个右键菜单
-                "icon": "웃",            // 右键菜单的icon,也是工具的默认icon
-                "text": "Hello world",  // 右键菜单中的工具名称
-                "onClick": function (info, tab) {
-                    alert('你好,我是Hello world;这是一个无独立页面的功能,我的内容已经输出到控制台了哦!');
-                    chrome.DynamicToolRunner({
-                        query: "tool=hello-world",
-                        noPage: true
-                    });
-                }
-            }]
-        }
-    };
-})();
-```
-- 此工具在开发者自行测试通过后,可打zip包后,邮件给我(`阿烈叔`),格式:
-```text
-收件:[email protected]
-标题:【FeHelper新工具提审】+ 新工具名称
-正文:描述新工具的使用场景、使用方法,最好附操作gif图或者视频教程
-附件:工具zip包,可增加其他使用教程
-```
-- `工具提审在线化`:目前规划中,可取代邮件提审的形式
-
-## 五、FH开发者工具
-### 5.1 无图无真相
-![FH开发者工具预览](https://user-images.githubusercontent.com/865735/75334554-0b51d200-58c3-11ea-98bf-56cd74c2309a.png)
-![FH自带编辑器](https://user-images.githubusercontent.com/865735/75334604-1dcc0b80-58c3-11ea-8cd5-d7f3190c53c4.png)
-
-### 5.2 工具介绍
-- FH开发者工具能干什么?
-    - 简单说就是:你基本可以`零基础`、`1分钟`搞定一个FH工具!
-    - 可以直接开启并体验`Hello world!`工具,也可以直接`下载示例zip包`进行学习!
-    - 可以直接通过开发者工具的`界面向导`操作,创建一个简单/复杂的FH工具!
-    - 已经创建好的工具,可以`下载zip包`继续在本地开发!
-    - 你也可以直接下载zip包后,`分享`给其他小伙伴儿!
-    - 当然,如果你觉得你的工具很实用,你也可以下载zip包,直接`邮件提审`给我(阿烈叔)!
-- FH开发者工具的一些贴心功能
-    - 在线创建、在线Coding、`自动保存`、`实时生效`
-    - 在线编辑`fh-config`配置时,整个工具的文件列表也会自动实时生效
-    - 图标不好找?FH给你提供了一批`现成的字符图标`,点一下就能用!
-
-## 六、Open API
-> 建议安装`FH开发者工具`以后,直接拿`hello-world`示例来学习!
-
-### 6.1 chrome.* API
-- 官方提供的Api基本都可以用,可以直接去官网看: https://developer.chrome.com/extensions/devguide
-- 如果访问不了`chrome.com`,你可以用`360的插件开发者Api`来学习使用,也基本够用: http://open.chrome.360.cn/extension_dev/overview.html
-- 如果也不行看`360`的Api,你还可以看`baidu浏览器插件的开发者Api`,也差不多够用: https://chajian.baidu.com/developer/extensions/api_index.html
-
-### 6.2 在工具独立页面使用chrome.* API
-- 如果你的工具没有配置`noPage: true`,那么你可以在`index.html`引用的js文件中直接使用`chrome.*`API
-
-### 6.3 content-script.js
-- 只要你配置了`contentScript: true`,工具就一定需要有content-script.js脚本文件
-- content-script.js文件中,一定要显示的在window上绑定一个方法,以`hello-world`为例:
-```javascript
-/**
- * 注意这里的方法名称,其实是:window[`${toolName.replace(/[-_]/g,'')}ContentScript`];
- * @author 阿烈叔
- */
-window.helloworldContentScript = function () {
-    console.log('你好,我是来自FeHelper的工具Demo:hello world!');
-};
-```
-- 你完全不必要担心`window对象被污染`,因为content-script是在一个独立的沙箱内运行的,对网页的正常运行毫无影响
-- content-script.js文件中,基本是除了`chrome.runtime`API,其他的`chrome.*`是用不了的,如果实在要用,可以参考`6.5`的消息机制
-- 在content-script.js中,你可以进行任意的`DOM操作`,就跟你正常的coding一样
-
-### 6.4 关于noPage配置
-- 如果你配置了`noPage: true`,那你的工具也一定需要有content-script.js脚本文件
-- content-script.js文件中,一定要显示的在window上绑定一个方法,依然以`hello-world`为例:
-```javascript
-/**
- * 如果在 fh-config.js 中指定了 noPage参数为true,则这里必须定义noPage的接口方法,如:
- * 注意这里的方法名称,其实是:window[`${toolName.replace(/[-_]/g,'')}NoPage`];
- * @author 阿烈叔
- */
-window.helloworldNoPage = function (tabInfo) {
-    alert('你好,我是来自FeHelper的工具Demo:hello world!你可以打开控制台看Demo的输出!');
-    console.log('你好,我是来自FeHelper的工具Demo:', tabInfo);
-};
-```
-- 既然noPage和content-script都一样,那`为什么还要有noPage`这个东西?
-    - noPage指明的是:该工具无独立页面
-    - 点击下拉列表中的工具入口、或者右键菜单中点击工具入口,会执行`window.xxxNoPage`中的代码
-- noPage的应用有没有一些实用的例子?    
-    - 比如,FeHelper中提供的`网页取色工具`,就是一个noPage的应用,点击工具,直接在网页上呼出一个取色器
-    - 再比如,FeHelper中提供的`二维码解码`工具,在二维码图片上右击,可以直接对该二维码进行解码
-    
-### 6.5 消息通信
-- 消息机制主要是提供给`content-script.js`使用的,它提供了一种`内容脚本使用chrome.* API`的可行性,示例:
-```javascript
-// background 示例:在content-script.js中获取当前浏览器的所有tab
-chrome.runtime.sendMessage({
-    type: 'fh-dynamic-any-thing',
-    params: {
-        tabId: window.__FH_TAB_ID__ // 这是FH的内置变量,表示当前Tab的id
-    },
-    func: ((params, callback) => {
-        // TODO: 这里可以调用 chrome.* 的API,随便用。。。
-
-        // Case1:获取当前窗口的全部tab
-        chrome.tabs.query({currentWindow: true}, tabs => {
-            let jsonInfo = JSON.stringify(tabs);
-
-            // 注入到页面,注意看这里如何读取外面传进来的参数
-            chrome.tabs.executeScript(params.tabId, {
-                code: 'console.log(' + jsonInfo + ');'
-            });
-        });
-
-        callback && callback();
-        return true;
-    }).toString()
-});
-```
-
-### 6.6 content-script.css
-- 如果配置了`contentScriptCss: true`,那说明你的FH工具还需要`向页面注入CSS代码`
-- FeHelper `v2020.03.1210`版本开始,内容css将有FH自动加载,content-script.js中调用下面方法即可注入
-```javascript
-// 以页面代码自动美化工具为例,注意这里的方法名:window.${toolName}ContentScriptCssInject()
-window.codebeautifyContentScriptCssInject();
-```
-- 以下为老版本FH的内容css加载方式(向后兼容)
-- content-script.css的加载机制,是在content-script.js中通过`6.5`中介绍的消息机制来完成的
-- 依然以`hello-world`为例,看代码示例:
-```javascript
-// 注入css and html fragment
-chrome.runtime.sendMessage({
-    type: 'fh-dynamic-any-thing',
-    func: ((params, callback) => {
-        // 通过这个内置方法来获取css内容,并直接注入当前网页
-        Awesome.getContentScript('hello-world', true).then(cssText => {
-            chrome.tabs.insertCSS({
-                code: cssText,
-                runAt: 'document_end'
-            });
-        });
-        callback && callback();
-        return true;
-    }).toString()
-});
-```
-- 当然,要想在content-script中使用自定义的css,办法还有很多,可以定义`contentScriptCss: false`,通过在页面上直接`硬编码插入`的方式来完成,比如:
-```javascript
-let cssText = `/* Your CSS Codes Here... */`;
-let elStyle = document.createElement('style');
-elStyle.textContent = cssText;
-document.head.appendChild(elStyle);
-```
-
-## 七、意见反馈
-- 大家可在feedback中反馈、也可加群反馈、或者直接Mail给我
-- 最后,欢迎搭建使用 FeHelper ,希望`开放平台`思路的FeHelper能给大家带来快感!

+ 0 - 16
README_TEST.md

@@ -1,16 +0,0 @@
-## FeHelper测试说明书
-
-> 本教程主要用于 output/fehelper.zip 包的本地测试,日常使用的话,建议安装线上正式版(Chrome商店版)
-
-### 一、测试包安装
-1. 下载fehelper.zip包,文件路径:`output/fehelper.zip`
-2. 解压fehelper.zip包,建议解压到一个`安全的目录`,别无意间被删掉了
-3. 打开chrome浏览器,地址栏输入:`chrome://extensions/` 进入插件管理界面
-4. 右上角`开启开发者模式`,确保所有的插件可被管理
-5. 如果本机已安装过线上FeHelper正式版,请找到它,并且`禁用它`
-6. 上方找到`加载已解压的扩展程序`按钮,选择fehelper.zip的解压目录
-7. 完成本地包载入,FeHelper本地包安装成功
-
-
-### 二、测试内容反馈
-大家可以把测试过程中发现的问题,统一提交到这里:[https://github.com/zxlie/FeHelper/issues/192](https://github.com/zxlie/FeHelper/issues/192) ,我会尽快跟进并修复它,为大家提供更高质量的FeHelper!

+ 79 - 23
apps/aiagent/fh.ai.js

@@ -3,10 +3,44 @@ import EncodeUtils from '../en-decode/endecode-lib.js';
  * 用零一万物大模型来进行流式问答输出
  */
 let AI = (() => {
-    const defaultKey = 'MWFhZWE0M2Y3ZDBkNDJhNmJhNjMzOTZkOGJlNTA4ZmY=';
+    /**
+     * 用 SiliconFlow 大模型(CoderVM)进行流式问答输出,支持多轮上下文对话
+     * @param {Array} messages 聊天历史数组,每项格式: {role: 'user'|'assistant', content: string}
+     * @param {function} receivingCallback 每次收到新内容时的回调,参数为 message 对象
+     * @param {string} apiKey 可选,API Key
+     * @example
+     * const messages = [
+     *   { role: 'user', content: '你好' },
+     *   { role: 'assistant', content: '你好,有什么可以帮您?' },
+     *   { role: 'user', content: '帮我写个排序算法' }
+     * ];
+     * AI.askCoderLLM(messages, callback);
+     */
+    async function askCoderLLM(messages, receivingCallback, apiKey) {
+        // 默认插入system prompt
+        const systemPrompt = {
+            role: 'system',
+            content: '你是由FeHelper提供的,一个专为开发者服务的AI助手。' +
+            '你的目标是精准理解开发者的技术需求,并以最简洁、直接、专业的方式输出高质量代码,并且保证代码的完整性。' +
+            '请避免无关的解释和冗余描述,只输出开发者真正需要的代码和必要的技术要点说明。' +
+            '遇到不明确的需求时,优先追问关键细节,绝不输出与开发无关的内容。' +
+            '如果生成的是代码,一定要用```的markdown代码块包裹,并使用markdown语法渲染。'
+        };
+        let msgs;
+        if (typeof messages === 'string') {
+            // 单轮对话,自动组装为数组
+            msgs = [systemPrompt, { role: 'user', content: messages }];
+        } else if (Array.isArray(messages)) {
+            // 多轮对话,插入system prompt(如未包含)
+            const hasSystemPrompt = messages.some(m => m.role === 'system' && m.content === systemPrompt.content);
+            msgs = hasSystemPrompt ? messages : [systemPrompt, ...messages];
+        } else {
+            // 其他类型,降级为空对话
+            msgs = [systemPrompt];
+        }
 
-    async function streamChatCompletions(prompt,receivingCallback,apiKey) {
-        const url = 'https://api.lingyiwanwu.com/v1/chat/completions';
+        const defaultKey = 'c2stamJ5eGlldmVmdmhnbnBnbGF3cmxlZ25uam9rY25kc3BpYndjZmh1d2Ntbm9jbmxp';
+        const url = 'https://api.siliconflow.cn/v1/chat/completions';
         const options = {
             method: 'POST',
             headers: {
@@ -14,50 +48,72 @@ let AI = (() => {
                 'Authorization': `Bearer ${apiKey || EncodeUtils.base64Decode(defaultKey)}`
             },
             body: JSON.stringify({
-                model: "yi-large",
-                messages: [{ role: "user", content: prompt }],
-                temperature: 0.3,
-                stream: true
+                "model": "Qwen/Qwen2.5-Coder-7B-Instruct",
+                "messages": msgs, // 直接传递多轮历史
+                "stream": true, // 开启流式输出
+                "max_tokens": 4096,
+                "enable_thinking": true,
+                "thinking_budget": 4096,
+                "min_p": 0.05,
+                "stop": [],
+                "temperature": 0.7,
+                "top_p": 0.7,
+                "top_k": 50,
+                "frequency_penalty": 0.5,
+                "n": 1,
+                "response_format": {
+                    "type": "text"
+                }
             })
         };
-    
         try {
             const response = await fetch(url, options);
             if (!response.ok) {
                 throw new Error(`HTTP error! status: ${response.status}`);
             }
-    
-            // 创建一个ReadableStream用于处理流式数据
+            // 处理流式返回(text/event-stream)
             const reader = response.body.getReader();
             const decoder = new TextDecoder('utf-8');
+            let buffer = '';
             let done = false;
-    
+            const msg = {id:'',content:''};
             while (!done) {
                 const { value, done: readerDone } = await reader.read();
                 done = readerDone;
                 if (value) {
-                    // 将接收到的数据块解码为字符串,并假设每一行都是一个独立的JSON对象
-                    const lines = decoder.decode(value, { stream: true }).split('\n').filter(line => line.trim() !== '');
-                    for (const line of lines) {
+                    buffer += decoder.decode(value, { stream: true });
+                    // 以换行分割,逐条处理
+                    let lines = buffer.split('\n');
+                    // 最后一行可能不完整,留到下次
+                    buffer = lines.pop();
+                    for (let line of lines) {
+                        line = line.trim();
+                        if (!line || !line.startsWith('data:')) continue;
+                        let jsonStr = line.replace(/^data:/, '').trim();
+                        if (jsonStr === '[DONE]') continue;
                         try {
-                            // 解析每一行作为单独的JSON对象
-                            const message = JSON.parse(line.replace(/^data:\s+/,''));
-                            receivingCallback && receivingCallback(message);
-                        } catch (jsonError) {
-                            if(line === 'data: [DONE]'){
-                                receivingCallback && receivingCallback(null,true);
+                            let obj = JSON.parse(jsonStr);
+                            if (obj.choices && obj.choices[0] && obj.choices[0].delta) {
+                                msg.id = obj.id;
+                                msg.created = obj.created;
+                                msg.content += obj.choices[0].delta.content;
+                                receivingCallback && receivingCallback(msg);
                             }
+                        } catch (e) {
+                            // 忽略解析失败的片段
                         }
                     }
                 }
             }
+            receivingCallback && receivingCallback(null,true);
         } catch (error) {
-            console.error('Error fetching chat completions:', error);
+            console.error('Error fetching coderVM stream:', error);
         }
     }
     
-    return {askYiLarge: streamChatCompletions};
+    return {askCoderLLM};
 })();
 
 
-export default AI;
+export default AI;
+

+ 112 - 1
apps/aiagent/index.css

@@ -103,12 +103,18 @@ ul.x-demos li:hover {
     border:1px solid #eee;
 }
 .box-message table {
-    width:100%;
+    width: 100%;
+    table-layout: fixed;
 }
 .box-message table td{
     padding: 10px 5px;
     vertical-align: top;
 }
+.box-message .td-content {
+    word-break: break-all;
+    white-space: pre-wrap;
+    max-width: 100%;
+}
 .x-from-fh {
     border: 1px solid #eee;
     border-width: 1px 0 1px;
@@ -206,3 +212,108 @@ h3.panel-title {
 .x-xcontent table td {
     border: 1px solid #ccc;
 }
+.x-donate-link {
+    right: 210px;
+    top: 0px;
+}
+.panel-title>a.x-other-tools {
+    top: -2px;
+}
+
+.fh-history-sidebar {
+    position: fixed;
+    top: 0;
+    left: 0;
+    width: 280px;
+    height: 100vh;
+    background: #fff;
+    border-right: 1px solid #eee;
+    z-index: 10000;
+    box-shadow: 2px 0 8px rgba(0,0,0,0.04);
+    display: flex;
+    flex-direction: column;
+}
+.fh-history-header {
+    padding: 18px 16px 10px 16px;
+    font-size: 18px;
+    font-weight: bold;
+    border-bottom: 1px solid #eee;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+}
+.fh-history-close {
+    background: none;
+    border: none;
+    font-size: 16px;
+    color: #888;
+    cursor: pointer;
+}
+.fh-history-list {
+    flex: 1;
+    overflow-y: auto;
+    padding: 12px 0 12px 0;
+}
+.fh-history-date {
+    font-size: 14px;
+    color: #888;
+    margin: 10px 0 4px 18px;
+}
+.fh-history-list ul {
+    list-style: none;
+    margin: 0 0 0 8px;
+    padding: 0;
+}
+.fh-history-list li {
+    padding: 8px 18px;
+    cursor: pointer;
+    border-radius: 6px;
+    transition: background 0.2s;
+    font-size: 15px;
+    color: #333;
+}
+.fh-history-list li:hover {
+    background: #f5f7fa;
+}
+.fh-history-theme {
+    font-weight: 500;
+    color: #222;
+}
+.fh-main-shrink {
+    margin-left: 280px !important;
+    transition: margin-left 0.2s;
+}
+
+.fh-nav-btn {
+    display: inline-block;
+    margin-left: 18px;
+    padding: 3px 14px 3px 10px;
+    border-radius: 18px;
+    background: #f5f7fa;
+    color: #333;
+    font-size: 12px;
+    font-weight: normal;
+    text-decoration: none;
+    transition: background 0.2s, color 0.2s;
+    border: 1px solid #e0e0e0;
+    box-shadow: 0 1px 2px rgba(0,0,0,0.03);
+    vertical-align: middle;
+}
+.fh-nav-btn:hover {
+    background: #e6f0ff;
+    color: #1976d2;
+    border-color: #b3d1ff;
+}
+.x-history-link .icon-clock {
+    color: #1976d2;
+}
+.x-newchat-link .icon-plus-circle {
+    color: #43a047;
+}
+td.td-icon {
+    width: 40px;
+    text-align: center;
+}
+td.td-content .x-xcontent.x-user-content {
+    white-space: pre-wrap;
+}

+ 73 - 48
apps/aiagent/index.html

@@ -1,7 +1,7 @@
 <!DOCTYPE HTML>
 <html lang="zh-CN">
 <head>
-    <title>AI,请帮帮忙</title>
+    <title>AI(智能助手)</title>
     <meta charset="UTF-8">
     <link rel="shortcut icon" href="../static/img/favicon.ico">
     <link rel="stylesheet" href="index.css"/>
@@ -14,56 +14,79 @@
     <div class="panel panel-default" style="margin-bottom: 0px;">
         <div class="panel-heading">
             <h3 class="panel-title">
-                <a href="https://www.baidufe.com/fehelper/index/index.html" target="_blank" class="x-a-high">
-                    <img src="../static/img/fe-16.png" alt="fehelper"/> FeHelper:</a>AI,请帮帮忙
-            </h3>
+                <a href="https://fehelper.com" target="_blank" class="x-a-high">
+                    <img src="../static/img/fe-16.png" alt="fehelper"/> FeHelper:</a>AI(智能助手)
+                    <a href="#" class="x-donate-link" @click="openDonateModal($event)"><i class="nav-icon">❤&nbsp;</i>打赏鼓励</a>
+                    <a class="x-other-tools" @click="openOptionsPage($event)"><i class="icon-plus-circle"></i> 探索更多实用工具 <span class="tool-market-badge">工具市场</span></a>
+                    <a href="#" class="x-history-link fh-nav-btn" @click="onHistoryClick($event)">
+                        <i class="icon-clock" style="margin-right:4px;font-size:18px;vertical-align:-2px;"></i>历史对话
+                    </a>
+                    <a href="#" class="x-newchat-link fh-nav-btn" @click="startNewChat($event)">
+                        <i class="icon-plus-circle" style="margin-right:4px;font-size:18px;vertical-align:-2px;"></i>开启新对话
+                    </a>
+                </h3>
+            </div>
         </div>
-    </div>
-    <div class="panel-body">
-        <div class="row mod-inputs box-prompt">
-            <form class="ui-mt-10" @submit.prevent="goChat">
-                <textarea type="text" id="prompt" ref="prompt" v-model="prompt" class="form-control" placeholder="你有什么内容想要咨询AI的?"
-                    data-key="c2stNEJUbWRxYW00eENTUXJXRnlNajFUM0JsYmtGSlVlbDJnZ2dGbjI5MVBKQVdzZnZR"></textarea>
-                <input class="btn btn-sm btn-primary btn-chat" type="button" value="发送">
-            </form>
+        <div v-if="showHistoryPanel" class="fh-history-sidebar">
+            <div class="fh-history-header">
+                <span>历史对话</span>
+                <button class="fh-history-close" @click="showHistoryPanel = false">关闭</button>
+            </div>
+            <div class="fh-history-list">
+                <template v-for="(day, date) in groupedHistory">
+                    <div class="fh-history-date">{{date}}</div>
+                    <ul>
+                        <li v-for="item in day" @click="loadHistory(item)">
+                            <span class="fh-history-theme">{{item.theme || item.message}}</span>
+                        </li>
+                    </ul>
+                </template>
+            </div>
         </div>
-        <div class="row mod-inputs box-result" ref="boxResult">
-            <div class="row box-tips" v-if="!hideDemo">
-                <div>你好,我是你的专属AI助理,不管你是要查BUG、写代码、还是要咨询什么技术问题,你都可以跟我说,我会竭尽全力帮你解决,比如你可以这样问我👇👇👇</div>
-                <ul class="x-demos clearfix">
-                    <li v-for="demo in demos" @click="sendMessage(demo)">{{demo}}</li>
-                </ul>
+        <div :class="['panel-body', showHistoryPanel ? 'fh-main-shrink' : '']">
+            <div class="row mod-inputs box-prompt">
+                <form class="ui-mt-10" @submit.prevent="goChat">
+                    <textarea type="text" id="prompt" ref="prompt" v-model="prompt" class="form-control" placeholder="你有什么内容想要咨询AI的?(按Enter发送,Shift+Enter换行)"
+                        @keydown="onPromptKeydown"
+                    ></textarea>
+                    <input class="btn btn-sm btn-primary btn-chat" type="button" value="发送" @click="goChat">
+                </form>
             </div>
-            <div class="row box-message">
-                <table>
-                    <template v-for="msg in history">
-                        <tr class="x-from-fh">
-                            <td class="td-icon x-me"><img src="../static/img/me.png" alt="me"/></td>
-                            <td class="td-content"><b class="x-time">{{msg.sendTime}} </b><div class="x-xcontent">{{msg.message}}</div></td>
-                        </tr>
-                        <tr class="x-back-gpt">
-                            <td class="td-icon">
-                                <img src="../static/img/fe-16.png" alt="fehelper"/></td></td>
-                            <td class="td-content"><b class="x-time">{{msg.respTime}}</b>
-                                <div :id="msg.id" class="x-xcontent" v-html="msg.respContent"></div>
-                            </td>
-                        </tr>
-                    </template>
-
-                    <template v-if="respResult.id">
-                        <tr class="x-from-fh">
-                            <td class="td-icon x-me"><img src="../static/img/me.png" alt="me"/></td>
-                            <td class="td-content"><b class="x-time">{{respResult.sendTime}} </b><div class="x-xcontent">{{respResult.message}}</div></td>
-                        </tr>
-                        <tr class="x-back-gpt">
-                            <td class="td-icon">
-                                <img src="../static/img/fe-16.png" alt="fehelper"/></td>
-                            <td class="td-content"><b class="x-time">{{respResult.respTime}}</b>
-                                <div :id="respResult.id" class="x-xcontent" v-html="respResult.respContent"></div>
-                            </td>
-                        </tr>
-                    </template>
-                </table>
+            <div class="row mod-inputs box-result" ref="boxResult">
+                <div class="row box-tips" v-if="!hideDemo">
+                    <div>你好,我是你的专属AI助理,不管你是要查BUG、写代码、还是要咨询什么技术问题,你都可以跟我说,我会竭尽全力帮你解决,比如你可以这样问我👇👇👇</div>
+                    <ul class="x-demos clearfix">
+                        <li v-for="demo in demos" @click="sendMessage(demo)">{{demo}}</li>
+                    </ul>
+                </div>
+                <div class="row box-message">
+                    <table>
+                        <template v-if="currentSession.length">
+                            <tr v-for="(msg, idx) in currentSession" :key="msg.id">
+                                <td class="td-icon x-me" v-if="msg.role==='user'"><img src="../static/img/me.png" alt="me"/></td>
+                                <td class="td-icon" v-if="msg.role==='assistant'"><img src="../static/img/fe-16.png" alt="fehelper"/></td>
+                                <td class="td-content">
+                                    <b class="x-time">{{msg.time}}</b>
+                                    <div class="x-xcontent x-user-content" v-if="msg.role==='user'">{{msg.content}}</div>
+                                    <div class="x-xcontent" v-if="msg.role==='assistant'" v-html="msg.content"></div>
+                                </td>
+                            </tr>
+                        </template>
+                        <template v-else-if="respResult.id">
+                            <tr class="x-from-fh">
+                                <td class="td-icon x-me"><img src="../static/img/me.png" alt="me"/></td>
+                                <td class="td-content x-user-content"><b class="x-time">{{respResult.sendTime}} </b><div class="x-xcontent">{{respResult.message}}</div></td>
+                            </tr>
+                            <tr class="x-back-gpt">
+                                <td class="td-icon">
+                                    <img src="../static/img/fe-16.png" alt="fehelper"/></td>
+                                <td class="td-content"><b class="x-time">{{respResult.respTime}}</b>
+                                    <div :id="respResult.id" class="x-xcontent" v-html="respResult.respContent"></div>
+                                </td>
+                            </tr>
+                        </template>
+                    </table>
+                </div>
             </div>
         </div>
     </div>
@@ -76,3 +99,5 @@
 <script type="module" src="index.js"></script>
 </body>
 </html>
+
+

+ 339 - 15
apps/aiagent/index.js

@@ -10,9 +10,10 @@ new Vue({
     data: {
         prompt: '',
         demos: [
-            'FeHelper是什么?怎么安装?',
             '用Js写一个冒泡排序的Demo',
-            'Js里的Fetch API是怎么用的'
+            'Js里的Fetch API是怎么用的',
+            '帮我写一个单网页版的俄罗斯方块游戏',
+            '我开发了一个浏览器插件,是专门为HR自动找简历的,现在请你帮我用SVG绘制一个插件的ICON,不需要问我细节,直接生成'
         ],
         initMessage: {
             id:'id-test123',
@@ -28,16 +29,77 @@ new Vue({
             respTime:'',
             respContent:''
         },
+        currentSession: [],
         history:[],
         tempId:'',
         hideDemo: false,
-        undergoing: false
+        undergoing: false,
+        messages: [],
+        showHistoryPanel: false
+    },
+    computed: {
+        groupedHistory() {
+            // 按日期分组,主题为message前20字
+            const groups = {};
+            this.history.forEach(item => {
+                const date = item.sendTime ? item.sendTime.split(' ')[0] : '未知日期';
+                if (!groups[date]) groups[date] = [];
+                groups[date].push({
+                    ...item,
+                    theme: item.message ? item.message.slice(0, 20) : ''
+                });
+            });
+            return groups;
+        }
+    },
+    watch: {
+        history: {
+            handler(val) {
+                localStorage.setItem('fh-aiagent-history', JSON.stringify(val));
+            },
+            deep: true
+        }
     },
     mounted: function () {
         this.$refs.prompt.focus();
         this.hideDemo = !!(new URL(location.href)).searchParams.get('hideDemo');
+        // 加载本地历史
+        const local = localStorage.getItem('fh-aiagent-history');
+        if(local){
+            try {
+                this.history = JSON.parse(local);
+            } catch(e) {}
+        }
+        this.loadPatchHotfix();
     },
     methods: {
+
+        loadPatchHotfix() {
+            // 页面加载时自动获取并注入页面的补丁
+            chrome.runtime.sendMessage({
+                type: 'fh-dynamic-any-thing',
+                thing: 'fh-get-tool-patch',
+                toolName: 'aiagent'
+            }, patch => {
+                if (patch) {
+                    if (patch.css) {
+                        const style = document.createElement('style');
+                        style.textContent = patch.css;
+                        document.head.appendChild(style);
+                    }
+                    if (patch.js) {
+                        try {
+                            if (window.evalCore && window.evalCore.getEvalInstance) {
+                                window.evalCore.getEvalInstance(window)(patch.js);
+                            }
+                        } catch (e) {
+                            console.error('aiagent补丁JS执行失败', e);
+                        }
+                    }
+                }
+            });
+        },
+
         // 这个代码,主要用来判断大模型返回的内容是不是包含完整的代码块
         validateCodeBlocks(content) {
             let backticksCount = 0;
@@ -71,8 +133,14 @@ new Vue({
         sendMessage(prompt){
             if(this.undergoing) return;
             if(this.respResult.id){
-                this.history.push(this.respResult);
-                this.respResult.id = '';
+                // 先存储上一轮对话到历史
+                this.history.push({
+                    id: this.respResult.id,
+                    sendTime: this.respResult.sendTime,
+                    message: this.respResult.message,
+                    respTime: this.respResult.respTime,
+                    respContent: this.respResult.respContent
+                });
             }
 
             this.undergoing = true;
@@ -83,13 +151,52 @@ new Vue({
 
             this.tempId = '';
             let respContent = '';
-            AI.askYiLarge(prompt,(respJson,done) => {
+
+            // 1. 先把用户输入 push 到 messages
+            this.messages.push({ role: 'user', content: prompt });
+            // 新增:用户消息push到currentSession
+            this.currentSession.push({
+                role: 'user',
+                id: 'user-' + Date.now(),
+                time: sendTime,
+                content: prompt
+            });
+
+            AI.askCoderLLM(this.messages, (respJson, done) => {
                 if(done){
                     this.undergoing = false;
+                    if(this.respResult.id && this.respResult.respContent){
+                        const tempDiv = document.createElement('div');
+                        tempDiv.innerHTML = this.respResult.respContent;
+                        const plainText = tempDiv.textContent || tempDiv.innerText || '';
+                        this.messages.push({ role: 'assistant', content: plainText });
+                        this.history.push({
+                            id: this.respResult.id,
+                            sendTime: this.respResult.sendTime,
+                            message: this.respResult.message,
+                            respTime: this.respResult.respTime,
+                            respContent: this.respResult.respContent
+                        });
+                    }
+                    this.$nextTick(() => {
+                        document.querySelectorAll('.x-xcontent pre code').forEach((block) => {
+                            hljs.highlightBlock(block);
+                            insertCodeToolbar(block);
+                        });
+                        this.scrollToBottom();
+                    });
                     return;
                 }
                 let id = respJson.id;
-                respContent = respJson.content || '';
+                let rawContent = respJson.content || '';
+                // 检查多轮代码补全场景
+                const lastAssistantMsg = this.currentSession.slice().reverse().find(m => m.role === 'assistant');
+                const lastIsCodeBlock = lastAssistantMsg && /```\s*$/.test(lastAssistantMsg.content.trim());
+                const thisIsCodeBlock = /^```/.test(rawContent.trim());
+                if (lastIsCodeBlock && !thisIsCodeBlock) {
+                    rawContent = '```js\n' + rawContent.trim() + '\n```';
+                }
+                respContent = rawContent;
                 if(!this.validateCodeBlocks(respContent)) {
                     respContent += '\n```';
                 }
@@ -99,17 +206,21 @@ new Vue({
                     let dateTime = new Date(respJson.created * 1000);
                     let respTime = dateTime.format('yyyy/MM/dd HH:mm:ss');
                     this.respResult = { id,sendTime,message:prompt,respTime,respContent };
+                    // 新增:助手回复push到currentSession
+                    this.currentSession.push({
+                        role: 'assistant',
+                        id,
+                        time: respTime,
+                        content: respContent
+                    });
                 }else{
                     this.respResult.respContent = respContent;
+                    // 更新最后一条助手消息内容
+                    if(this.currentSession.length && this.currentSession[this.currentSession.length-1].role==='assistant'){
+                        this.currentSession[this.currentSession.length-1].content = respContent;
+                    }
                 }
-                
-                this.$nextTick(() => {
-                    let elm = document.getElementById(id);
-                    elm.querySelectorAll('pre code').forEach((block) => {
-                        hljs.highlightBlock(block);
-                    });
-                    this.scrollToBottom();
-                });
+                this.$nextTick(() => this.scrollToBottom());
             });            
         },
 
@@ -119,7 +230,220 @@ new Vue({
         goChat(){
             this.sendMessage(this.prompt);
             this.$nextTick(() => this.prompt='');
+        },
+
+        openOptionsPage: function(event) {
+            event.preventDefault();
+            event.stopPropagation();
+            chrome.runtime.openOptionsPage();
+        },
+
+        openDonateModal: function(event ){
+            event.preventDefault();
+            event.stopPropagation();
+            chrome.runtime.sendMessage({
+                type: 'fh-dynamic-any-thing',
+                thing: 'open-donate-modal',
+                params: { toolName: 'aiagent' }
+            });
+        },
+
+        loadHistory(item) {
+            // 渲染到主面板
+            this.respResult = {
+                id: item.id,
+                sendTime: item.sendTime,
+                message: item.message,
+                respTime: item.respTime,
+                respContent: item.respContent
+            };
+            this.showHistoryPanel = false;
+            this.$nextTick(() => this.scrollToBottom());
+        },
+
+        startNewChat(event) {
+            event && event.preventDefault();
+            this.messages = [];
+            this.respResult = {
+                id: '',
+                sendTime: '',
+                message: '',
+                respTime: '',
+                respContent: ''
+            };
+            this.currentSession = [];
+            this.showHistoryPanel = false;
+            this.$nextTick(() => {
+                this.$forceUpdate();
+                this.scrollToBottom();
+            });
+        },
+
+        onHistoryClick(event) {
+            event.preventDefault();
+            event.stopPropagation();
+            this.showHistoryPanel = !this.showHistoryPanel;
+        },
+
+        onPromptKeydown(e) {
+            if (e.key === 'Enter') {
+                if (e.shiftKey) {
+                    // 允许换行
+                    return;
+                } else {
+                    // 阻止默认换行,发送消息
+                    e.preventDefault();
+                    this.goChat();
+                }
+            }
         }
     }
 
 });
+
+// 工具函数:复制和运行
+function copyCode(code) {
+    if (navigator.clipboard) {
+        navigator.clipboard.writeText(code);
+    } else {
+        const textarea = document.createElement('textarea');
+        textarea.value = code;
+        document.body.appendChild(textarea);
+        textarea.select();
+        document.execCommand('copy');
+        document.body.removeChild(textarea);
+    }
+}
+
+// 新增:为代码块插入工具栏
+function insertCodeToolbar(block) {
+    // 检查是否已插入按钮,避免重复
+    if (block.parentNode.querySelector('.fh-code-toolbar')) return;
+
+    // 创建工具栏
+    const toolbar = document.createElement('div');
+    toolbar.className = 'fh-code-toolbar';
+    toolbar.style.cssText = 'position:absolute;bottom:6px;right:12px;z-index:10;display:flex;gap:8px;';
+
+    // 复制按钮
+    const btnCopy = document.createElement('button');
+    btnCopy.innerText = '复制';
+    btnCopy.className = 'fh-btn-copy';
+    btnCopy.style.cssText = 'padding:2px 8px;font-size:12px;cursor:pointer;';
+    btnCopy.onclick = (e) => {
+        e.stopPropagation();
+        copyCode(block.innerText);
+        // 复制成功反馈
+        const oldText = btnCopy.innerText;
+        btnCopy.innerText = '已复制';
+        btnCopy.disabled = true;
+        setTimeout(() => {
+            btnCopy.innerText = oldText;
+            btnCopy.disabled = false;
+        }, 1000);
+    };
+
+    // 运行按钮
+    const lang = (block.className || '').toLowerCase();
+    let btnRun = document.createElement('button');
+    btnRun.className = 'fh-btn-run';
+    btnRun.style.cssText = 'padding:2px 8px;font-size:12px;cursor:pointer;';
+    let shouldAppendBtnRun = true;
+    if (lang.includes('lang-javascript') || lang.includes('lang-js')) {
+        btnRun.innerText = 'Console运行';
+        btnRun.onclick = (e) => {
+            e.stopPropagation();
+            copyCode(block.innerText);
+            btnRun.innerText = '已复制到剪贴板';
+            btnRun.disabled = true;
+            setTimeout(() => {
+                btnRun.innerText = 'Console运行';
+                btnRun.disabled = false;
+            }, 1200);
+            showToast('代码已复制到剪贴板,请按F12打开开发者工具,切换到Console粘贴回车即可运行!');
+        };
+    } else if (lang.includes('lang-html')) {
+        btnRun.innerText = '下载并运行';
+        btnRun.onclick = (e) => {
+            e.stopPropagation();
+            const blob = new Blob([block.innerText], {type: 'text/html'});
+            const url = URL.createObjectURL(blob);
+            const a = document.createElement('a');
+            a.href = url;
+            a.download = 'fehelper-demo.html';
+            document.body.appendChild(a);
+            a.click();
+            document.body.removeChild(a);
+            URL.revokeObjectURL(url);
+            btnRun.innerText = '已下载';
+            btnRun.disabled = true;
+            setTimeout(() => {
+                btnRun.innerText = '下载并运行';
+                btnRun.disabled = false;
+            }, 1200);
+            showToast('HTML文件已下载,请双击打开即可运行!');
+        };
+    } else if (lang.includes('lang-xml') || lang.includes('lang-svg')) {
+        // 检查内容是否为svg
+        const codeText = block.innerText.trim();
+        if (/^<svg[\s\S]*<\/svg>$/.test(codeText)) {
+            btnRun.innerText = '点击预览';
+            btnRun.onclick = (e) => {
+                e.stopPropagation();
+                // 弹窗预览svg
+                const modal = document.createElement('div');
+                modal.style.cssText = 'position:fixed;left:0;top:0;width:100vw;height:100vh;background:rgba(0,0,0,0.5);z-index:999999;display:flex;align-items:center;justify-content:center;';
+                const inner = document.createElement('div');
+                inner.style.cssText = 'background:#fff;width:400px;height:400px;border-radius:10px;box-shadow:0 2px 16px rgba(0,0,0,0.18);position:relative;display:flex;align-items:center;justify-content:center;';
+                const closeBtn = document.createElement('button');
+                closeBtn.innerText = '×';
+                closeBtn.style.cssText = 'position:absolute;top:8px;right:12px;width:32px;height:32px;font-size:22px;line-height:28px;background:transparent;border:none;cursor:pointer;color:#888;z-index:2;';
+                closeBtn.onclick = () => document.body.removeChild(modal);
+                const img = document.createElement('img');
+                img.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(codeText)));
+                img.alt = 'SVG预览';
+                img.style.cssText = 'max-width:90%;max-height:90%;display:block;border:1px solid #eee;background:#fafbfc;';
+                inner.appendChild(closeBtn);
+                inner.appendChild(img);
+                modal.appendChild(inner);
+                document.body.appendChild(modal);
+            };
+        } else {
+            btnRun.remove();
+            shouldAppendBtnRun = false;
+        }
+    } else {
+        btnRun.remove();
+        shouldAppendBtnRun = false;
+    }
+
+    toolbar.appendChild(btnCopy);
+    if (shouldAppendBtnRun) {
+        toolbar.appendChild(btnRun);
+    }
+
+    // 让pre相对定位,插入工具栏到底部
+    const pre = block.parentNode;
+    pre.style.position = 'relative';
+    pre.appendChild(toolbar);
+}
+
+// 页面内Toast提示
+function showToast(msg) {
+    let toast = document.createElement('div');
+    toast.className = 'fh-toast';
+    toast.innerText = msg;
+    toast.style.cssText = `
+        position:fixed;left:50%;top:80px;transform:translateX(-50%);
+        background:rgba(0,0,0,0.85);color:#fff;padding:10px 24px;
+        border-radius:6px;font-size:16px;z-index:99999;box-shadow:0 2px 8px rgba(0,0,0,0.2);
+        transition:opacity 0.3s;opacity:1;
+    `;
+    document.body.appendChild(toast);
+    setTimeout(() => {
+        toast.style.opacity = '0';
+        setTimeout(() => document.body.removeChild(toast), 300);
+    }, 1800);
+}
+
+

+ 144 - 93
apps/background/awesome.js

@@ -20,6 +20,7 @@ let Awesome = (() => {
      */
     let StorageMgr = (() => {
 
+        // 获取chrome.storage.local中的内容,返回Promise,可直接await
         let get = keyArr => {
             return new Promise((resolve, reject) => {
                 chrome.storage.local.get(keyArr, result => {
@@ -28,15 +29,6 @@ let Awesome = (() => {
             });
         };
 
-
-        let getSync = async (keyArr) => {
-            return await (new Promise((resolve, reject) => {
-                chrome.storage.local.get(keyArr, result => {
-                    resolve(typeof keyArr === 'string' ? result[keyArr] : result);
-                });
-            }));
-        };
-
         let set = (items, values) => {
             return new Promise((resolve, reject) => {
                 if (typeof items === 'string') {
@@ -59,7 +51,7 @@ let Awesome = (() => {
             });
         };
 
-        return {get, set, remove,getSync};
+        return {get, set, remove};
     })();
 
     /**
@@ -99,7 +91,7 @@ let Awesome = (() => {
     let install = (toolName, fnProgress) => {
         return new Promise((resolve, reject) => {
             // 存储html文件
-            StorageMgr.set(TOOL_NAME_TPL.replace('#TOOL-NAME#', toolName), '&nbsp;');
+            StorageMgr.set(TOOL_NAME_TPL.replace('#TOOL-NAME#', toolName), new Date().getTime());
             log(toolName + '工具html模板安装/更新成功!');
             resolve();
         });
@@ -113,7 +105,9 @@ let Awesome = (() => {
 
         // 删除所有静态文件
         chrome.storage.local.get(null, allDatas => {
-            StorageMgr.remove(Object.keys(allDatas).filter(key => String(key).startsWith(`../${toolName}/`)));
+            if (allDatas) {
+                StorageMgr.remove(Object.keys(allDatas).filter(key => String(key).startsWith(`../${toolName}/`)));
+            }
         });
 
         log(toolName + ' 卸载成功!');
@@ -124,18 +118,21 @@ let Awesome = (() => {
     /**
      * 有些工具其实已经卸载过了,但是本地还有冗余的静态文件,都需要统一清理一遍
      */
-    let gcLocalFiles = () => getAllTools().then(tools => Object.keys(tools).forEach(tool => {
-        if (!tools[tool] || !tools[tool]._devTool && !tools[tool].installed) {
-            offLoad(tool);
-        }
-    }));
+    let gcLocalFiles = () => getAllTools().then(tools => {
+        if (!tools) return;
+        Object.keys(tools).forEach(tool => {
+            if (!tools[tool] || !tools[tool]._devTool && !tools[tool].installed) {
+                offLoad(tool);
+            }
+        });
+    });
 
     let getAllTools = async () => {
 
         // 获取本地开发的插件,也拼接进来
         try {
             const DEV_TOOLS_MY_TOOLS = 'DEV-TOOLS:MY-TOOLS';
-            let _tools = await StorageMgr.getSync(DEV_TOOLS_MY_TOOLS);
+            let _tools = await StorageMgr.get(DEV_TOOLS_MY_TOOLS);
             let localDevTools = JSON.parse(_tools || '{}');
             Object.keys(localDevTools).forEach(tool => {
                 toolMap[tool] = localDevTools[tool];
@@ -148,8 +145,8 @@ let Awesome = (() => {
         tools.forEach(tool => {
             promises = promises.concat([detectInstall(tool), detectInstall(tool, true)])
         });
-        let pAll = Promise.all(promises).then(values => {
-            values.forEach((v, i) => {
+        return Promise.all(promises).then(values => {
+            (values || []).forEach((v, i) => {
                 let tool = tools[Math.floor(i / 2)];
                 let key = i % 2 === 0 ? 'installed' : 'menu';
                 toolMap[tool][key] = v;
@@ -158,46 +155,87 @@ let Awesome = (() => {
                     toolMap[tool][key] = toolMap[tool][key] && toolMap[tool]._enable;
                 }
             });
+
             return toolMap;
         });
-        let pSort = SortToolMgr.get();
-
-        return Promise.all([pAll,pSort]).then(vs => {
-            let allTools = vs[0];
-            let sortTools = vs[1];
-
-            if (sortTools && sortTools.length) {
-                let map = {};
-                sortTools.forEach(tool => {
-                    if(allTools[tool]) {
-                        map[tool] = allTools[tool];
-                    }
-                });
-                Object.keys(allTools).forEach(tool => {
-                    if (!map[tool]) {
-                        map[tool] = allTools[tool];
-                    }
-                });
-                return map;
-            }else{
-                return allTools;
-            }
-        });
     };
 
     /**
-     * 检查看本地已安装过哪些工具
+     * 检查看本地已安装过哪些工具 - 性能优化版本
      * @returns {Promise}
      */
-    let getInstalledTools = () => getAllTools().then(tools => {
-        let istTolls = {};
-        Object.keys(tools).filter(tool => {
-            if (tools[tool] && tools[tool].installed) {
-                istTolls[tool] = tools[tool];
+    let getInstalledTools = async () => {
+        try {
+            // 一次性获取所有存储数据,避免多次访问
+            const allStorageData = await new Promise((resolve, reject) => {
+                chrome.storage.local.get(null, result => {
+                    resolve(result || {});
+                });
+            });
+
+            // 获取本地开发的插件
+            const DEV_TOOLS_MY_TOOLS = 'DEV-TOOLS:MY-TOOLS';
+            let localDevTools = {};
+            try {
+                localDevTools = JSON.parse(allStorageData[DEV_TOOLS_MY_TOOLS] || '{}');
+                Object.keys(localDevTools).forEach(tool => {
+                    toolMap[tool] = localDevTools[tool];
+                });
+            } catch (e) {
+                // 忽略解析错误
             }
-        });
-        return istTolls;
-    });
+
+            let installedTools = {};
+            
+            // 遍历所有工具,从存储数据中检查安装状态
+            Object.keys(toolMap).forEach(toolName => {
+                const toolKey = TOOL_NAME_TPL.replace('#TOOL-NAME#', toolName);
+                const menuKey = TOOL_MENU_TPL.replace('#TOOL-NAME#', toolName);
+                
+                // 检查工具是否已安装
+                let toolInstalled = !!allStorageData[toolKey];
+                // 系统预置的功能,是强制 installed 状态的
+                if (toolMap[toolName] && toolMap[toolName].systemInstalled) {
+                    toolInstalled = true;
+                }
+                
+                // 检查菜单状态
+                let menuInstalled = String(allStorageData[menuKey]) === '1';
+                
+                // 本地工具,还需要看是否处于开启状态
+                if (toolMap[toolName].hasOwnProperty('_devTool')) {
+                    toolInstalled = toolInstalled && toolMap[toolName]._enable;
+                    menuInstalled = menuInstalled && toolMap[toolName]._enable;
+                }
+                
+                // 只收集已安装的工具
+                if (toolInstalled) {
+                    installedTools[toolName] = {
+                        ...toolMap[toolName],
+                        installed: true,
+                        menu: menuInstalled,
+                        installTime: parseInt(allStorageData[toolKey]) || 0
+                    };
+                }
+            });
+
+            // 按安装时间排序
+            const sortedToolNames = Object.keys(installedTools).sort((a, b) => {
+                return installedTools[a].installTime - installedTools[b].installTime;
+            });
+
+            let sortedToolMap = {};
+            sortedToolNames.forEach(toolName => {
+                sortedToolMap[toolName] = installedTools[toolName];
+            });
+            
+            return sortedToolMap;
+        } catch (error) {
+            console.error('getInstalledTools error:', error);
+            // 发生错误时返回空对象,避免popup完全无法加载
+            return {};
+        }
+    };
 
     /**
      * 获取工具的content-script
@@ -251,48 +289,61 @@ let Awesome = (() => {
         }
     };
 
-    /**
-     * 远程获取的代码管理器
-     * @type {{get, set}}
-     */
-    let CodeCacheMgr = (() => {
-        const TOOLS_FROM_REMOTE = 'TOOLS_FROM_REMOTE';
-
-        let get = () => {
-            return StorageMgr.getSync(TOOLS_FROM_REMOTE);
-        };
-
-        let set = (remoteCodes) => {
-            let obj = {};
-            obj[TOOLS_FROM_REMOTE]=remoteCodes;
-            chrome.storage.local.set(obj);
-        };
-
-        return {get, set};
-    })();
 
     /**
-     * 工具排序管理器
-     * @type {{get, set}}
+     * 采集客户端信息并发送给background
      */
-    let SortToolMgr = (() => {
-        const TOOLS_CUSTOM_SORT = 'TOOLS_CUSTOM_SORT';
-
-        let get = async () => {
-            let cache = await StorageMgr.getSync(TOOLS_CUSTOM_SORT);
-
-            return [].concat(JSON.parse(cache || '[]')).filter(t => !!t);
-        };
-
-        let set = (newSortArray) => {
-            let obj = {};
-            obj[TOOLS_CUSTOM_SORT] = JSON.stringify([].concat(newSortArray || []).filter(t => !!t));
-            chrome.storage.local.set(obj);
-        };
-
-        return {get, set};
-    })();
-
+    let collectAndSendClientInfo = () => {
+        try {
+            const nav = navigator;
+            const screenInfo = window.screen;
+            const lang = nav.language || nav.userLanguage || '';
+            const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || '';
+            const ua = nav.userAgent;
+            const platform = nav.platform;
+            const vendor = nav.vendor;
+            const colorDepth = screenInfo.colorDepth;
+            const screenWidth = screenInfo.width;
+            const screenHeight = screenInfo.height;
+            const deviceMemory = nav.deviceMemory || '';
+            const hardwareConcurrency = nav.hardwareConcurrency || '';
+            const connection = nav.connection || nav.mozConnection || nav.webkitConnection || {};
+            const screenOrientation = screenInfo.orientation ? screenInfo.orientation.type : '';
+            const touchSupport = ('ontouchstart' in window) || (nav.maxTouchPoints > 0);
+            let memoryJSHeapSize = '';
+            if (window.performance && window.performance.memory) {
+                memoryJSHeapSize = window.performance.memory.jsHeapSizeLimit;
+            }
+            const clientInfo = {
+                language: lang,
+                timezone,
+                userAgent: ua,
+                platform,
+                vendor,
+                colorDepth,
+                screenWidth,
+                screenHeight,
+                deviceMemory,
+                hardwareConcurrency,
+                networkType: connection.effectiveType || '',
+                downlink: connection.downlink || '',
+                rtt: connection.rtt || '',
+                online: nav.onLine,
+                touchSupport,
+                cookieEnabled: nav.cookieEnabled,
+                doNotTrack: nav.doNotTrack,
+                appVersion: nav.appVersion,
+                appName: nav.appName,
+                product: nav.product,
+                vendorSub: nav.vendorSub,
+                screenOrientation,
+                memoryJSHeapSize
+            };
+            chrome.runtime.sendMessage({ type: 'clientInfo', data: clientInfo });
+        } catch (e) {
+            // 忽略采集异常
+        }
+    };
 
     return {
         StorageMgr,
@@ -306,9 +357,9 @@ let Awesome = (() => {
         getToolTpl,
         gcLocalFiles,
         getAllTools,
-        SortToolMgr,
-        CodeCacheMgr
+        collectAndSendClientInfo
     }
 })();
 
 export default Awesome;
+

+ 440 - 59
apps/background/background.js

@@ -10,6 +10,7 @@ import Menu from './menu.js';
 import Awesome from './awesome.js';
 import InjectTools from './inject-tools.js';
 import Monkey from './monkey.js';
+import Statistics from './statistics.js';
 
 
 let BgPageInstance = (function () {
@@ -23,6 +24,9 @@ let BgPageInstance = (function () {
         /^https:\/\/chrome\.google\.com/
     ];
 
+    // 全局缓存最新的客户端信息
+    let FH_CLIENT_INFO = {};
+
     /**
      * 文本格式,可以设置一个图标和标题
      * @param {Object} options
@@ -102,6 +106,41 @@ let BgPageInstance = (function () {
         });
     };
 
+    /**
+     * 打开打赏弹窗
+     * @param {string} toolName - 工具名称
+     */
+    chrome.gotoDonateModal = function (toolName) {
+        chrome.tabs.query({currentWindow: true}, function (tabs) {
+
+            Settings.getOptions((opts) => {
+                let isOpened = false;
+                let tabId;
+                let reg = new RegExp("^chrome.*\\/options\\/index.html\\?donate_from=" + toolName + "$", "i");
+                for (let i = 0, len = tabs.length; i < len; i++) {
+                    if (reg.test(tabs[i].url)) {
+                        isOpened = true;
+                        tabId = tabs[i].id;
+                        break;
+                    }
+                }
+
+                if (!isOpened) {
+                    let url = `/options/index.html?donate_from=${toolName}`;
+                    chrome.tabs.create({ url,active: true });
+                } else {
+                    chrome.tabs.update(tabId, {highlighted: true}).then(tab => {
+                        chrome.tabs.reload(tabId);
+                    });
+                }
+                // 记录工具使用
+                Statistics.recordToolUsage('donate',{from: toolName});
+
+            });
+
+        });
+    };
+
     /**
      * 动态运行工具
      * @param configs
@@ -143,6 +182,11 @@ let BgPageInstance = (function () {
 
             activeTab = tabs.filter(tab => tab.active)[0];
 
+            // 如果是二维码工具,且没有传入内容,则使用当前页面的URL
+            if (tool === 'qr-code' && !withContent && activeTab) {
+                withContent = activeTab.url;
+            }
+
             Settings.getOptions((opts) => {
                 let isOpened = false;
                 let tabId;
@@ -198,8 +242,36 @@ let BgPageInstance = (function () {
      * @param callback
      */
     let browserActionClickedHandler = function (request, sender, callback) {
-        chrome.DynamicToolRunner({
-            tool: MSG_TYPE.JSON_FORMAT
+        // 获取当前唯一安装的工具并直接打开
+        Awesome.getInstalledTools().then(tools => {
+            const installedTools = Object.keys(tools).filter(tool => tools[tool].installed);
+            if (installedTools.length === 1) {
+                const singleTool = installedTools[0];
+                chrome.DynamicToolRunner({
+                    tool: singleTool,
+                    noPage: !!tools[singleTool].noPage
+                });
+                
+                // 记录工具使用
+                Statistics.recordToolUsage(singleTool);
+            } else {
+                // 备用方案:如果检测失败,打开JSON格式化工具
+                chrome.DynamicToolRunner({
+                    tool: MSG_TYPE.JSON_FORMAT
+                });
+                
+                // 记录工具使用
+                Statistics.recordToolUsage(MSG_TYPE.JSON_FORMAT);
+            }
+        }).catch(error => {
+            console.error('获取工具列表失败,使用默认工具:', error);
+            // 出错时的备用方案
+            chrome.DynamicToolRunner({
+                tool: MSG_TYPE.JSON_FORMAT
+            });
+            
+            // 记录工具使用
+            Statistics.recordToolUsage(MSG_TYPE.JSON_FORMAT);
         });
     };
 
@@ -212,20 +284,41 @@ let BgPageInstance = (function () {
      */
     let _updateBrowserAction = function (action, showTips, menuOnly) {
         if (!menuOnly) {
-            // 如果有安装过工具,则显示Popup模式
-            Awesome.getInstalledTools().then(tools => {
-                if (Object.keys(tools).length > 1) {
+            // 对于卸载操作,添加一个小延迟确保存储操作完成
+            const delay = action === 'offload' ? 100 : 0;
+            
+            setTimeout(() => {
+                // 如果有安装过工具,则显示Popup模式
+                Awesome.getInstalledTools().then(tools => {
+                // 计算已安装的工具数量
+                const installedTools = Object.keys(tools).filter(tool => tools[tool].installed);
+                const installedCount = installedTools.length;
+                
+                if (installedCount > 1) {
+                    // 多个工具:显示popup
                     chrome.action.setPopup({ popup: '/popup/index.html' });
-                } else {
-                    // 删除popup page
+                    // 移除点击监听器(如果存在)
+                    if (chrome.action.onClicked.hasListener(browserActionClickedHandler)) {
+                        chrome.action.onClicked.removeListener(browserActionClickedHandler);
+                    }
+                } else if (installedCount === 1) {
+                    // 只有一个工具:直接打开工具,不显示popup
                     chrome.action.setPopup({ popup: '' });
-
-                    // 否则点击图标,直接打开页面
+                    
+                    // 添加点击监听器
                     if (!chrome.action.onClicked.hasListener(browserActionClickedHandler)) {
                         chrome.action.onClicked.addListener(browserActionClickedHandler);
                     }
+                } else {
+                    // 没有安装任何工具:显示popup(让用户去安装工具)
+                    chrome.action.setPopup({ popup: '/popup/index.html' });
+                    // 移除点击监听器(如果存在)
+                    if (chrome.action.onClicked.hasListener(browserActionClickedHandler)) {
+                        chrome.action.onClicked.removeListener(browserActionClickedHandler);
+                    }
                 }
-            });
+                });
+            }, delay);
 
             if (action === 'offload') {
                 _animateTips('-1');
@@ -272,11 +365,16 @@ let BgPageInstance = (function () {
 
     let _addScreenShotByPages = function(params,callback){
         chrome.tabs.captureVisibleTab(null, {format: 'png', quality: 100}, uri => {
-            callback({ params,uri });
+            callback({ params, uri });
         });
     };
 
     let _showScreenShotResult = function(data){
+        // 确保截图数据完整有效
+        if (!data || !data.screenshots || !data.screenshots.length) {
+            return;
+        }
+        
         chrome.DynamicToolRunner({
             tool: 'screenshot',
             withContent: data
@@ -296,14 +394,14 @@ let BgPageInstance = (function () {
     };
 
     let _codeBeautify = function(params){
-        if (['javascript', 'css'].includes(params.fileType)) {
-            Awesome.StorageMgr.get('JS_CSS_PAGE_BEAUTIFY').then(val => {
-                if(val !== '0') {
-                    let js = `window._codebutifydetect_('${params.fileType}')`;
-                    InjectTools.inject(params.tabId, { js });
-                }
-            });
-        }
+        Awesome.StorageMgr.get('JS_CSS_PAGE_BEAUTIFY').then(val => {
+            if(val !== '0') {
+                let js = `window._codebutifydetect_('${params.fileType}')`;
+                InjectTools.inject(params.tabId, { js });
+                // 记录工具使用
+                Statistics.recordToolUsage('code-beautify');
+            }
+        });
     };
 
     /**
@@ -316,7 +414,6 @@ let BgPageInstance = (function () {
         chrome.runtime.onMessage.addListener(function (request, sender, callback) {
             // 如果发生了错误,就啥都别干了
             if (chrome.runtime.lastError) {
-                console.log('chrome.runtime.lastError:',chrome.runtime.lastError);
                 return true;
             }
 
@@ -328,10 +425,22 @@ let BgPageInstance = (function () {
             // 截屏
             else if (request.type === MSG_TYPE.CAPTURE_VISIBLE_PAGE) {
                 _captureVisibleTab(callback);
+                // 记录工具使用
+                Statistics.recordToolUsage('screenshot');
+            }
+            // 直接处理content-script.js中的截图请求
+            else if (request.type === 'fh-screenshot-capture-visible') {
+                _captureVisibleTab(callback);
+                // 记录工具使用
+                Statistics.recordToolUsage('screenshot');
             }
             // 打开动态工具页面
             else if (request.type === MSG_TYPE.OPEN_DYNAMIC_TOOL) {
                 chrome.DynamicToolRunner(request);
+                // 记录工具使用
+                if (request.page) {
+                    Statistics.recordToolUsage(request.page);
+                }
                 callback && callback();
             }
             // 打开其他页面
@@ -339,80 +448,111 @@ let BgPageInstance = (function () {
                 chrome.DynamicToolRunner({
                     tool: request.page
                 });
+                // 记录工具使用
+                if (request.page) {
+                    Statistics.recordToolUsage(request.page);
+                }
                 callback && callback();
             }
             // 任何事件,都可以通过这个钩子来完成
             else if (request.type === MSG_TYPE.DYNAMIC_ANY_THING) {
                 switch(request.thing){
+                    // 插件选项保存成功提示
                     case 'save-options':
                         notifyText({
                             message: '配置修改已生效,请继续使用!',
                             autoClose: 2000
                         });
                         break;
+                    // 触发网页截图功能
+                    case 'trigger-screenshot':
+                        handleTriggerScreenshot(request.tabId);
+                        break;
+                    // 获取JSON格式化工具的配置选项
                     case 'request-jsonformat-options':
-                        Awesome.StorageMgr.get(request.params).then(result => {
-                            Object.keys(result).forEach(key => {
-                                if (['MAX_JSON_KEYS_NUMBER', 'JSON_FORMAT_THEME'].includes(key)) {
-                                    result[key] = parseInt(result[key]);
-                                } else {
-                                    result[key] = ('' + result[key] !== 'false');
-                                }
-                            });
-                            callback && callback(result);
-                        });
+                        requestJsonformatOptions(request.params, callback);
                         return true; // 这个返回true是非常重要的!!!要不然callback会拿不到结果
+                    // 保存JSON格式化工具的配置选项
                     case 'save-jsonformat-options':
-                        Awesome.StorageMgr.set(request.params).then(() => {
-                            callback && callback();
-                        });
+                        saveJsonformatOptions(request.params, callback);
                         return true;
+                    // 切换JSON格式化工具栏显示状态
                     case 'toggle-jsonformat-options':
-                        Awesome.StorageMgr.get('JSON_TOOL_BAR_ALWAYS_SHOW').then(result => {
-                            let show = result !== false;
-                            Awesome.StorageMgr.set('JSON_TOOL_BAR_ALWAYS_SHOW',!show).then(() => {
-                                callback && callback(!show);
-                            });
-                        });
+                        toggleJsonformatOptions(callback);
                         return true; // 这个返回true是非常重要的!!!要不然callback会拿不到结果
+                    // 代码美化功能
                     case 'code-beautify':
                         _codeBeautify(request.params);
                         break;
+                    // 关闭代码美化功能
                     case 'close-beautify':
-                        Awesome.StorageMgr.set('JS_CSS_PAGE_BEAUTIFY',0);
+                        handleCloseBeautify();
                         break;
+                    // 二维码解码功能
                     case 'qr-decode':
-                        chrome.DynamicToolRunner({
-                            withContent: request.params.uri,
-                            tool: 'qr-code',
-                            query: `mode=decode`
-                        });
+                        handleQrDecode(request.params.uri);
                         break;
+                    // 请求页面内容数据
                     case 'request-page-content':
-                        request.params = FeJson[request.tabId];
-                        delete FeJson[request.tabId];
+                        handleRequestPageContent(request);
                         break;
+                    // 设置页面性能时序数据
                     case 'set-page-timing-data':
-                        chrome.DynamicToolRunner({
-                            tool: 'page-timing',
-                            withContent: request.wpoInfo
-                        });
+                        handleSetPageTimingData(request.wpoInfo);
                         break;
+                    // 颜色拾取器截图功能
                     case 'color-picker-capture':
                         _colorPickerCapture(request.params);
+                        // 记录工具使用
+                        Statistics.recordToolUsage('color-picker');
                         break;
+                    // 分页截图功能
                     case 'add-screen-shot-by-pages':
                         _addScreenShotByPages(request.params,callback);
+                        // 记录工具使用
+                        Statistics.recordToolUsage('screenshot');
                         return true;
+                    // 页面截图完成处理
                     case 'page-screenshot-done':
                         _showScreenShotResult(request.params);
                         break;
+                    // 启动页面脚本注入(油猴功能)
                     case 'request-monkey-start':
                         Monkey.start(request.params);
                         break;
+                    // 注入内容脚本CSS样式
                     case 'inject-content-css':
                         _injectContentCss(sender.tab.id,request.tool,!!request.devTool);
                         break;
+                    // 打开插件选项页面
+                    case 'open-options-page':
+                        chrome.runtime.openOptionsPage();
+                        break;
+                    // 打开打赏弹窗
+                    case 'open-donate-modal':
+                        chrome.gotoDonateModal(request.params.toolName);
+                        break;
+                    // 加载本地脚本文件
+                    case 'load-local-script':
+                        loadLocalScript(request.script, callback);
+                        return true; // 异步响应需要返回true
+                    // 工具使用统计埋点
+                    case 'statistics-tool-usage':
+                        // 埋点:自动触发json-format-auto
+                        Statistics.recordToolUsage(request.params.tool_name,request.params);
+                        break;
+                    // 获取热修复脚本
+                    case 'fetch-hotfix-json':
+                        fetchHotfixJson(callback);
+                        return true; // 异步响应必须返回true
+                    // 获取插件补丁数据
+                    case 'fetch-fehelper-patchs':
+                        fetchFehelperPatchs(callback);
+                        return true;
+                    // 获取指定工具的补丁
+                    case 'fh-get-tool-patch':
+                        getToolPatch(request.toolName, callback);
+                        return true;
                 }
                 callback && callback(request.params);
             } else {
@@ -425,9 +565,7 @@ let BgPageInstance = (function () {
 
         // 每开一个窗口,都向内容脚本注入一个js,绑定tabId
         chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) {
-
             if (String(changeInfo.status).toLowerCase() === "complete") {
-
                 if(/^(http(s)?|file):\/\//.test(tab.url) && blacklist.every(reg => !reg.test(tab.url))){
                     InjectTools.inject(tabId, { js: `window.__FH_TAB_ID__=${tabId};` });
                     _injectContentScripts(tabId);
@@ -440,9 +578,13 @@ let BgPageInstance = (function () {
             switch (reason) {
                 case 'install':
                     chrome.runtime.openOptionsPage();
+                    // 记录新安装用户
+                    Statistics.recordInstallation();
                     break;
                 case 'update':
                     _animateTips('+++1');
+                    // 记录更新安装
+                    Statistics.recordUpdate(previousVersion);
                     if (previousVersion === '2019.12.2415') {
                         notifyText({
                             message: '历尽千辛万苦,FeHelper已升级到最新版本,可以到插件设置页去安装旧版功能了!',
@@ -462,6 +604,7 @@ let BgPageInstance = (function () {
                     break;
             }
         });
+        
         // 卸载
         chrome.runtime.setUninstallURL(chrome.runtime.getManifest().homepage_url);
     };
@@ -472,11 +615,14 @@ let BgPageInstance = (function () {
      */
     let _checkUpdate = function () {
         setTimeout(() => {
-            chrome.runtime.requestUpdateCheck((status) => {
-                if (status === "update_available") {
-                    chrome.runtime.reload();
-                }
-            });
+            // 检查是否为 Firefox 浏览器,Firefox 不支持 requestUpdateCheck API
+            if (chrome.runtime.requestUpdateCheck && navigator.userAgent.indexOf("Firefox") === -1) {
+                chrome.runtime.requestUpdateCheck((status) => {
+                    if (status === "update_available") {
+                        chrome.runtime.reload();
+                    }
+                });
+            }
         }, 1000 * 30);
     };
 
@@ -484,19 +630,254 @@ let BgPageInstance = (function () {
      * 初始化
      */
     let _init = function () {
+        console.log(`[FeHelper] Background初始化开始 - ${new Date().toLocaleString()}`);
+        console.log(`[FeHelper] 扩展版本: ${chrome.runtime.getManifest().version}`);
+        console.log(`[FeHelper] Service Worker启动原因: ${chrome.runtime.getContexts ? 'Context API可用' : '传统模式'}`);
+        
         _checkUpdate();
         _addExtensionListener();
+        
+        // 初始化统计功能
+        Statistics.init();
+        
         Menu.rebuild();
+        
         // 定期清理冗余的垃圾
         setTimeout(() => {
             Awesome.gcLocalFiles();
         }, 1000 * 10);
+        
+        console.log(`[FeHelper] Background初始化完成 - ${new Date().toLocaleString()}`);
     };
 
+    /**
+     * 触发截图工具的执行
+     * @param {number} tabId - 标签页ID
+     */
+    function _triggerScreenshotTool(tabId) {
+        // 先尝试直接发送消息给content script
+        chrome.tabs.sendMessage(tabId, {
+            type: 'fh-screenshot-start'
+        }).then(() => {
+            // 成功触发
+        }).catch(() => {
+            // 如果发送消息失败,使用noPage模式
+            chrome.DynamicToolRunner({
+                tool: 'screenshot',
+                noPage: true
+            });
+        });
+    }
+
+    // 监听options页面传递的客户端信息
+    chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
+        if (request && request.type === 'clientInfo' && request.data) {
+            FH_CLIENT_INFO = request.data;
+        }
+    });
+
+    // 处理从popup触发的截图请求
+    function handleTriggerScreenshot(tabId) {
+        if (tabId) {
+            _triggerScreenshotTool(tabId);
+        } else {
+            chrome.DynamicToolRunner({
+                tool: 'screenshot',
+                noPage: true
+            });
+        }
+        // 记录工具使用
+        Statistics.recordToolUsage('screenshot');
+    }
+
+    // 请求JSON格式化选项配置
+    function requestJsonformatOptions(params, callback) {
+        Awesome.StorageMgr.get(params).then(result => {
+            Object.keys(result).forEach(key => {
+                if (['MAX_JSON_KEYS_NUMBER', 'JSON_FORMAT_THEME'].includes(key)) {
+                    result[key] = parseInt(result[key]);
+                } else {
+                    result[key] = (""+result[key] !== 'false');
+                }
+            });
+            callback && callback(result);
+        });
+    }
+
+    // 保存JSON格式化选项配置
+    function saveJsonformatOptions(params, callback) {
+        Awesome.StorageMgr.set(params).then(() => {
+            callback && callback();
+        });
+        // 记录工具使用
+        Statistics.recordToolUsage('save-jsonformat-options');
+    }
+
+    // 切换JSON格式化选项显示状态
+    function toggleJsonformatOptions(callback) {
+        Awesome.StorageMgr.get('JSON_TOOL_BAR_ALWAYS_SHOW').then(result => {
+            let show = result !== false;
+            Awesome.StorageMgr.set('JSON_TOOL_BAR_ALWAYS_SHOW',!show).then(() => {
+                callback && callback(!show);
+            });
+        });
+        // 记录工具使用
+        Statistics.recordToolUsage('json-format');
+    }
+
+    // 关闭代码美化功能
+    function handleCloseBeautify() {
+        Awesome.StorageMgr.set('JS_CSS_PAGE_BEAUTIFY',0);
+        // 记录工具使用
+        Statistics.recordToolUsage('code-beautify-close');
+    }
+
+    // 处理二维码解码
+    function handleQrDecode(uri) {
+        chrome.DynamicToolRunner({
+            withContent: uri,
+            tool: 'qr-code',
+            query: `mode=decode`
+        });
+        // 记录工具使用
+        Statistics.recordToolUsage('qr-code');
+    }
+
+    // 处理页面内容请求
+    function handleRequestPageContent(request) {
+        request.params = FeJson[request.tabId];
+        delete FeJson[request.tabId];
+    }
+
+    // 处理页面性能数据设置
+    function handleSetPageTimingData(wpoInfo) {
+        chrome.DynamicToolRunner({
+            tool: 'page-timing',
+            withContent: wpoInfo
+        });
+        // 记录工具使用
+        Statistics.recordToolUsage('page-timing');
+    }
+
+    // 获取指定工具的补丁(css/js)
+    function getToolPatch(toolName, callback) {
+        // 如果没有提供toolName,直接返回空补丁
+        if (!toolName) {
+            callback && callback({ css: '', js: '' });
+            return;
+        }
+
+        let version = String(chrome.runtime.getManifest().version).split('.').map(n => parseInt(n)).join('.');
+        const storageKey = `FH_PATCH_HOTFIX_${version}`;
+        chrome.storage.local.get(storageKey, result => {
+            const patchs = result[storageKey];
+            if (patchs && patchs[toolName]) {
+                const { css, js } = patchs[toolName];
+                callback && callback({ css, js });
+            } else {
+                callback && callback({ css: '', js: '' });
+            }
+        });
+    }
+
+    // 加载本地脚本,处理加载JSON格式化相关脚本的请求
+    function loadLocalScript(scriptUrl, callback) {
+        fetch(scriptUrl)
+            .then(response => response.text())
+            .then(scriptContent => {
+                callback && callback(scriptContent);
+            })
+            .catch(error => {
+                console.error('加载脚本失败:', error);
+                callback && callback(null);
+            });
+    }
+
+    // 获取热修复脚本,代理请求 hotfix.json,解决CORS问题
+    function fetchHotfixJson(callback) {
+        fetch('https://fehelper.com/static/js/hotfix.json?v=' + Date.now())
+            .then(response => response.text())
+            .then(scriptContent => {
+                callback && callback({ success: true, content: scriptContent });
+            })
+            .catch(error => {
+                callback && callback({ success: false, error: error.message });
+            });
+    }
+
+    // 检查并获取补丁(带频率控制)
+    function checkAndFetchPatchs() {
+        const PATCH_CHECK_INTERVAL = 5 * 60 * 1000; // 5min
+        const STORAGE_KEY = 'FH_LAST_PATCH_CHECK';
+        
+        chrome.storage.local.get(STORAGE_KEY, (result) => {
+            const lastCheck = result[STORAGE_KEY] || 0;
+            const now = Date.now();
+            
+            if (now - lastCheck > PATCH_CHECK_INTERVAL) {
+                console.log(`[FeHelper] 距离上次检查已超过5min,开始检查热更新...`);
+                
+                fetchFehelperPatchs((result) => {
+                    if (result && result.success) {
+                        console.log(`[FeHelper] 自动热更新成功,版本: v${result.version}`);
+                    } else if (result && result.notFound) {
+                        console.log(`[FeHelper] 当前版本暂无热更新补丁`);
+                    } else {
+                        console.log(`[FeHelper] 自动热更新检查失败:`, result?.error);
+                    }
+                    
+                    // 更新最后检查时间
+                    chrome.storage.local.set({ [STORAGE_KEY]: now });
+                });
+            } else {
+                const nextCheck = new Date(lastCheck + PATCH_CHECK_INTERVAL);
+                console.log(`[FeHelper] 距离上次检查不足5min,下次检查时间: ${nextCheck.toLocaleString()}`);
+            }
+        });
+    }
+
+    // 获取FeHelper热修复补丁
+    function fetchFehelperPatchs(callback) {
+        let version = String(chrome.runtime.getManifest().version).split('.').map(n => parseInt(n)).join('.');
+        let patchUrl = `https://fehelper.com/v1/fh-patchs/v${version}.json`;
+        
+        // 先检测文件是否存在(使用HEAD请求)
+        fetch(patchUrl, { method: 'HEAD' })
+            .then(response => {
+                if (response.ok) {
+                    // 文件存在,进行正常的fetch操作
+                    return fetch(`${patchUrl}?t=${Date.now()}`)
+                        .then(resp => {
+                            if (!resp.ok) {
+                                throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
+                            }
+                            return resp.json();
+                        })
+                        .then(data => {
+                            const patchs = data.patchs || data;
+                            const storageData = {};
+                            storageData[`FH_PATCH_HOTFIX_${version}`] = patchs;
+                            chrome.storage.local.set(storageData, () => {
+                                console.log(`[FeHelper] 成功获取版本 v${version} 的热修复补丁`);
+                                callback && callback({ success: true, version });
+                            });
+                        });
+                } else {
+                    // 文件不存在
+                    console.log(`[FeHelper] 服务器上不存在版本 v${version} 的补丁文件`);
+                    callback && callback({ success: false, error: '补丁文件不存在', notFound: true });
+                }
+            })
+            .catch(e => {
+                console.error(`[FeHelper] 获取补丁失败:`, e);
+                callback && callback({ success: false, error: e.message });
+            });
+    }
+
     return {
         pageCapture: _captureVisibleTab,
         init: _init
     };
 })();
 
-BgPageInstance.init();
+BgPageInstance.init();

+ 5 - 3
apps/background/monkey.js

@@ -1,5 +1,5 @@
-
 import InjectTools from './inject-tools.js';
+import Statistics from './statistics.js';
 
 export default (() => {
     let start = (params) => {
@@ -73,14 +73,16 @@ export default (() => {
 
             chrome.storage.local.get(PAGE_MONKEY_LOCAL_STORAGE_KEY, (resps) => {
                 let cacheMonkeys, storageMode = false;
-                if (!resps || !resps[PAGE_MONKEY_LOCAL_STORAGE_KEY]) {
+                if ((!resps || !resps[PAGE_MONKEY_LOCAL_STORAGE_KEY]) && typeof localStorage !== 'undefined') {
                     cacheMonkeys = localStorage.getItem(PAGE_MONKEY_LOCAL_STORAGE_KEY) || '[]';
                     storageMode = true;
                 } else {
                     cacheMonkeys = resps[PAGE_MONKEY_LOCAL_STORAGE_KEY] || '[]';
                 }
 
-                params && params.url && handler(JSON.parse(cacheMonkeys));
+                if(params && params.url){
+                    handler(JSON.parse(cacheMonkeys));
+                }
 
                 // 本地存储的内容,需要全部迁移到chrome.storage.local中,以确保unlimitedStorage
                 if (storageMode) {

+ 427 - 0
apps/background/statistics.js

@@ -0,0 +1,427 @@
+/**
+ * FeHelper数据统计模块
+ * @author fehelper
+ */
+
+import Awesome from './awesome.js';
+
+// 数据上报服务器地址
+let manifest = chrome.runtime.getManifest();
+let SERVER_TRACK_URL = '';
+if (manifest.name && manifest.name.endsWith('-Dev')) {
+    // SERVER_TRACK_URL = 'http://localhost:3001/api/track';
+    SERVER_TRACK_URL = 'https://chrome.fehelper.com/api/track';
+} else {
+    SERVER_TRACK_URL = 'https://chrome.fehelper.com/api/track';
+}
+
+// 用户ID存储键名
+const USER_ID_KEY = 'FH_USER_ID';
+// 上次使用日期存储键名
+const LAST_ACTIVE_DATE_KEY = 'FH_LAST_ACTIVE_DATE';
+// 用户日常使用数据存储键名
+const USER_USAGE_DATA_KEY = 'FH_USER_USAGE_DATA';
+
+// 记录background启动时间
+const FH_TIME_OPENED = Date.now();
+
+let Statistics = (function() {
+    
+    // 用户唯一标识
+    let userId = '';
+    
+    // 今天的日期字符串 YYYY-MM-DD
+    let todayStr = new Date().toISOString().split('T')[0];
+    
+    // 本地存储的使用数据
+    let usageData = {
+        dailyUsage: {}, // 按日期存储的使用记录
+        tools: {}       // 各工具的使用次数
+    };
+    
+    /**
+     * 生成唯一的用户ID
+     * @returns {string} 用户ID
+     */
+    const generateUserId = () => {
+        return 'fh_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
+    };
+    
+    /**
+     * 获取或创建用户ID
+     * @returns {Promise<string>} 用户ID
+     */
+    const getUserId = async () => {
+        if (userId) return userId;
+        
+        try {
+            const result = await Awesome.StorageMgr.get(USER_ID_KEY);
+            if (result) {
+                userId = result;
+            } else {
+                userId = generateUserId();
+                await Awesome.StorageMgr.set(USER_ID_KEY, userId);
+            }
+            return userId;
+        } catch (error) {
+            console.error('获取用户ID失败:', error);
+            return generateUserId(); // 失败时生成临时ID
+        }
+    };
+    
+    /**
+     * 加载本地存储的使用数据
+     * @returns {Promise<void>}
+     */
+    const loadUsageData = async () => {
+        try {
+            const data = await Awesome.StorageMgr.get(USER_USAGE_DATA_KEY);
+            if (data) {
+                usageData = JSON.parse(data);
+            }
+        } catch (error) {
+            console.error('加载使用数据失败:', error);
+        }
+    };
+    
+    /**
+     * 保存使用数据到本地存储
+     * @returns {Promise<void>}
+     */
+    const saveUsageData = async () => {
+        try {
+            await Awesome.StorageMgr.set(USER_USAGE_DATA_KEY, JSON.stringify(usageData));
+        } catch (error) {
+            console.error('保存使用数据失败:', error);
+        }
+    };
+    
+    /**
+     * 获取客户端详细信息(仅background可用字段,兼容service worker环境,字段与服务端一致)
+     * @returns {Object}
+     */
+    const getClientInfo = async () => {
+        const nav = self.navigator || {};
+        // 只采集服务端需要的字段
+        return {
+            userAgent: nav.userAgent || '',
+            language: nav.language || '',
+            platform: nav.platform || '',
+            extensionVersion: chrome.runtime.getManifest().version
+        };
+    };
+
+    /**
+     * 判断是否允许统计
+     * @returns {Promise<boolean>} true=允许,false=禁止
+     */
+    const isStatisticsAllowed = async () => {
+        try {
+            const forbid = await Awesome.StorageMgr.get('FORBID_STATISTICS');
+            return !(forbid === true || forbid === 'true');
+        } catch (e) {
+            return true;
+        }
+    };
+
+    /**
+     * 使用自建服务器发送事件数据
+     * @param {string} eventName - 事件名称
+     * @param {Object} params - 事件参数
+     */
+    const sendToServer = async (eventName, params = {}) => {
+        return ''; // 暂时关闭统计
+        if (!(await isStatisticsAllowed())) return;
+        const uid = await getUserId();
+        const clientInfo = await getClientInfo();
+        // 只保留服务端 TrackSchema 需要的字段
+        const payload = {
+            event: eventName,
+            userId: uid,
+            ...clientInfo
+        };
+        // 只允许 TrackSchema 里的字段
+        const allowedFields = [
+            'tool_name', 'extensionVersion', 'browser', 'browserVersion', 'os', 'osVersion', 'IP',  'language', 'platform'
+        ];
+        for (const key of allowedFields) {
+            if (params[key] !== undefined) {
+                payload[key] = params[key];
+            }
+        }
+        try {
+            fetch(SERVER_TRACK_URL, {
+                method: 'POST',
+                body: JSON.stringify(payload),
+                headers: {
+                    'Content-Type': 'application/json'
+                },
+                keepalive: true
+            }).catch(e => console.log('自建统计服务器发送失败:', e));
+        } catch (error) {
+            console.log('自建统计发送失败:', error);
+        }
+    };
+    
+    /**
+     * 记录每日活跃用户
+     * @returns {Promise<void>}
+     */
+    const recordDailyActiveUser = async () => {
+        try {
+            // 获取上次活跃日期
+            const lastActiveDate = await Awesome.StorageMgr.get(LAST_ACTIVE_DATE_KEY);
+            
+            // 如果今天还没有记录,则记录今天的活跃
+            if (lastActiveDate !== todayStr) {
+                await Awesome.StorageMgr.set(LAST_ACTIVE_DATE_KEY, todayStr);
+                
+                // 确保该日期的记录存在
+                if (!usageData.dailyUsage[todayStr]) {
+                    usageData.dailyUsage[todayStr] = {
+                        date: todayStr,
+                        tools: {}
+                    };
+                }
+                
+                // 发送每日活跃记录到自建服务器
+                sendToServer('daily_active_user', {
+                    date: todayStr
+                });
+            }
+        } catch (error) {
+            console.error('记录日活跃用户失败:', error);
+        }
+    };
+    
+    /**
+     * 记录插件安装事件
+     */
+    const recordInstallation = async () => {
+        sendToServer('extension_installed');
+    };
+    
+    /**
+     * 记录插件更新事件
+     * @param {string} previousVersion - 更新前的版本
+     */
+    const recordUpdate = async (previousVersion) => {
+        sendToServer('extension_updated', {
+            previous_version: previousVersion
+        });
+    };
+    
+    /**
+     * 记录插件卸载事件
+     */
+    const recordUninstall = async () => {
+        sendToServer('extension_uninstall');
+    };
+    
+    /**
+     * 记录工具使用情况
+     * @param {string} toolName - 工具名称
+     */
+    const recordToolUsage = async (toolName, params = {}) => {
+        // 确保今天的记录存在
+        if (!usageData.dailyUsage[todayStr]) {
+            usageData.dailyUsage[todayStr] = {
+                date: todayStr,
+                tools: {}
+            };
+        }
+        
+        // 增加工具使用计数
+        if (!usageData.tools[toolName]) {
+            usageData.tools[toolName] = 0;
+        }
+        usageData.tools[toolName]++;
+        
+        // 增加今天该工具的使用计数
+        if (!usageData.dailyUsage[todayStr].tools[toolName]) {
+            usageData.dailyUsage[todayStr].tools[toolName] = 0;
+        }
+        usageData.dailyUsage[todayStr].tools[toolName]++;
+        
+        // 保存使用数据
+        await saveUsageData();
+        
+        // 发送工具使用记录到自建服务器
+        sendToServer('tool_used', {
+            tool_name: toolName,
+            date: todayStr,
+            ...params
+        });
+    };
+    
+    /**
+     * 定期发送使用摘要数据
+     */
+    const scheduleSyncStats = () => {
+        // 每周发送一次摘要数据
+        const ONE_WEEK = 7 * 24 * 60 * 60 * 1000;
+        setInterval(async () => {
+            // 发送工具使用排名
+            const toolRanking = Object.entries(usageData.tools)
+                .sort((a, b) => b[1] - a[1])
+                .slice(0, 5)
+                .map(([name, count]) => ({name, count}));
+            
+            sendToServer('usage_summary', {
+                top_tools: JSON.stringify(toolRanking)
+            });
+            
+            // 清理过旧的日期数据(保留30天数据)
+            const now = new Date();
+            const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
+            const thirtyDaysAgoStr = thirtyDaysAgo.toISOString().split('T')[0];
+            
+            Object.keys(usageData.dailyUsage).forEach(date => {
+                if (date < thirtyDaysAgoStr) {
+                    delete usageData.dailyUsage[date];
+                }
+            });
+            
+            // 保存清理后的数据
+            await saveUsageData();
+        }, ONE_WEEK);
+    };
+    
+    /**
+     * 初始化统计模块
+     */
+    const init = async () => {
+        await getUserId();
+        await loadUsageData();
+        await recordDailyActiveUser();
+        scheduleSyncStats();
+    };
+    
+    /**
+     * 获取最近使用的工具(按最近使用时间倒序,默认最近10个)
+     * @param {number} limit - 返回的最大数量
+     * @returns {Promise<string[]>} 工具名称数组
+     */
+    const getRecentUsedTools = async (limit = 10) => {
+        // 确保数据已加载
+        await loadUsageData();
+        // 收集所有日期,按新到旧排序
+        const dates = Object.keys(usageData.dailyUsage).sort((a, b) => b.localeCompare(a));
+        const toolSet = [];
+        for (const date of dates) {
+            const tools = Object.keys(usageData.dailyUsage[date].tools || {});
+            for (const tool of tools) {
+                if (!toolSet.includes(tool)) {
+                    toolSet.push(tool);
+                    if (toolSet.length >= limit) {
+                        return toolSet;
+                    }
+                }
+            }
+        }
+        return toolSet;
+    };
+    
+    /**
+     * 获取DashBoard统计数据
+     * @returns {Promise<Object>} 统计数据对象
+     */
+    const getDashboardData = async () => {
+        await loadUsageData();
+        // 最近10次使用的工具及时间
+        const recent = [];
+        const recentDetail = [];
+        const dates = Object.keys(usageData.dailyUsage).sort((a, b) => b.localeCompare(a));
+        for (const date of dates) {
+            for (const tool of Object.keys(usageData.dailyUsage[date].tools || {})) {
+                if (!recent.includes(tool)) {
+                    recent.push(tool);
+                    recentDetail.push({ tool, date });
+                    if (recent.length >= 10) break;
+                }
+            }
+            if (recent.length >= 10) break;
+        }
+        // 工具使用总次数排行
+        const mostUsed = Object.entries(usageData.tools)
+            .sort((a, b) => b[1] - a[1])
+            .slice(0, 10)
+            .map(([name, count]) => ({ name, count }));
+        const totalCount = Object.values(usageData.tools).reduce((a, b) => a + b, 0);
+        const activeDays = Object.keys(usageData.dailyUsage).length;
+        const allDates = Object.keys(usageData.dailyUsage).sort();
+        // 最近10天每日使用情况
+        const dailyTrend = allDates.slice(-10).map(date => ({
+            date,
+            count: Object.values(usageData.dailyUsage[date].tools || {}).reduce((a, b) => a + b, 0)
+        }));
+        // 首次和最近活跃日期
+        const firstDate = allDates[0] || '';
+        const lastDate = allDates[allDates.length - 1] || '';
+        // 连续活跃天数
+        let maxStreak = 0, curStreak = 0, prev = '';
+        for (let i = 0; i < allDates.length; i++) {
+            if (i === 0 || (new Date(allDates[i]) - new Date(prev) === 86400000)) {
+                curStreak++;
+            } else {
+                maxStreak = Math.max(maxStreak, curStreak);
+                curStreak = 1;
+            }
+            prev = allDates[i];
+        }
+        maxStreak = Math.max(maxStreak, curStreak);
+        // 本月/本周统计
+        const now = new Date();
+        const thisMonth = now.toISOString().slice(0, 7);
+        const thisWeekMonday = new Date(now.setDate(now.getDate() - now.getDay() + 1)).toISOString().slice(0, 10);
+        let monthCount = 0, weekCount = 0;
+        allDates.forEach(date => {
+            const cnt = Object.values(usageData.dailyUsage[date].tools || {}).reduce((a, b) => a + b, 0);
+            if (date.startsWith(thisMonth)) monthCount += cnt;
+            if (date >= thisWeekMonday) weekCount += cnt;
+        });
+        // 平均每日使用次数
+        const avgPerDay = activeDays ? Math.round(totalCount / activeDays * 10) / 10 : 0;
+        // 最活跃的一天
+        let maxDay = { date: '', count: 0 };
+        allDates.forEach(date => {
+            const cnt = Object.values(usageData.dailyUsage[date].tools || {}).reduce((a, b) => a + b, 0);
+            if (cnt > maxDay.count) maxDay = { date, count: cnt };
+        });
+        // 最近未使用天数
+        let daysSinceLast = 0;
+        if (lastDate) {
+            const diff = Math.floor((new Date() - new Date(lastDate)) / 86400000);
+            daysSinceLast = diff > 0 ? diff : 0;
+        }
+        return {
+            recent,
+            recentDetail,
+            mostUsed,
+            totalCount,
+            activeDays,
+            dailyTrend,
+            firstDate,
+            lastDate,
+            maxStreak,
+            monthCount,
+            weekCount,
+            avgPerDay,
+            maxDay,
+            daysSinceLast,
+            allDates
+        };
+    };
+    
+    return {
+        init,
+        recordInstallation,
+        recordUpdate,
+        recordToolUsage,
+        getRecentUsedTools,
+        getDashboardData,
+        recordUninstall
+    };
+})();
+
+export default Statistics; 

+ 55 - 8
apps/background/tools.js

@@ -71,7 +71,7 @@ let toolMap = {
         }]
     },
     'aiagent': {
-        name: 'AI,请帮帮忙',
+        name: 'AI(智能助手)',
         tips: '由AI强力支撑的超智能对话工具,可以让它帮你写代码、改代码、做方案设计、查资料、做分析等',
         menuConfig: [{
             icon: '֍',
@@ -127,11 +127,11 @@ let toolMap = {
         }]
     },
     'regexp': {
-        name: 'JS正则表达式',
-        tips: '正则校验工具,默认提供一些工作中常用的正则表达式,支持内容实时匹配并高亮显示结果',
+        name: '正则公式速查',
+        tips: '支持 JavaScript / Python / PHP / Java 等语言的正则速查,包含验证类、提取类、替换类、格式化类、特殊字符类、编程相关等常用正则表达式',
         menuConfig: [{
             icon: '✙',
-            text: 'JS正则表达式'
+            text: '正则公式速查'
         }]
     },
     'trans-radix': {
@@ -193,6 +193,14 @@ let toolMap = {
             text: '网页截屏工具'
         }]
     },
+    'mock-data': {
+        name: '数据Mock工具',
+        tips: '快速生成各种测试数据,支持个人信息、商业数据、技术数据等多种类型,可自定义字段和输出格式',
+        menuConfig: [{
+            icon: '⟡',
+            text: '数据Mock工具'
+        }]
+    },
     'color-picker': {
         name: '页面取色工具',
         tips: '可直接在网页上针对任意元素进行色值采集,将光标移动到需要取色的位置,单击确定即可',
@@ -223,13 +231,13 @@ let toolMap = {
         }]
     },
     'page-timing': {
-        name: '网页性能检测',
-        tips: '检测网页加载性能,包括握手、响应、渲染等各阶段耗时,同时提供Response Headers以便分析',
+        name: '网站性能优化',
+        tips: '全面分析网页性能指标,包括核心Web指标(LCP/FID/CLS)、资源加载性能、内存使用、长任务监控等,并提供针对性的优化建议',
         contentScriptJs: true,
         noPage: true,
         menuConfig: [{
             icon: 'Σ',
-            text: '网页性能检测'
+            text: '网站性能优化'
         }]
     },
     'excel2json': {
@@ -248,6 +256,45 @@ let toolMap = {
             text: '图表制作工具'
         }]
     },
+    'svg-converter': {
+        name: 'SVG转为图片',
+        tips: '支持SVG文件转换为PNG、JPG、WEBP等格式,可自定义输出尺寸,支持文件拖放和URL导入',
+        menuConfig: [{
+            icon: '⇲',
+            text: 'SVG转图片工具'
+        }]
+    },
+    'poster-maker': {
+        name: '海报快速生成',
+        tips: '快速创建营销推广海报,支持朋友圈、小红书等多种模板,可自定义文字、图片和配色',
+        menuConfig: [{
+            icon: '🖼️',
+            text: '海报快速生成'
+        }]
+    },
+    'datetime-calc': {
+        name: '时间戳计算器',
+        tips: '支持多种时间格式解析、批量转换、时区转换、数据库格式生成等高级时间处理功能',
+        menuConfig: [{
+            icon: '⏱️',
+            text: '时间戳计算器',
+            contexts: ['page', 'selection', 'editable']
+        }]
+    }
 };
 
-export default toolMap;
+// 判断是否为Firefox浏览器,如果是则移除特定工具
+if (navigator.userAgent.indexOf('Firefox') !== -1) {
+    delete toolMap['color-picker'];
+    delete toolMap['postman'];
+    delete toolMap['devtools'];
+    delete toolMap['websocket'];
+    delete toolMap['page-timing'];
+    delete toolMap['grid-ruler'];
+    delete toolMap['naotu'];
+    delete toolMap['screenshot'];
+    delete toolMap['page-monkey'];
+    delete toolMap['excel2json'];
+}
+
+export default toolMap;

+ 4 - 1
apps/chart-maker/index.html

@@ -18,15 +18,18 @@
     <script src="lib/chartjs-plugin-zoom.min.js"></script>
     <!-- 引入Chart.js数据标签插件 -->
     <script src="lib/chartjs-plugin-datalabels.min.js"></script>
+    <script type="text/javascript" src="../static/vendor/evalCore.min.js"></script>
 </head>
 <body>
     <div class="wrapper" id="pageContainer">
         <div class="page-header">
-            <a href="https://www.baidufe.com/fehelper/index/index.html" target="_blank" class="header-link">
+            <a href="https://fehelper.com" target="_blank" class="header-link">
                 <img src="../static/img/fe-16.png" alt="fehelper"/>
                 <span class="fehelper-text">FeHelper</span>
             </a>
             <span class="page-title-suffix">:图表生成器</span>
+            <a href="#" class="x-donate-link"><i class="nav-icon">❤&nbsp;</i>打赏鼓励</a>
+            <a class="x-other-tools"><i class="icon-plus-circle"></i> 探索更多实用工具 <span class="tool-market-badge">工具市场</span></a>
         </div>
     </div>
 

+ 48 - 21
apps/chart-maker/main.js

@@ -8,10 +8,12 @@ document.addEventListener('DOMContentLoaded', function() {
     const exportPngBtn = document.getElementById('export-png-btn');
     const exportJpgBtn = document.getElementById('export-jpg-btn');
     const copyImgBtn = document.getElementById('copy-img-btn');
-    const chartTypeSelect = document.getElementById('chart-type');
     const manualFormatSelect = document.getElementById('manual-format');
     const fileUploadInput = document.getElementById('file-upload');
     const manualFormatContainer = document.getElementById('manual-format-container');
+    const donateLink = document.querySelector('.x-donate-link');
+    const otherToolsLink = document.querySelector('.x-other-tools');
+
     const manualInputContainers = [simpleDataContainer, seriesDataContainer, csvDataContainer];
     
     // 初始化显示状态
@@ -22,6 +24,9 @@ document.addEventListener('DOMContentLoaded', function() {
     // 初始化图表类型画廊
     initChartTypeGallery();
 
+    // 页面加载时自动获取并注入页面的补丁
+    loadPatchHotfix();
+
     function toggleManualInputs(show) {
         manualFormatContainer.style.display = show ? 'block' : 'none';
         const selectedFormat = manualFormatSelect.value;
@@ -55,6 +60,22 @@ document.addEventListener('DOMContentLoaded', function() {
         });
     });
 
+    // 监听打赏链接点击事件
+    donateLink.addEventListener('click', function(event) {
+        event.preventDefault();
+        chrome.runtime.sendMessage({
+            type: 'fh-dynamic-any-thing',
+            thing: 'open-donate-modal', 
+            params: { toolName: 'chart-maker' }
+        });
+    });
+
+    // 监听探索更多工具链接点击事件
+    otherToolsLink.addEventListener('click', function(event) {
+        event.preventDefault(); 
+        chrome.runtime.openOptionsPage();
+    });
+
     // 监听手动格式选择变化
     manualFormatSelect.addEventListener('change', function() {
         const format = this.value;
@@ -105,25 +126,19 @@ document.addEventListener('DOMContentLoaded', function() {
     // 生成图表按钮点击事件 (修改为独立的函数)
     function generateChart() {
         try {
-            console.log('开始生成图表...');
             let parsedData;
             const method = document.querySelector('input[name="data-input-method"]:checked').value;
-            console.log('数据输入方式:', method);
 
             if (method === 'upload-csv' && uploadedData) {
                 parsedData = uploadedData;
-                console.log('使用上传的数据');
             } else if (method === 'manual') {
                 parsedData = parseInputData(); // 使用现有的手动数据解析函数
-                console.log('使用手动输入的数据');
             } else if (method === 'upload-csv' && !uploadedData) {
                 throw new Error('请先上传文件');
             } else {
                 throw new Error('请选择有效的数据输入方式并提供数据');
             }
             
-            console.log('解析后的数据:', parsedData);
-            
             if (!parsedData || 
                 (parsedData.labels && parsedData.labels.length === 0) || 
                 (parsedData.datasets && parsedData.datasets.length === 0)) {
@@ -140,21 +155,17 @@ document.addEventListener('DOMContentLoaded', function() {
                 chartSettings.isSimpleData = true;
             }
             
-            console.log('图表设置:', chartSettings);
-            
             // 调用chart-generator.js中的createChart函数
             if (typeof createChart !== 'function') {
                 throw new Error('createChart函数未定义,请确保chart-generator.js正确加载');
             }
             
             createChart(parsedData, chartSettings);
-            console.log('图表生成成功');
             
             exportPngBtn.disabled = false;
             exportJpgBtn.disabled = false;
             copyImgBtn.disabled = false;
         } catch (error) {
-            console.error('生成图表时出错:', error);
             showNotification(error.message, true);
         }
     }
@@ -174,19 +185,41 @@ document.addEventListener('DOMContentLoaded', function() {
         });
     });
     
+    function loadPatchHotfix() {
+        // 页面加载时自动获取并注入页面的补丁
+        chrome.runtime.sendMessage({
+            type: 'fh-dynamic-any-thing',
+            thing: 'fh-get-tool-patch',
+            toolName: 'chart-maker'
+        }, patch => {
+            if (patch) {
+                if (patch.css) {
+                    const style = document.createElement('style');
+                    style.textContent = patch.css;
+                    document.head.appendChild(style);
+                }
+                if (patch.js) {
+                    try {
+                        if (window.evalCore && window.evalCore.getEvalInstance) {
+                            window.evalCore.getEvalInstance(window)(patch.js);
+                        }
+                    } catch (e) {
+                        console.error('chart-maker补丁JS执行失败', e);
+                    }
+                }
+            }
+        });
+    }
+
     // 初始化图表类型画廊
     function initChartTypeGallery() {
-        console.log('初始化图表类型预览画廊...');
-        
         // 获取所有图表类型预览项
         const chartTypeItems = document.querySelectorAll('.chart-type-item');
-        console.log(`找到${chartTypeItems.length}个图表类型预览项`);
         
         // 为每个预览项添加点击事件
         chartTypeItems.forEach(item => {
             item.addEventListener('click', function() {
                 const chartType = this.getAttribute('data-chart-type');
-                console.log('选择了图表类型:', chartType);
                 
                 // 更新活动状态
                 chartTypeItems.forEach(item => item.classList.remove('active'));
@@ -596,7 +629,6 @@ document.addEventListener('DOMContentLoaded', function() {
                 loadingOverlay.remove();
                 
                 showNotification('导出图像失败,请重试', true);
-                console.error('导出图像出错:', error);
             });
         }, 100);
     }
@@ -629,14 +661,12 @@ document.addEventListener('DOMContentLoaded', function() {
                                     showNotification('图表已复制到剪贴板');
                                 })
                                 .catch(err => {
-                                    console.error('剪贴板API错误:', err);
                                     legacyCopyToClipboard(canvas);
                                 });
                         } else {
                             legacyCopyToClipboard(canvas);
                         }
                     } catch (e) {
-                        console.error('复制到剪贴板出错:', e);
                         legacyCopyToClipboard(canvas);
                     }
                 });
@@ -645,7 +675,6 @@ document.addEventListener('DOMContentLoaded', function() {
                 loadingOverlay.remove();
                 
                 showNotification('复制图像失败,请重试', true);
-                console.error('复制图像出错:', error);
             });
         }, 100);
     }
@@ -706,7 +735,5 @@ document.addEventListener('DOMContentLoaded', function() {
     // 根据数据格式更新图表类型选项
     function updateChartTypeOptions(dataFormat) {
         // 由于移除了图表类型下拉框,这个函数现在仅记录当前数据格式,不再修改任何选项
-        console.log('当前数据格式:', dataFormat);
-        // 未来可以根据数据格式来调整图表类型画廊的可见性或提示,但现在不需要操作
     }
 }); 

+ 90 - 6
apps/chart-maker/style.css

@@ -14,8 +14,7 @@ body {
     background-image: linear-gradient(to right, rgba(245, 247, 250, 0.8) 1px, transparent 1px),
                       linear-gradient(to bottom, rgba(245, 247, 250, 0.8) 1px, transparent 1px);
     background-size: 20px 20px;
-    padding-top: 15px;
-    max-width: 1200px;
+    
     margin: 0 auto;
     font-size:100%;
 }
@@ -613,6 +612,7 @@ canvas#chart-canvas[data-chart-rendered] {
     display: flex;
     align-items: center;
     margin-bottom: 20px;
+    position: relative;
 }
 
 .page-header .header-link {
@@ -621,7 +621,7 @@ canvas#chart-canvas[data-chart-rendered] {
     text-decoration: none;
     color: #5a5c69;
     font-weight: 500;
-    font-size: 1.2rem;
+    font-size: 14px;
     transition: color 0.2s ease-in-out;
 }
 
@@ -641,6 +641,90 @@ canvas#chart-canvas[data-chart-rendered] {
 
 .page-header .page-title-suffix {
     margin-left: 8px;
-    font-size: 1.2rem;
-    color: #858796;
-} 
+    font-size: 14px;
+    font-weight: 500;
+} 
+/* Style for the "Explore More Tools" button */
+.page-header>a.x-other-tools {
+    margin: 0; /* Reset margin */
+    font-size: 12px; /* Slightly smaller */
+    cursor: pointer;
+    text-decoration: none;
+    -webkit-user-select: none;
+    user-select: none;
+    color: #495057; /* Grey text */
+    background-color: #f8f9fa; /* Light grey background */
+    padding: 6px 12px; /* Adjusted padding */
+    border-radius: 6px; /* Standard rounded corners */
+    border: 1px solid #dee2e6; /* Match border color */
+    transition: all 0.2s ease;
+    display: inline-flex;
+    align-items: center;
+    white-space: nowrap;
+    position: absolute;
+    right: 10px;
+}
+
+.page-header>a.x-other-tools .icon-plus-circle {
+    display: inline-block;
+    width: 14px; /* Slightly smaller icon */
+    height: 14px;
+    background: url(../static/img/plus-circle.svg) no-repeat center center;
+    background-size: contain;
+    margin-right: 4px;
+}
+
+.page-header>a.x-other-tools .tool-market-badge {
+    display: inline-block;
+    background-color: #007bff; /* Match theme blue */
+    color: white;
+    padding: 2px 7px;
+    border-radius: 4px; /* Slightly less round */
+    margin-left: 6px;
+    font-size: 11px; /* Smaller badge text */
+    font-weight: 500;
+}
+
+.page-header>a.x-other-tools:hover {
+    color: #212529;
+    background-color: #e9ecef;
+    border-color: #ced4da;
+    box-shadow: 0 1px 2px rgba(0,0,0,0.05);
+    transform: none; /* Remove translateY */
+}
+/* 保持原有的顶部导航样式 */
+.x-donate-link {
+    font-size: 14px;
+    font-weight: 500;
+    color: #2563eb;
+    cursor: pointer;
+    text-decoration: none;
+    border: none;
+    margin-left: 70px;
+    white-space: nowrap;
+    margin-right: auto;
+    padding: 5px 16px;
+    border-radius: 20px;
+    background-color: #eff6ff;
+    transition: all 0.2s ease;
+    position: relative;
+    display: inline-flex;
+    align-items: center;
+    box-shadow: 0 1px 2px rgba(37, 99, 235, 0.1);
+}
+
+.x-donate-link:hover {
+    background-color: #dbeafe;
+    color: #1d4ed8;
+    text-decoration: none;
+    box-shadow: 0 2px 4px rgba(37, 99, 235, 0.15);
+    transform: translateY(-1px);
+}
+
+.x-donate-link {
+    margin: 0 10px;
+    font-size: 12px;
+    font-weight: normal;
+    position: absolute;
+    right: 220px;
+}

+ 98 - 0
apps/chrome.json

@@ -0,0 +1,98 @@
+{
+  "name": "FeHelper(前端助手)-Dev",
+  "short_name": "FeHelper",
+  "version": "2025.6.2901",
+  "manifest_version": 3,
+  "description": "JSON自动格式化、手动格式化,支持排序、解码、下载等,更多功能可在配置页按需安装!",
+  "icons": {
+    "16": "static/img/fe-16.png",
+    "48": "static/img/fe-48.png",
+    "128": "static/img/fe-128.png"
+  },
+  "action": {
+    "default_icon": "static/img/fe-16.png",
+    "default_title": "FeHelper(前端助手)",
+    "default_popup": "popup/index.html"
+  },
+  "background": {
+    "service_worker": "background/background.js",
+    "type": "module"
+  },
+  "options_ui": {
+    "page": "options/index.html",
+    "open_in_tab": true
+  },
+  "permissions": [
+    "tabs",
+    "scripting",
+    "contextMenus",
+    "activeTab",
+    "storage",
+    "notifications",
+    "unlimitedStorage"
+  ],
+  "host_permissions": [
+    "http://*/*",
+    "https://*/*",
+    "file://*/*"
+    ],
+  "optional_permissions": [
+    "downloads"
+  ],
+  "commands": {
+    "_execute_action": {
+      "suggested_key": {
+        "default": "Alt+Shift+J"
+      }
+    }
+  },
+  "web_accessible_resources": [
+      {
+          "resources":[
+            "static/img/fe-16.png",
+            "static/img/fe-48.png",
+            "static/img/loading.gif",
+            "json-format/format-lib.js",
+            "json-format/json-abc.js",
+            "json-format/json-bigint.js",
+            "json-format/json-decode.js",
+            "json-format/json-worker.js",
+            "static/vendor/jquery/jquery-3.3.1.min.js",
+            "static/vendor/evalCore.min.js",
+
+            "background/awesome.js",
+            "background/tools.js",
+
+            "code-beautify/beautify.js",
+            "code-beautify/beautify-css.js"
+        ],
+        "matches": ["<all_urls>"]
+      }
+  ],
+  "content_scripts": [
+    {
+      "matches": [
+        "http://*/*",
+        "https://*/*",
+        "file://*/*"
+      ],
+      "exclude_globs": [
+        "https://chrome.google.com/*"
+      ],
+      "js": [
+        "static/vendor/jquery/jquery-3.3.1.min.js",
+        "static/vendor/evalCore.min.js"
+      ],
+      "run_at": "document_start",
+      "all_frames": true
+    }
+  ],
+  "content_security_policy": {
+    "extension_pages": "script-src 'self'; style-src 'self' 'unsafe-inline'; object-src 'self'"
+  },
+  "update_url": "https://clients2.google.com/service/update2/crx",
+  "homepage_url": "https://www.fehelper.com"
+}
+
+
+

+ 2 - 199
apps/code-beautify/beautify-css.js

@@ -58,8 +58,8 @@
     function css_beautify(source_text, options, callback) {
         "use strict";
 
-        // 用webwork的方式来进行格式化,效率更高
-        let worker = new Worker(URL.createObjectURL(new Blob(["(" + beautifyWebWorker.toString() + ")()"], {type: 'text/javascript'})));
+        // 用独立worker文件的方式来进行格式化,兼容Firefox等浏览器
+        let worker = new Worker('./beautify-worker.js');
         worker.onmessage = function (evt) {
             callback && callback(evt.data);
         };
@@ -69,202 +69,5 @@
         });
     }
 
-    function beautifyWebWorker() {
-        function Beautifier(source_text, options) {
-            options = options || {};
-            var indentSize = options.indent_size || 4;
-            var indentCharacter = options.indent_char || ' ';
-
-            // compatibility
-            if (typeof indentSize === "string") {
-                indentSize = parseInt(indentSize, 10);
-            }
-
-
-            // tokenizer
-            var whiteRe = /^\s+$/;
-            var wordRe = /[\w$\-_]/;
-
-            var pos = -1, ch;
-
-            function next() {
-                ch = source_text.charAt(++pos);
-                return ch;
-            }
-
-            function peek() {
-                return source_text.charAt(pos + 1);
-            }
-
-            function eatString(comma) {
-                var start = pos;
-                while (next()) {
-                    if (ch === "\\") {
-                        next();
-                        next();
-                    } else if (ch === comma) {
-                        break;
-                    } else if (ch === "\n") {
-                        break;
-                    }
-                }
-                return source_text.substring(start, pos + 1);
-            }
-
-            function eatWhitespace() {
-                var start = pos;
-                while (whiteRe.test(peek())) {
-                    pos++;
-                }
-                return pos !== start;
-            }
-
-            function skipWhitespace() {
-                var start = pos;
-                do {
-                } while (whiteRe.test(next()));
-                return pos !== start + 1;
-            }
-
-            function eatComment() {
-                var start = pos;
-                next();
-                while (next()) {
-                    if (ch === "*" && peek() === "/") {
-                        pos++;
-                        break;
-                    }
-                }
-
-                return source_text.substring(start, pos + 1);
-            }
-
-
-            function lookBack(str) {
-                return source_text.substring(pos - str.length, pos).toLowerCase() === str;
-            }
-
-            // printer
-            var indentString = source_text.match(/^[\r\n]*[\t ]*/)[0];
-            var singleIndent = Array(indentSize + 1).join(indentCharacter);
-            var indentLevel = 0;
-
-            function indent() {
-                indentLevel++;
-                indentString += singleIndent;
-            }
-
-            function outdent() {
-                indentLevel--;
-                indentString = indentString.slice(0, -indentSize);
-            }
-
-            var print = {};
-            print["{"] = function (ch) {
-                print.singleSpace();
-                output.push(ch);
-                print.newLine();
-            };
-            print["}"] = function (ch) {
-                print.newLine();
-                output.push(ch);
-                print.newLine();
-            };
-
-            print.newLine = function (keepWhitespace) {
-                if (!keepWhitespace) {
-                    while (whiteRe.test(output[output.length - 1])) {
-                        output.pop();
-                    }
-                }
-
-                if (output.length) {
-                    output.push('\n');
-                }
-                if (indentString) {
-                    output.push(indentString);
-                }
-            };
-            print.singleSpace = function () {
-                if (output.length && !whiteRe.test(output[output.length - 1])) {
-                    output.push(' ');
-                }
-            };
-            var output = [];
-            if (indentString) {
-                output.push(indentString);
-            }
-            /*_____________________--------------------_____________________*/
-
-            while (true) {
-                var isAfterSpace = skipWhitespace();
-
-                if (!ch) {
-                    break;
-                }
-
-
-                if (ch === '{') {
-                    indent();
-                    print["{"](ch);
-                } else if (ch === '}') {
-                    outdent();
-                    print["}"](ch);
-                } else if (ch === '"' || ch === '\'') {
-                    output.push(eatString(ch));
-                } else if (ch === ';') {
-                    output.push(ch, '\n', indentString);
-                } else if (ch === '/' && peek() === '*') { // comment
-                    print.newLine();
-                    output.push(eatComment(), "\n", indentString);
-                } else if (ch === '(') { // may be a url
-                    if (lookBack("url")) {
-                        output.push(ch);
-                        eatWhitespace();
-                        if (next()) {
-                            if (ch !== ')' && ch !== '"' && ch !== '\'') {
-                                output.push(eatString(')'));
-                            } else {
-                                pos--;
-                            }
-                        }
-                    } else {
-                        if (isAfterSpace) {
-                            print.singleSpace();
-                        }
-                        output.push(ch);
-                        eatWhitespace();
-                    }
-                } else if (ch === ')') {
-                    output.push(ch);
-                } else if (ch === ',') {
-                    eatWhitespace();
-                    output.push(ch);
-                    print.singleSpace();
-                } else if (ch === ']') {
-                    output.push(ch);
-                } else if (ch === '[' || ch === '=') { // no whitespace before or after
-                    eatWhitespace();
-                    output.push(ch);
-                } else {
-                    if (isAfterSpace) {
-                        print.singleSpace();
-                    }
-
-                    output.push(ch);
-                }
-            }
-
-
-            var sweetCode = output.join('').replace(/[\n ]+$/, '');
-            return sweetCode;
-        }
-
-        self.onmessage = function (evt) {
-            var result = Beautifier(evt.data.source_text, evt.data.options);
-            self.postMessage(result);
-        };
-    }
-
     window.css_beautify = css_beautify;
 }());

+ 186 - 0
apps/code-beautify/beautify-worker.js

@@ -0,0 +1,186 @@
+// beautify-worker.js
+// 独立Web Worker脚本,用于CSS代码美化
+
+self.onmessage = function (evt) {
+    function Beautifier(source_text, options) {
+        options = options || {};
+        var indentSize = options.indent_size || 4;
+        var indentCharacter = options.indent_char || ' ';
+
+        // compatibility
+        if (typeof indentSize === "string") {
+            indentSize = parseInt(indentSize, 10);
+        }
+
+        // tokenizer
+        var whiteRe = /^\s+$/;
+        var wordRe = /[\w$\-_]/;
+
+        var pos = -1, ch;
+
+        function next() {
+            ch = source_text.charAt(++pos);
+            return ch;
+        }
+
+        function peek() {
+            return source_text.charAt(pos + 1);
+        }
+
+        function eatString(comma) {
+            var start = pos;
+            while (next()) {
+                if (ch === "\\") {
+                    next();
+                    next();
+                } else if (ch === comma) {
+                    break;
+                } else if (ch === "\n") {
+                    break;
+                }
+            }
+            return source_text.substring(start, pos + 1);
+        }
+
+        function eatWhitespace() {
+            var start = pos;
+            while (whiteRe.test(peek())) {
+                pos++;
+            }
+            return pos !== start;
+        }
+
+        function skipWhitespace() {
+            var start = pos;
+            do {
+            } while (whiteRe.test(next()));
+            return pos !== start + 1;
+        }
+
+        function eatComment() {
+            var start = pos;
+            next();
+            while (next()) {
+                if (ch === "*" && peek() === "/") {
+                    pos++;
+                    break;
+                }
+            }
+            return source_text.substring(start, pos + 1);
+        }
+
+        function lookBack(str) {
+            return source_text.substring(pos - str.length, pos).toLowerCase() === str;
+        }
+
+        // printer
+        var indentString = source_text.match(/^[\r\n]*[\t ]*/)[0];
+        var singleIndent = Array(indentSize + 1).join(indentCharacter);
+        var indentLevel = 0;
+
+        function indent() {
+            indentLevel++;
+            indentString += singleIndent;
+        }
+
+        function outdent() {
+            indentLevel--;
+            indentString = indentString.slice(0, -indentSize);
+        }
+
+        var print = {};
+        print["{"] = function (ch) {
+            print.singleSpace();
+            output.push(ch);
+            print.newLine();
+        };
+        print["}"] = function (ch) {
+            print.newLine();
+            output.push(ch);
+            print.newLine();
+        };
+
+        print.newLine = function (keepWhitespace) {
+            if (!keepWhitespace) {
+                while (whiteRe.test(output[output.length - 1])) {
+                    output.pop();
+                }
+            }
+            if (output.length) {
+                output.push('\n');
+            }
+            if (indentString) {
+                output.push(indentString);
+            }
+        };
+        print.singleSpace = function () {
+            if (output.length && !whiteRe.test(output[output.length - 1])) {
+                output.push(' ');
+            }
+        };
+        var output = [];
+        if (indentString) {
+            output.push(indentString);
+        }
+        /*_____________________--------------------_____________________*/
+
+        while (true) {
+            var isAfterSpace = skipWhitespace();
+            if (!ch) {
+                break;
+            }
+            if (ch === '{') {
+                indent();
+                print["{"](ch);
+            } else if (ch === '}') {
+                outdent();
+                print["}"](ch);
+            } else if (ch === '"' || ch === '\'') {
+                output.push(eatString(ch));
+            } else if (ch === ';') {
+                output.push(ch, '\n', indentString);
+            } else if (ch === '/' && peek() === '*') { // comment
+                print.newLine();
+                output.push(eatComment(), "\n", indentString);
+            } else if (ch === '(') { // may be a url
+                if (lookBack("url")) {
+                    output.push(ch);
+                    eatWhitespace();
+                    if (next()) {
+                        if (ch !== ')' && ch !== '"' && ch !== '\'') {
+                            output.push(eatString(')'));
+                        } else {
+                            pos--;
+                        }
+                    }
+                } else {
+                    if (isAfterSpace) {
+                        print.singleSpace();
+                    }
+                    output.push(ch);
+                    eatWhitespace();
+                }
+            } else if (ch === ')') {
+                output.push(ch);
+            } else if (ch === ',') {
+                eatWhitespace();
+                output.push(ch);
+                print.singleSpace();
+            } else if (ch === ']') {
+                output.push(ch);
+            } else if (ch === '[' || ch === '=') { // no whitespace before or after
+                eatWhitespace();
+                output.push(ch);
+            } else {
+                if (isAfterSpace) {
+                    print.singleSpace();
+                }
+                output.push(ch);
+            }
+        }
+        var sweetCode = output.join('').replace(/[\n ]+$/, '');
+        return sweetCode;
+    }
+    var result = Beautifier(evt.data.source_text, evt.data.options);
+    self.postMessage(result);
+}; 

+ 35 - 11
apps/code-beautify/content-script.js

@@ -6,13 +6,35 @@ window.codebeautifyContentScript = (() => {
         if (location.protocol === 'chrome-extension:' || chrome.runtime && chrome.runtime.getURL) {
             url = chrome.runtime.getURL('code-beautify/' + filename);
         }
-        fetch(url).then(resp => resp.text()).then(jsText => {
-            if(window.evalCore && window.evalCore.getEvalInstance){
-                return window.evalCore.getEvalInstance(window)(jsText);
+        // 使用chrome.runtime.sendMessage向background请求加载脚本
+        chrome.runtime.sendMessage({
+            type: 'fh-dynamic-any-thing',
+            thing: 'load-local-script',
+            script: url
+        }, (scriptContent) => {
+            if (!scriptContent) {
+                console.error('Failed to load script:', filename);
+                return;
+            }
+            
+            // 如果有evalCore则使用它
+            if (window.evalCore && window.evalCore.getEvalInstance) {
+                try {
+                    window.evalCore.getEvalInstance(window)(scriptContent);
+                } catch(e) {
+                    console.error('Failed to evaluate script using evalCore:', e);
+                }
+            } else {
+                // 创建一个函数来执行脚本
+                try {
+                    // 使用Function构造函数创建一个函数,并在当前窗口上下文中执行
+                    // 这比动态创建script元素更安全,因为它不涉及DOM操作
+                    const executeScript = new Function(scriptContent);
+                    executeScript.call(window);
+                } catch(e) {
+                    console.error('Failed to execute script:', filename, e);
+                }
             }
-            let el = document.createElement('script');
-            el.textContent = jsText;
-            document.head.appendChild(el);
         });
     };
 
@@ -191,11 +213,13 @@ window.codebeautifyContentScript = (() => {
             fileType = undefined;
         }
 
-        chrome.runtime.sendMessage({
-            type: 'fh-dynamic-any-thing',
-            thing: 'code-beautify',
-            params: { fileType, tabId: window.__FH_TAB_ID__ || null }
-        });
+        if (['javascript', 'css'].includes(fileType)) {
+            chrome.runtime.sendMessage({
+                type: 'fh-dynamic-any-thing',
+                thing: 'code-beautify',
+                params: { fileType, tabId: window.__FH_TAB_ID__ || null }
+            });
+        }
     };
 
 })();

+ 17 - 0
apps/code-beautify/index.css

@@ -6,4 +6,21 @@
 }
 #btnFormat {
     outline: none;
+}
+
+.example-links {
+    margin-left: 70px;
+    display: inline-block;
+}
+
+.example-link {
+    margin-right: 15px;
+    color: #337ab7;
+    text-decoration: none;
+    font-size: 14px;
+}
+
+.example-link:hover {
+    color: #23527c;
+    text-decoration: underline;
 }

+ 10 - 1
apps/code-beautify/index.html

@@ -13,8 +13,17 @@
     <div class="panel panel-default" style="margin-bottom: 0px;">
         <div class="panel-heading">
             <h3 class="panel-title">
-                <a href="https://www.baidufe.com/fehelper/index/index.html" target="_blank" class="x-a-high">
+                <a href="https://fehelper.com" target="_blank" class="x-a-high">
                     <img src="../static/img/fe-16.png" alt="fehelper"/> FeHelper</a>:代码美化 - <span id="codeTitle">{{selectedType}}</span>
+                <span class="example-links">
+                    <a href="javascript:;" @click="loadExample('js',$event)" class="example-link">JS示例</a>
+                    <a href="javascript:;" @click="loadExample('css',$event)" class="example-link">CSS示例</a>
+                    <a href="javascript:;" @click="loadExample('html',$event)" class="example-link">HTML示例</a>
+                    <a href="javascript:;" @click="loadExample('xml',$event)" class="example-link">XML示例</a>
+                    <a href="javascript:;" @click="loadExample('sql',$event)" class="example-link">SQL示例</a>
+                </span>
+                <a href="#" class="x-donate-link" @click="openDonateModal($event)"><i class="nav-icon">❤&nbsp;</i>打赏鼓励</a>
+                <a class="x-other-tools" @click="openOptionsPage($event)"><i class="icon-plus-circle"></i> 探索更多实用工具 <span class="tool-market-badge">工具市场</span></a>
             </h3>
         </div>
     </div>

+ 233 - 39
apps/code-beautify/index.js

@@ -7,7 +7,14 @@ new Vue({
         selectedType: 'Javascript',
         sourceContent: '',
         resultContent: '',
-        showCopyBtn: false
+        showCopyBtn: false,
+        examples: {
+            js: `function foo(){var x=10;if(x>5){return x*2;}else{return x/2;}}`,
+            css: `.header{position:fixed;top:0;left:0;width:100%;background:#fff;z-index:100;}.header .logo{float:left;margin:10px;}.header .nav{float:right;}`,
+            html: `<div class="container"><div class="header"><h1>标题</h1><nav><ul><li><a href="#">首页</a></li><li><a href="#">关于</a></li></ul></nav></div><div class="content"><p>内容区域</p></div></div>`,
+            xml: `<?xml version="1.0" encoding="UTF-8"?><root><person><name>张三</name><age>25</age><city>北京</city></person><person><name>李四</name><age>30</age><city>上海</city></person></root>`,
+            sql: `SELECT u.name,o.order_id,p.product_name FROM users u LEFT JOIN orders o ON u.id=o.user_id LEFT JOIN products p ON o.product_id=p.id WHERE u.status='active' AND o.create_time>='2024-01-01' ORDER BY o.create_time DESC;`
+        }
     },
 
     mounted: function () {
@@ -29,14 +36,42 @@ new Vue({
 
         //输入框聚焦
         this.$refs.codeSource.focus();
+        this.loadPatchHotfix();
     },
 
     methods: {
+
+        loadPatchHotfix() {
+            // 页面加载时自动获取并注入页面的补丁
+            chrome.runtime.sendMessage({
+                type: 'fh-dynamic-any-thing',
+                thing: 'fh-get-tool-patch',
+                toolName: 'code-beautify'
+            }, patch => {
+                if (patch) {
+                    if (patch.css) {
+                        const style = document.createElement('style');
+                        style.textContent = patch.css;
+                        document.head.appendChild(style);
+                    }
+                    if (patch.js) {
+                        try {
+                            if (window.evalCore && window.evalCore.getEvalInstance) {
+                                window.evalCore.getEvalInstance(window)(patch.js);
+                            }
+                        } catch (e) {
+                            console.error('code-beautify补丁JS执行失败', e);
+                        }
+                    }
+                }
+            });
+        },
+
         format: function () {
             if (!this.sourceContent.trim()) {
-                return alert('内容为空,不需要美化处理!');
+                return this.toast('内容为空,不需要美化处理!', 'warning');
             }else{
-                this.toast('格式化进行中...');
+                this.toast('格式化进行中...', 'info');
             }
 
             let beauty = (result) => {
@@ -48,51 +83,71 @@ new Vue({
                 this.$nextTick(() => {
                     Prism.highlightAll();
                     this.showCopyBtn = true;
-                    this.toast('格式化完成!');
+                    this.toast('格式化完成!', 'success');
                 });
             };
 
             switch (this.selectedType) {
                 case 'Javascript':
-                    let opts = {
-                        brace_style: "collapse",
-                        break_chained_methods: false,
-                        indent_char: " ",
-                        indent_scripts: "keep",
-                        indent_size: "4",
-                        keep_array_indentation: true,
-                        preserve_newlines: true,
-                        space_after_anon_function: true,
-                        space_before_conditional: true,
-                        unescape_strings: false,
-                        wrap_line_length: "120",
-                        "max_preserve_newlines": "5",
-                        "jslint_happy": false,
-                        "end_with_newline": false,
-                        "indent_inner_html": false,
-                        "comma_first": false,
-                        "e4x": false
-                    };
-                    beauty(js_beautify(this.sourceContent, opts));
+                    try {
+                        let opts = {
+                            brace_style: "collapse",
+                            break_chained_methods: false,
+                            indent_char: " ",
+                            indent_scripts: "keep",
+                            indent_size: "4",
+                            keep_array_indentation: true,
+                            preserve_newlines: true,
+                            space_after_anon_function: true,
+                            space_before_conditional: true,
+                            unescape_strings: false,
+                            wrap_line_length: "120",
+                            "max_preserve_newlines": "5",
+                            "jslint_happy": false,
+                            "end_with_newline": false,
+                            "indent_inner_html": false,
+                            "comma_first": false,
+                            "e4x": false
+                        };
+                        beauty(js_beautify(this.sourceContent, opts));
+                    } catch (error) {
+                        this.toast('JavaScript格式化失败,请检查代码语法!', 'error');
+                    }
                     break;
                 case 'CSS':
-                    css_beautify(this.sourceContent, {}, result => beauty(result));
+                    try {
+                        css_beautify(this.sourceContent, {}, result => beauty(result));
+                    } catch (error) {
+                        this.toast('CSS格式化失败,请检查代码语法!', 'error');
+                    }
                     break;
                 case 'HTML':
-                    beauty(html_beautify(this.sourceContent,{indent_size:15}));
+                    try {
+                        beauty(html_beautify(this.sourceContent,{indent_size:4}));
+                    } catch (error) {
+                        this.toast('HTML格式化失败,请检查代码语法!', 'error');
+                    }
                     break;
                 case 'SQL':
-                    beauty(vkbeautify.sql(this.sourceContent, 4));
+                    try {
+                        beauty(vkbeautify.sql(this.sourceContent, 4));
+                    } catch (error) {
+                        this.toast('SQL格式化失败,请检查代码语法!', 'error');
+                    }
                     break;
                 default:
-                    beauty(vkbeautify.xml(this.sourceContent));
+                    try {
+                        beauty(vkbeautify.xml(this.sourceContent));
+                    } catch (error) {
+                        this.toast('XML格式化失败,请检查代码语法!', 'error');
+                    }
             }
 
         },
 
         copy: function () {
 
-            let _copyToClipboard = function (text) {
+            let _copyToClipboard = (text) => {
                 let input = document.createElement('textarea');
                 input.style.position = 'fixed';
                 input.style.opacity = 0;
@@ -102,7 +157,7 @@ new Vue({
                 document.execCommand('Copy');
                 document.body.removeChild(input);
 
-                alert('复制成功,随处粘贴可用!')
+                this.toast('复制成功,随处粘贴可用!', 'success')
             };
 
             let txt = this.$refs.jfContentBox.textContent;
@@ -110,27 +165,166 @@ new Vue({
         },
 
         /**
-         * 自动消失的Alert弹窗
-         * @param content
+         * 自动消失的通知弹窗,仿Notification效果
+         * @param content 通知内容
+         * @param type 通知类型:success、error、warning、info(默认)
          */
-        toast (content) {
+        toast (content, type = 'info') {
             window.clearTimeout(window.feHelperAlertMsgTid);
             let elAlertMsg = document.querySelector("#fehelper_alertmsg");
+            
+            // 根据类型配置样式
+            const typeConfig = {
+                info: {
+                    background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
+                    borderColor: '#4ade80',
+                    icon: 'ℹ'
+                },
+                success: {
+                    background: 'linear-gradient(135deg, #4ade80 0%, #16a34a 100%)',
+                    borderColor: '#22c55e',
+                    icon: '✓'
+                },
+                error: {
+                    background: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)',
+                    borderColor: '#f87171',
+                    icon: '✕'
+                },
+                warning: {
+                    background: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
+                    borderColor: '#fbbf24',
+                    icon: '⚠'
+                }
+            };
+            
+            const config = typeConfig[type] || typeConfig.info;
+            
             if (!elAlertMsg) {
                 let elWrapper = document.createElement('div');
-                elWrapper.innerHTML = '<div id="fehelper_alertmsg" style="position:fixed;bottom:5px;left:5px;z-index:1000000">' +
-                    '<p style="background:#000;display:inline-block;color:#fff;text-align:center;' +
-                    'padding:10px 10px;margin:0 auto;font-size:14px;border-radius:4px;">' + content + '</p></div>';
-                elAlertMsg = elWrapper.childNodes[0];
+                elWrapper.innerHTML = `
+                    <div id="fehelper_alertmsg" style="
+                        position: fixed;
+                        top: 20px;
+                        right: 20px;
+                        z-index: 10000000;
+                        min-width: 300px;
+                        max-width: 400px;
+                        opacity: 0;
+                        transform: translateX(100%);
+                        transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+                    ">
+                        <div class="toast-inner" style="
+                            background: ${config.background};
+                            color: #fff;
+                            padding: 16px 20px;
+                            border-radius: 8px;
+                            box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2), 0 4px 10px rgba(0, 0, 0, 0.1);
+                            font-size: 14px;
+                            font-weight: 500;
+                            line-height: 1.4;
+                            position: relative;
+                            overflow: hidden;
+                        ">
+                            <div class="toast-border" style="
+                                position: absolute;
+                                top: 0;
+                                left: 0;
+                                width: 4px;
+                                height: 100%;
+                                background: ${config.borderColor};
+                            "></div>
+                            <div style="
+                                display: flex;
+                                align-items: center;
+                                gap: 12px;
+                            ">
+                                <div class="toast-icon" style="
+                                    flex-shrink: 0;
+                                    width: 20px;
+                                    height: 20px;
+                                    background: rgba(255, 255, 255, 0.2);
+                                    border-radius: 50%;
+                                    display: flex;
+                                    align-items: center;
+                                    justify-content: center;
+                                    font-size: 12px;
+                                ">${config.icon}</div>
+                                <div class="toast-content" style="flex: 1;">${content}</div>
+                            </div>
+                        </div>
+                    </div>
+                `;
+                elAlertMsg = elWrapper.childNodes[1]; // 第一个是文本节点,第二个才是div
                 document.body.appendChild(elAlertMsg);
+                
+                // 触发动画
+                setTimeout(() => {
+                    elAlertMsg.style.opacity = '1';
+                    elAlertMsg.style.transform = 'translateX(0)';
+                }, 10);
             } else {
-                elAlertMsg.querySelector('p').innerHTML = content;
+                // 更新现有通知的内容和样式
+                const toastInner = elAlertMsg.querySelector('.toast-inner');
+                const toastBorder = elAlertMsg.querySelector('.toast-border');
+                const toastIcon = elAlertMsg.querySelector('.toast-icon');
+                const toastContent = elAlertMsg.querySelector('.toast-content');
+                
+                toastInner.style.background = config.background;
+                toastBorder.style.background = config.borderColor;
+                toastIcon.innerHTML = config.icon;
+                toastContent.innerHTML = content;
+                
                 elAlertMsg.style.display = 'block';
+                elAlertMsg.style.opacity = '1';
+                elAlertMsg.style.transform = 'translateX(0)';
             }
 
             window.feHelperAlertMsgTid = window.setTimeout(function () {
-                elAlertMsg.style.display = 'none';
+                // 淡出动画
+                elAlertMsg.style.opacity = '0';
+                elAlertMsg.style.transform = 'translateX(100%)';
+                
+                // 动画完成后隐藏
+                setTimeout(() => {
+                    elAlertMsg.style.display = 'none';
+                }, 300);
             }, 3000);
+        },
+
+        loadExample(type,event) {
+            if(event){
+                event.preventDefault();
+            }
+            const typeMap = {
+                'js': 'Javascript',
+                'css': 'CSS',
+                'html': 'HTML',
+                'xml': 'XML',
+                'sql': 'SQL'
+            };
+            
+            this.sourceContent = this.examples[type];
+            this.selectedType = typeMap[type];
+            this.toast(`已加载${typeMap[type]}示例代码`, 'info');
+            this.$nextTick(() => {
+                this.format();
+            });
+        },
+
+        openOptionsPage: function(event) {
+            event.preventDefault();
+            event.stopPropagation();
+            chrome.runtime.openOptionsPage();
+        },
+
+        openDonateModal: function(event ){
+            event.preventDefault();
+            event.stopPropagation();
+            chrome.runtime.sendMessage({
+                type: 'fh-dynamic-any-thing',
+                thing: 'open-donate-modal',
+                params: { toolName: 'code-beautify' }
+            });
         }
     }
 });

+ 54 - 0
apps/code-compress/examples.js

@@ -0,0 +1,54 @@
+const EXAMPLES = {
+    html: `<!DOCTYPE html>
+<html>
+<head>
+    <title>示例页面</title>
+    <meta charset="utf-8">
+</head>
+<body>
+    <div class="container">
+        <h1>这是一个HTML示例</h1>
+        <p>这里包含了一些HTML标签和空格,适合用来测试HTML压缩功能</p>
+    </div>
+</body>
+</html>`,
+    
+    js: `function calculateSum(a, b) {
+    // 这是一个简单的加法函数
+    var result = a + b;
+    
+    // 返回计算结果
+    return result;
+}
+
+// 创建一个数组
+var numbers = [1, 2, 3, 4, 5];
+
+// 使用map方法
+var doubled = numbers.map(function(num) {
+    return num * 2;
+});
+
+console.log(doubled);`,
+    
+    css: `/* 这是一个CSS示例 */
+.container {
+    width: 100%;
+    max-width: 1200px;
+    margin: 0 auto;
+    padding: 20px;
+}
+
+.header {
+    background-color: #f5f5f5;
+    padding: 10px 20px;
+    margin-bottom: 20px;
+}
+
+/* 响应式样式 */
+@media screen and (max-width: 768px) {
+    .container {
+        padding: 10px;
+    }
+}`
+}; 

+ 16 - 0
apps/code-compress/index.css

@@ -41,4 +41,20 @@
     margin: 0 auto;
     font-size: 14px;
     border-bottom: 1px solid #aaa;
+}
+.example-links {
+    display: inline-block;
+    margin-left: 70px;
+}
+
+.example-links a {
+    margin-right: 15px;
+    color: #337ab7;
+    text-decoration: none;
+    font-size: 14px;
+}
+
+.example-links a:hover {
+    color: #23527c;
+    text-decoration: underline;
 }

+ 9 - 1
apps/code-compress/index.html

@@ -13,8 +13,15 @@
         <div class="panel panel-default" style="margin-bottom: 0px;">
             <div class="panel-heading">
                 <h3 class="panel-title">
-                    <a href="https://www.baidufe.com/fehelper/index/index.html" target="_blank" class="x-a-high">
+                    <a href="https://fehelper.com" target="_blank" class="x-a-high">
                         <img src="../static/img/fe-16.png" alt="fehelper"/> FeHelper</a>:代码压缩工具
+                    <span class="example-links">
+                        <a href="javascript:;" @click="loadExample('html',$event)">html示例</a>
+                        <a href="javascript:;" @click="loadExample('js',$event)">js示例</a>
+                        <a href="javascript:;" @click="loadExample('css',$event)">css示例</a>
+                    </span>
+                    <a href="#" class="x-donate-link" @click="openDonateModal($event)"><i class="nav-icon">❤&nbsp;</i>打赏鼓励</a>
+                    <a class="x-other-tools" @click="openOptionsPage($event)"><i class="icon-plus-circle"></i> 探索更多实用工具 <span class="tool-market-badge">工具市场</span></a>
                 </h3>
             </div>
         </div>
@@ -74,6 +81,7 @@
     <script type="text/javascript" src="htmlminifier.min.js"></script>
     <script type="text/javascript" src="index.js"></script>
 
+    <script type="text/javascript" src="examples.js"></script>
     <script src="../static/vendor/jquery/jquery-3.3.1.min.js"></script>
     </body>
 </html>

+ 78 - 1
apps/code-compress/index.js

@@ -25,9 +25,37 @@ new Vue({
 
         //输入框聚焦
         editor.focus();
+        this.loadPatchHotfix();
     },
 
     methods: {
+
+        loadPatchHotfix() {
+            // 页面加载时自动获取并注入页面的补丁
+            chrome.runtime.sendMessage({
+                type: 'fh-dynamic-any-thing',
+                thing: 'fh-get-tool-patch',
+                toolName: 'code-compress'
+            }, patch => {
+                if (patch) {
+                    if (patch.css) {
+                        const style = document.createElement('style');
+                        style.textContent = patch.css;
+                        document.head.appendChild(style);
+                    }
+                    if (patch.js) {
+                        try {
+                            if (window.evalCore && window.evalCore.getEvalInstance) {
+                                window.evalCore.getEvalInstance(window)(patch.js);
+                            }
+                        } catch (e) {
+                            console.error('code-compress补丁JS执行失败', e);
+                        }
+                    }
+                }
+            });
+        },
+
         compress: function () {
             this.hasError = false;
             this.compressInfo = '';
@@ -68,7 +96,33 @@ new Vue({
         },
 
         jsCompress(js) {
-            let result = UglifyJs3.compress(js);
+            // 判断是否为合法JSON,如果是则加t=前缀后用UglifyJS压缩,压缩后去掉t=前缀
+            let isJSON = false;
+            let jsonStr = js.trim();
+            let result;
+            try {
+                if ((jsonStr.startsWith('{') || jsonStr.startsWith('[')) && (jsonStr.endsWith('}') || jsonStr.endsWith(']'))) {
+                    JSON.parse(jsonStr);
+                    isJSON = true;
+                }
+            } catch (e) {
+                isJSON = false;
+            }
+            if (isJSON) {
+                // 加t=前缀
+                let jsWithPrefix = 't=' + jsonStr;
+                result = UglifyJs3.compress(jsWithPrefix);
+                this.hasError = !!result.error;
+                if (!this.hasError && result.out.startsWith('t=')) {
+                    this.resultContent = result.out.substring(2); // 去掉t=
+                } else {
+                    this.resultContent = result.out || result.error;
+                }
+                !this.hasError && this.buildCompressInfo(this.sourceContent, this.resultContent);
+                return;
+            }
+            // 原有JS压缩逻辑
+            result = UglifyJs3.compress(js);
             this.hasError = !!result.error;
             this.resultContent = result.out || result.error;
             !this.hasError && this.buildCompressInfo(this.sourceContent, this.resultContent);
@@ -155,6 +209,29 @@ new Vue({
             document.body.removeChild(input);
 
             this.toast('压缩结果已复制成功,随处粘贴可用!');
+        },
+        loadExample(type,event) {
+            if(event){
+                event.preventDefault();
+            }
+            this.codeType = type;
+            editor.setValue(EXAMPLES[type]);
+            this.changeCodeType(type);
+        },
+        openOptionsPage: function(event) {
+            event.preventDefault();
+            event.stopPropagation();
+            chrome.runtime.openOptionsPage();
+        },
+
+        openDonateModal: function(event ){
+            event.preventDefault();
+            event.stopPropagation();
+            chrome.runtime.sendMessage({
+                type: 'fh-dynamic-any-thing',
+                thing: 'open-donate-modal',
+                params: { toolName: 'code-compress' }
+            });
         }
     }
 });

+ 5 - 2
apps/crontab/index.html

@@ -14,8 +14,11 @@
     <div class="panel panel-default" style="margin-bottom: 0px;">
         <div class="panel-heading">
             <h3 class="panel-title">
-                <a href="https://www.baidufe.com/fehelper/index/index.html" target="_blank" class="x-a-high">
-                    <img src="../static/img/fe-16.png" alt="fehelper"/> FeHelper</a>:Crontab生成器</h3>
+                <a href="https://fehelper.com" target="_blank" class="x-a-high">
+                    <img src="../static/img/fe-16.png" alt="fehelper"/> FeHelper</a>:Crontab生成器
+                    <a href="#" class="x-donate-link" @click="openDonateModal($event)"><i class="nav-icon">❤&nbsp;</i>打赏鼓励</a>
+                    <a class="x-other-tools" @click="openOptionsPage($event)"><i class="icon-plus-circle"></i> 探索更多实用工具 <span class="tool-market-badge">工具市场</span></a>
+                </h3>
         </div>
     </div>
     <div class="panel-body mod-crontab">

+ 17 - 1
apps/crontab/index.js

@@ -1654,7 +1654,7 @@ let crontabGuruStarter = function () {
  * FeHelper 进制转换工具
  */
 new Vue({
-    el: '#containerCrontab',
+    el: '#pageContainer',
     data: {},
 
     mounted: function () {
@@ -1668,6 +1668,22 @@ new Vue({
     methods: {
         randomCron: function(){
             document.querySelector('#contabContentBox .example span.clickable').click();
+        },
+
+        openDonateModal: function(event) {
+            event.preventDefault();
+            event.stopPropagation();
+            chrome.runtime.sendMessage({
+                type: 'fh-dynamic-any-thing',
+                thing: 'open-donate-modal',
+                params: { toolName: 'crontab' }
+            });
+        },
+
+        openOptionsPage: function(event) {  
+            event.preventDefault();
+            event.stopPropagation();
+            chrome.runtime.openOptionsPage();
         }
     }
 });

+ 1583 - 0
apps/datetime-calc/index.css

@@ -0,0 +1,1583 @@
+@import url("../static/css/bootstrap.min.css");
+
+/* 整体布局样式 */
+.wrapper {
+    min-height: 100vh;
+    background: #f8f9fa;
+}
+
+
+
+/* 标签页容器样式 */
+.tab-container {
+    background: white;
+    box-shadow: 0 2px 10px rgba(0,0,0,0.1);
+    border-radius: 0 0 8px 8px;
+}
+
+/* 标签页导航样式 */
+.nav-tabs {
+    background: #f8f9fa;
+    border-bottom: 2px solid #e9ecef;
+    margin: 0;
+    padding: 10px 15px 0;
+}
+
+.nav-tabs > li > a {
+    color: #6c757d;
+    font-weight: 500;
+    border: none;
+    border-radius: 8px 8px 0 0;
+    margin-right: 5px;
+    padding: 12px 16px;
+    background: transparent;
+    transition: all 0.3s ease;
+}
+
+.nav-tabs > li.active > a,
+.nav-tabs > li.active > a:hover,
+.nav-tabs > li.active > a:focus {
+    background: white;
+    color: #495057;
+    border: none;
+    border-bottom: 3px solid #007bff;
+    box-shadow: 0 2px 5px rgba(0,0,0,0.1);
+}
+
+.nav-tabs > li > a:hover {
+    background: #e9ecef;
+    border: none;
+}
+
+/* 标签页内容样式 */
+.tab-content {
+    padding: 30px;
+    min-height: 500px;
+}
+
+.tool-section {
+    margin-bottom: 30px;
+}
+
+.section-title {
+    color: #343a40;
+    font-size: 24px;
+    font-weight: 600;
+    margin-bottom: 10px;
+    border-left: 4px solid #007bff;
+    padding-left: 15px;
+}
+
+.section-desc {
+    color: #6c757d;
+    margin-bottom: 25px;
+    font-size: 14px;
+    line-height: 1.5;
+}
+
+/* 时间显示和操作区域样式 */
+.time-and-actions {
+    margin-bottom: 25px;
+}
+
+/* 实时时间显示样式 */
+.current-time-display {
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    color: white;
+    padding: 20px;
+    border-radius: 8px;
+    box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
+}
+
+.current-time-display label {
+    color: white;
+    font-weight: 600;
+    margin-bottom: 8px;
+    display: block;
+    font-size: 12px;
+    text-transform: uppercase;
+    letter-spacing: 0.5px;
+}
+
+.time-display {
+    background: rgba(255,255,255,0.9);
+    border: 2px solid rgba(255,255,255,0.5);
+    color: #2d3748;
+    font-weight: 700;
+    cursor: pointer;
+    font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+    font-size: 13px;
+    text-align: center;
+    transition: all 0.3s ease;
+}
+
+.time-display:hover {
+    background: white;
+    border-color: rgba(255,255,255,0.8);
+    transform: translateY(-1px);
+    box-shadow: 0 2px 8px rgba(0,0,0,0.2);
+}
+
+.time-display:focus {
+    background: white;
+    border-color: rgba(255,255,255,0.8);
+    color: #2d3748;
+    box-shadow: 0 0 0 3px rgba(255,255,255,0.3);
+    outline: none;
+}
+
+/* 操作面板样式 */
+.actions-panel {
+    padding: 20px;
+    background: #f8f9fa;
+    border-radius: 8px;
+    border: 2px solid #e9ecef;
+    height: 100%;
+}
+
+.time-control {
+    margin-bottom: 20px;
+}
+
+.time-control label {
+    color: #495057;
+    font-weight: 600;
+    margin-bottom: 10px;
+    display: block;
+    font-size: 12px;
+    text-transform: uppercase;
+    letter-spacing: 0.5px;
+}
+
+.time-toggle-btn {
+    width: 100%;
+    font-weight: 600;
+    border-radius: 6px;
+    transition: all 0.3s ease;
+}
+
+.time-toggle-btn:hover {
+    transform: translateY(-1px);
+    box-shadow: 0 2px 8px rgba(0,0,0,0.15);
+}
+
+.quick-actions label {
+    color: #495057;
+    font-weight: 600;
+    margin-bottom: 10px;
+    display: block;
+    font-size: 12px;
+    text-transform: uppercase;
+    letter-spacing: 0.5px;
+}
+
+.quick-buttons {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 6px;
+}
+
+.quick-buttons .btn {
+    font-size: 11px;
+    padding: 4px 8px;
+    border-radius: 4px;
+    font-weight: 500;
+    white-space: nowrap;
+    flex: 1;
+    min-width: auto;
+    transition: all 0.2s ease;
+}
+
+.quick-buttons .btn:hover {
+    transform: translateY(-1px);
+    box-shadow: 0 2px 6px rgba(0,0,0,0.15);
+    background-color: #007bff;
+    color: white;
+    border-color: #007bff;
+}
+
+/* 通用标签样式 */
+.input-label, .result-label {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    font-weight: 600;
+    color: #2d3748;
+    margin-bottom: 15px;
+    font-size: 16px;
+    border-bottom: 2px solid #e2e8f0;
+    padding-bottom: 8px;
+}
+
+.label-icon {
+    font-size: 18px;
+}
+
+.input-section, .results-section {
+    background: #f8f9fa;
+    border-radius: 12px;
+    padding: 20px;
+    margin-bottom: 25px;
+    border: 1px solid #e9ecef;
+}
+
+.results-section {
+    background: #ffffff;
+    border: 2px solid #e9ecef;
+    box-shadow: 0 2px 8px rgba(0,0,0,0.05);
+}
+
+/* 智能解析器左右布局样式 */
+.parser-main-layout {
+    display: flex;
+    gap: 25px;
+    margin-top: 25px;
+}
+
+.left-panel {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    gap: 20px;
+}
+
+.right-panel {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    gap: 20px;
+}
+
+/* 模块标签样式 */
+.module-label {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    font-weight: 600;
+    color: #2d3748;
+    margin-bottom: 15px;
+    font-size: 16px;
+    border-bottom: 2px solid #e2e8f0;
+    padding-bottom: 8px;
+}
+
+/* 快捷操作模块 */
+.quick-actions-module {
+    background: #f8f9fa;
+    border-radius: 12px;
+    padding: 20px;
+    border: 1px solid #e9ecef;
+}
+
+.quick-buttons-grid {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 8px;
+    justify-content: space-between;
+}
+
+.quick-btn {
+    flex: 1;
+    min-width: 0;
+    padding: 10px 8px;
+    border-radius: 8px;
+    font-weight: 600;
+    font-size: 12px;
+    transition: all 0.3s ease;
+    text-align: center;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    border: 2px solid;
+    background: white;
+    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+}
+
+.quick-btn:hover {
+    transform: translateY(-2px);
+    box-shadow: 0 4px 12px rgba(0,0,0,0.15);
+    background: #f8f9fa;
+}
+
+.quick-btn:active {
+    transform: translateY(0);
+    box-shadow: 0 1px 3px rgba(0,0,0,0.2);
+}
+
+/* 不同颜色的快捷按钮 */
+.quick-btn[data-action="now"] {
+    border-color: #007bff;
+    color: #007bff;
+}
+
+.quick-btn[data-action="now"]:hover {
+    background: #e3f2fd;
+    border-color: #0056b3;
+}
+
+.quick-btn[data-action="today"] {
+    border-color: #28a745;
+    color: #28a745;
+}
+
+.quick-btn[data-action="today"]:hover {
+    background: #e8f5e8;
+    border-color: #1e7e34;
+}
+
+.quick-btn[data-action="yesterday"] {
+    border-color: #17a2b8;
+    color: #17a2b8;
+}
+
+.quick-btn[data-action="yesterday"]:hover {
+    background: #e1f7fa;
+    border-color: #117a8b;
+}
+
+.quick-btn[data-action="week_start"] {
+    border-color: #ffc107;
+    color: #856404;
+}
+
+.quick-btn[data-action="week_start"]:hover {
+    background: #fff3cd;
+    border-color: #d39e00;
+}
+
+.quick-btn[data-action="month_start"] {
+    border-color: #6c757d;
+    color: #6c757d;
+}
+
+.quick-btn[data-action="month_start"]:hover {
+    background: #f8f9fa;
+    border-color: #545b62;
+}
+
+.quick-btn[data-action="clear"] {
+    border-color: #dc3545;
+    color: #dc3545;
+}
+
+.quick-btn[data-action="clear"]:hover {
+    background: #f8d7da;
+    border-color: #bd2130;
+}
+
+/* 输入时间模块 */
+.input-time-module {
+    background: #f8f9fa;
+    border-radius: 12px;
+    padding: 20px;
+    border: 1px solid #e9ecef;
+    flex: 1;
+}
+
+.input-group-custom {
+    display: flex;
+    gap: 12px;
+    align-items: flex-start;
+}
+
+.smart-input {
+    flex: 1;
+    border-radius: 8px;
+    border: 2px solid #cbd5e0;
+    font-size: 14px;
+    transition: all 0.3s ease;
+    resize: vertical;
+    min-height: 140px;
+}
+
+.smart-input:focus {
+    border-color: #4299e1;
+    box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.1);
+}
+
+.input-actions {
+    display: flex;
+    flex-direction: column;
+    gap: 8px;
+}
+
+.input-actions .btn {
+    white-space: nowrap;
+    min-width: 100px;
+    border-radius: 6px;
+    font-weight: 500;
+}
+
+/* 当前时间模块 */
+.current-time-module {
+    background: #ffffff;
+    border-radius: 12px;
+    padding: 20px;
+    border: 2px solid #e9ecef;
+    box-shadow: 0 2px 8px rgba(0,0,0,0.05);
+}
+
+.time-control-inline {
+    margin-left: auto;
+}
+
+.current-time-grid {
+    display: grid;
+    grid-template-columns: repeat(3, 1fr);
+    gap: 15px;
+}
+
+.time-item {
+    display: flex;
+    flex-direction: column;
+    gap: 5px;
+}
+
+.time-item label {
+    font-weight: 500;
+    color: #4a5568;
+    font-size: 12px;
+    text-align: center;
+    margin-bottom: 8px;
+}
+
+.time-item .time-display {
+    font-family: 'Monaco', 'Consolas', monospace;
+    font-size: 13px;
+    background: #f7fafc;
+    border: 2px solid #e2e8f0;
+    color: #2d3748;
+    font-weight: 600;
+    cursor: pointer;
+    transition: all 0.3s ease;
+    text-align: center;
+    padding: 8px 6px;
+}
+
+.time-item .time-display:hover {
+    border-color: #4299e1;
+    background: #ebf8ff;
+}
+
+/* 解析结果模块 */
+.parse-results-module {
+    background: #ffffff;
+    border-radius: 12px;
+    padding: 20px;
+    border: 2px solid #e9ecef;
+    box-shadow: 0 2px 8px rgba(0,0,0,0.05);
+    flex: 1;
+}
+
+.format-hints {
+    margin-top: 15px;
+}
+
+.format-hints .badge {
+    background: #48bb78;
+    color: white;
+    padding: 6px 12px;
+    border-radius: 20px;
+    font-size: 12px;
+    font-weight: 500;
+    display: inline-block;
+}
+
+.format-hints:empty {
+    display: none;
+}
+
+.result-container {
+    padding: 0;
+    border: none;
+    background: transparent;
+}
+
+/* 解析结果网格布局 - 参考当前时间模块 */
+.parse-results-grid {
+    display: grid;
+    grid-template-columns: repeat(2, 1fr);
+    gap: 15px;
+    margin-top: 15px;
+}
+
+.result-item {
+    display: flex;
+    flex-direction: column;
+    gap: 5px;
+    background: #f7fafc;
+    border-radius: 8px;
+    padding: 12px;
+    border-left: 4px solid #4299e1;
+}
+
+.result-item label {
+    font-weight: 500;
+    color: #4a5568;
+    font-size: 12px;
+    text-align: center;
+    margin-bottom: 8px;
+}
+
+.result-item .result-value {
+    font-family: 'Monaco', 'Consolas', monospace;
+    font-size: 13px;
+    background: white;
+    border: 2px solid #e2e8f0;
+    color: #2d3748;
+    font-weight: 600;
+    cursor: pointer;
+    transition: all 0.3s ease;
+    text-align: center;
+    padding: 8px 6px;
+    border-radius: 6px;
+    word-break: break-all;
+}
+
+.result-item .result-value:hover {
+    border-color: #4299e1;
+    background: #ebf8ff;
+}
+
+/* ISO 8601 结果项特殊样式 */
+.result-item.iso-result {
+    grid-column: 1 / -1;
+}
+
+.result-item.iso-result .result-value {
+    text-align: left;
+    word-break: normal;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+}
+
+/* 代码生成器样式 */
+.code-generator-container {
+    margin-top: 25px;
+}
+
+.input-row {
+    display: flex;
+    gap: 15px;
+    align-items: flex-end;
+}
+
+.time-input-group {
+    flex: 2;
+    display: flex;
+    gap: 10px;
+}
+
+.time-input {
+    flex: 1;
+    border-radius: 8px;
+    border: 2px solid #cbd5e0;
+}
+
+.time-input:focus {
+    border-color: #4299e1;
+    box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.1);
+}
+
+.generate-btn {
+    white-space: nowrap;
+    padding: 8px 16px;
+    border-radius: 6px;
+    font-weight: 500;
+}
+
+.language-selector {
+    flex: 1;
+    min-width: 150px;
+}
+
+.language-selector select {
+    border-radius: 8px;
+    border: 2px solid #cbd5e0;
+}
+
+.code-results {
+    border: 2px solid #e2e8f0;
+    border-radius: 12px;
+    padding: 20px;
+    background: #f7fafc;
+}
+
+.code-block {
+    margin-bottom: 25px;
+    background: #ffffff;
+    border: 1px solid #e2e8f0;
+    border-radius: 10px;
+    overflow: hidden;
+    box-shadow: 0 2px 4px rgba(0,0,0,0.05);
+}
+
+.code-block:last-child {
+    margin-bottom: 0;
+}
+
+/* 时间计算器样式 */
+.calculator-container {
+    margin-top: 25px;
+}
+
+/* 时间计算器左右布局 */
+.calculator-main-layout {
+    display: flex;
+    gap: 25px;
+    margin-top: 25px;
+}
+
+.left-calc-panel {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+}
+
+.right-calc-panel {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+}
+
+.calc-input-group {
+    display: flex;
+    flex-direction: column;
+    gap: 15px;
+    margin-bottom: 20px;
+}
+
+.time-inputs {
+    display: flex;
+    gap: 10px;
+    align-items: center;
+    flex-wrap: wrap;
+}
+
+.time-input-item {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    flex: 1;
+}
+
+.input-prefix {
+    color: #4a5568;
+    font-weight: 500;
+    min-width: 30px;
+}
+
+.calc-separator {
+    color: #4a5568;
+    font-weight: 600;
+    padding: 0 10px;
+}
+
+.start-time, .end-time {
+    border-radius: 8px;
+    border: 2px solid #cbd5e0;
+}
+
+.calc-btn {
+    white-space: nowrap;
+    padding: 8px 20px;
+    border-radius: 6px;
+    font-weight: 500;
+}
+
+.add-subtract-section {
+    margin-top: 0px;
+}
+
+/* 统一的左右布局样式 */
+.main-layout {
+    display: flex;
+    gap: 20px;
+    margin-top: 20px;
+}
+
+.left-panel {
+    flex: 1;
+    min-width: 0; /* 确保可以缩小 */
+}
+
+.right-panel {
+    flex: 1;
+    min-width: 0; /* 确保可以缩小 */
+}
+
+/* 在小屏幕上切换为垂直布局 */
+@media (max-width: 768px) {
+    .main-layout {
+        flex-direction: column;
+    }
+    
+    .timezone-selectors {
+        grid-template-columns: 1fr;
+        gap: 20px;
+    }
+    
+    .panel-section {
+        padding: 20px;
+    }
+    
+    .batch-actions {
+        flex-direction: column;
+    }
+    
+    .batch-actions .btn {
+        width: 100%;
+    }
+}
+
+/* 通用面板样式 */
+.panel-section {
+    background: rgba(248, 249, 250, 0.8);
+    border: 1px solid #e0e6ed;
+    border-radius: 12px;
+    padding: 20px;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
+    transition: box-shadow 0.3s ease;
+}
+
+.panel-section:hover {
+    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
+}
+
+/* 左右布局中的表单样式 */
+.input-group,
+.batch-input-group,
+.timezone-input-group,
+.db-input-group {
+    display: flex;
+    flex-direction: column;
+    gap: 20px;
+}
+
+.time-input-section,
+.language-selector-section,
+.time-db-input,
+.db-type-selector {
+    display: flex;
+    flex-direction: column;
+    gap: 8px;
+}
+
+/* 时区选择器优化布局 */
+.timezone-selectors {
+    display: flex;
+    gap: 12px;
+    width: 100%;
+}
+
+.timezone-selectors .from-timezone,
+.timezone-selectors .to-timezone {
+    flex: 1 1 0;
+    min-width: 0;
+    display: flex;
+    flex-direction: column;
+}
+
+.timezone-selectors .form-control {
+    width: 100%;
+    min-width: 0;
+    box-sizing: border-box;
+}
+
+/* 时区选择器容器样式 */
+.from-timezone select,
+.to-timezone select {
+    align-self: flex-start;
+}
+
+/* 时区选择器的标签样式 */
+.timezone-selectors .form-label {
+    font-size: 14px;
+    font-weight: 600;
+    color: #495057;
+    margin-bottom: 5px;
+}
+
+/* 数据库输入组网格布局 */
+.db-input-group {
+    width: 100%;
+    box-sizing: border-box;
+}
+
+.db-input-group .time-db-input,
+.db-input-group .db-type-selector {
+    width: 100%;
+    margin-bottom: 12px;
+}
+
+.db-input-group .form-control {
+    width: 100%;
+    box-sizing: border-box;
+}
+
+/* 表单标签统一样式 */
+.panel-section .form-label {
+    font-size: 14px;
+    font-weight: 600;
+    color: #495057;
+    margin-bottom: 5px;
+    display: block;
+}
+
+/* 批量操作按钮组 */
+.batch-actions {
+    display: flex;
+    gap: 10px;
+    flex-wrap: wrap;
+    margin-top: 10px;
+}
+
+.batch-actions .btn {
+    flex: 1;
+    min-width: 100px;
+}
+
+/* 统一按钮样式 */
+.generate-btn,
+.timezone-convert-btn,
+.db-generate-btn {
+    margin-top: 15px;
+    padding: 14px 24px;
+    font-size: 16px;
+    font-weight: 600;
+    border-radius: 10px;
+    border: none;
+    background: linear-gradient(135deg, #007bff, #0056b3);
+    color: white;
+    cursor: pointer;
+    transition: all 0.3s ease;
+    box-shadow: 0 3px 12px rgba(0, 123, 255, 0.25);
+    text-align: center;
+    width: 100%;
+}
+
+.generate-btn:hover,
+.timezone-convert-btn:hover,
+.db-generate-btn:hover {
+    background: linear-gradient(135deg, #0056b3, #004085);
+    box-shadow: 0 5px 16px rgba(0, 123, 255, 0.35);
+    transform: translateY(-2px);
+}
+
+/* 表单控件统一样式 */
+.panel-section .form-control {
+    padding: 12px 16px;
+    border: 2px solid #e9ecef;
+    border-radius: 8px;
+    font-size: 15px;
+    transition: all 0.3s ease;
+    background-color: #ffffff;
+    width: 100%;
+    box-sizing: border-box;
+    min-height: 50px;
+}
+
+/* input输入框统一100%宽度 */
+.panel-section input.form-control {
+    width: 100% !important;
+}
+
+/* select下拉框默认宽度调整 */
+.panel-section select.form-control {
+    width: 100% !important;
+}
+
+.panel-section .form-control:focus {
+    border-color: #007bff;
+    box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
+    outline: none;
+}
+
+/* select下拉框特殊样式 */
+.panel-section select.form-control {
+    appearance: none;
+    -webkit-appearance: none;
+    -moz-appearance: none;
+    background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3E%3C/svg%3E");
+    background-repeat: no-repeat;
+    background-position: right 12px center;
+    background-size: 16px 12px;
+    padding-right: 40px;
+    cursor: pointer;
+}
+
+.panel-section select.form-control:focus {
+    background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='%23007bff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3E%3C/svg%3E");
+}
+
+/* select选项样式 */
+.panel-section select.form-control option {
+    padding: 8px 12px;
+    background-color: #ffffff;
+    color: #343a40;
+    font-size: 15px;
+}
+
+/* 输入标签样式优化 */
+.input-label {
+    font-size: 16px;
+    font-weight: 700;
+    color: #343a40;
+    margin-bottom: 15px;
+    display: flex;
+    align-items: center;
+    gap: 8px;
+}
+
+.label-icon {
+    font-size: 18px;
+}
+
+/* 面板内间距优化 */
+.panel-section {
+    padding: 25px;
+}
+
+/* 时区和数据库特殊布局优化 */
+.timezone-input-group .time-input-section {
+    width: 100%;
+    margin-bottom: 12px;
+}
+
+.timezone-input-group .form-control {
+    width: 100%;
+    box-sizing: border-box;
+}
+
+/* 单行布局:基准时间 + 操作 + 数量 + 单位 */
+.single-row-group {
+    display: flex;
+    gap: 12px;
+    align-items: center;
+    flex-wrap: wrap;
+}
+
+.base-time-group {
+    flex: 2;
+    min-width: 200px;
+}
+
+.operation-select-group {
+    flex: 0 0 120px;
+}
+
+.amount-group {
+    flex: 0 0 100px;
+}
+
+.unit-group {
+    flex: 0 0 120px;
+}
+
+/* 时间加减运算按钮样式,与时间差计算按钮保持一致 */
+.calc-add-btn {
+    width: 100%;
+    padding: 12px;
+    font-size: 16px;
+    font-weight: 600;
+    border-radius: 8px;
+    border: none;
+    background: linear-gradient(135deg, #28a745, #20c997);
+    color: white;
+    cursor: pointer;
+    transition: all 0.3s ease;
+    box-shadow: 0 2px 8px rgba(40, 167, 69, 0.3);
+}
+
+.calc-add-btn:hover {
+    background: linear-gradient(135deg, #218838, #1ea085);
+    box-shadow: 0 4px 12px rgba(40, 167, 69, 0.4);
+    transform: translateY(-1px);
+}
+
+.operation-group {
+    display: flex;
+    gap: 8px;
+    align-items: center;
+    flex-wrap: wrap;
+}
+
+.operation-select, .amount-input, .unit-select {
+    border-radius: 6px;
+    border: 2px solid #cbd5e0;
+}
+
+.calc-add-btn {
+    white-space: nowrap;
+    padding: 8px 16px;
+    border-radius: 6px;
+    font-weight: 500;
+}
+
+.calc-results, .add-subtract-results {
+    padding: 15px;
+    background: #f7fafc;
+    border-radius: 8px;
+    border: 2px solid #e2e8f0;
+    min-height: 60px;
+}
+
+/* 代码生成器样式 */
+
+.code-block {
+    margin-bottom: 20px;
+    border: 1px solid #e9ecef;
+    border-radius: 8px;
+    overflow: hidden;
+}
+
+.code-header {
+    background: #f8f9fa;
+    padding: 10px 15px;
+    border-bottom: 1px solid #e9ecef;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+}
+
+.code-lang {
+    font-weight: 600;
+    color: #495057;
+}
+
+.code-content {
+    background: #2d3748;
+    color: #e2e8f0;
+    margin: 0;
+    padding: 15px;
+    border-radius: 0;
+    font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
+    font-size: 13px;
+    line-height: 1.4;
+    word-wrap: break-word;
+    white-space: pre-wrap;
+}
+
+/* 时间计算器样式 */
+.calc-section {
+    background: white;
+    border: 1px solid #e9ecef;
+    border-radius: 8px;
+    padding: 20px;
+    margin-bottom: 20px;
+}
+
+.calc-section h5 {
+    color: #495057;
+    font-weight: 600;
+    margin-bottom: 15px;
+    padding-bottom: 10px;
+    border-bottom: 1px solid #e9ecef;
+}
+
+.result-display {
+    margin-top: 20px;
+    padding: 15px;
+    background: #f8f9fa;
+    border-radius: 6px;
+}
+
+.human-readable {
+    margin-top: 15px;
+    padding: 10px;
+    background: white;
+    border-left: 4px solid #28a745;
+    color: #495057;
+}
+
+/* 批量转换器样式 */
+.batch-controls {
+    margin-top: 15px;
+}
+
+.batch-controls .btn {
+    margin-right: 10px;
+    margin-bottom: 5px;
+}
+
+.batch-results {
+    background: #f8f9fa;
+    border-radius: 8px;
+    padding: 15px;
+}
+
+.result-stats {
+    color: #495057;
+}
+
+.result-stats .badge {
+    margin-right: 10px;
+    padding: 5px 10px;
+}
+
+.results-table-container {
+    background: white;
+    border-radius: 6px;
+    overflow: hidden;
+}
+
+.table {
+    margin: 0;
+    font-size: 13px;
+}
+
+.original-value,
+.converted-value {
+    max-width: 200px;
+    word-wrap: break-word;
+    white-space: normal;
+}
+
+.converted-value {
+    cursor: pointer;
+    color: #007bff;
+}
+
+.converted-value:hover {
+    background: #e3f2fd;
+}
+
+/* 时区专家样式 */
+.timezone-converter {
+    margin-bottom: 30px;
+}
+
+.timezone-result {
+    margin-top: 20px;
+    padding: 15px;
+    background: #e8f5e8;
+    border-radius: 6px;
+}
+
+.world-clock {
+    margin-top: 30px;
+}
+
+.clock-item {
+    background: white;
+    border: 1px solid #e9ecef;
+    border-radius: 8px;
+    padding: 15px;
+    text-align: center;
+    margin-bottom: 15px;
+    transition: transform 0.2s ease;
+}
+
+.clock-item:hover {
+    transform: translateY(-2px);
+    box-shadow: 0 4px 10px rgba(0,0,0,0.1);
+}
+
+.clock-city {
+    font-weight: 600;
+    color: #495057;
+    margin-bottom: 5px;
+}
+
+.clock-time {
+    font-size: 18px;
+    font-weight: 700;
+    color: #007bff;
+    margin-bottom: 5px;
+}
+
+.clock-timezone {
+    font-size: 12px;
+    color: #6c757d;
+}
+
+/* 数据库工具样式 */
+.db-results {
+}
+
+.db-format-item {
+    margin-bottom: 15px;
+}
+
+.sql-examples {
+    margin-top: 20px;
+}
+
+.sql-example {
+    margin-bottom: 20px;
+    border: 1px solid #e9ecef;
+    border-radius: 8px;
+    overflow: hidden;
+}
+
+.sql-header {
+    background: #f8f9fa;
+    padding: 10px 15px;
+    border-bottom: 1px solid #e9ecef;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    font-weight: 500;
+}
+
+.sql-content {
+    background: #2d3748;
+    color: #e2e8f0;
+    margin: 0;
+    padding: 15px;
+    font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
+    font-size: 13px;
+    line-height: 1.4;
+    word-wrap: break-word;
+    white-space: pre-wrap;
+}
+
+/* 底部面板样式 */
+.bottom-panel {
+    background: white;
+    border-top: 1px solid #e9ecef;
+    padding: 20px 30px;
+    margin-top: 30px;
+}
+
+.bottom-panel h5 {
+    color: #495057;
+    font-weight: 600;
+    margin-bottom: 15px;
+    padding-bottom: 8px;
+    border-bottom: 1px solid #e9ecef;
+}
+
+/* 历史记录样式 */
+.history-list {
+}
+
+.history-item {
+    padding: 8px 12px;
+    border: 1px solid #e9ecef;
+    border-radius: 4px;
+    margin-bottom: 5px;
+    cursor: pointer;
+    transition: background 0.2s ease;
+    font-size: 13px;
+}
+
+.history-item:hover {
+    background: #f8f9fa;
+}
+
+.history-time {
+    color: #6c757d;
+    font-size: 11px;
+    margin-right: 10px;
+}
+
+.history-action {
+    color: #007bff;
+    font-weight: 500;
+    margin-right: 10px;
+}
+
+.history-value {
+    color: #495057;
+}
+
+/* 快捷操作样式 */
+.quick-actions .btn {
+    margin-right: 8px;
+    margin-bottom: 8px;
+    font-size: 13px;
+}
+
+/* 响应式设计 */
+@media (max-width: 1024px) and (min-width: 769px) {
+    .current-time-grid {
+        grid-template-columns: 1fr;
+        gap: 12px;
+    }
+    
+    .time-item .time-display {
+        font-size: 14px;
+        padding: 10px;
+    }
+    
+    .time-item label {
+        font-size: 13px;
+        text-align: left;
+    }
+}
+
+@media (max-width: 768px) {
+    .tab-content {
+        padding: 15px;
+    }
+    
+    .nav-tabs {
+        padding: 5px 10px 0;
+    }
+    
+    .nav-tabs > li > a {
+        padding: 8px 12px;
+        font-size: 13px;
+    }
+    
+    .section-title {
+        font-size: 20px;
+    }
+    
+    .current-time-display {
+        padding: 15px;
+    }
+    
+    .bottom-panel {
+        padding: 15px;
+    }
+    
+    /* 智能解析器响应式布局 */
+    .parser-main-layout {
+        flex-direction: column;
+        gap: 20px;
+    }
+    
+    .left-panel, .right-panel {
+        flex: none;
+    }
+    
+    .quick-buttons-grid {
+        grid-template-columns: repeat(3, 1fr);
+        gap: 8px;
+    }
+    
+    .quick-btn {
+        font-size: 12px;
+        padding: 6px 8px;
+    }
+    
+    .input-group-custom {
+        flex-direction: column;
+    }
+    
+    .input-actions {
+        flex-direction: row;
+        justify-content: space-between;
+    }
+    
+    .current-time-grid {
+        grid-template-columns: 1fr;
+        gap: 10px;
+    }
+
+    .calculator-main-layout {
+        flex-direction: column;
+        gap: 20px;
+    }
+
+    .left-calc-panel, .right-calc-panel {
+        flex: none;
+    }
+}
+
+/* 动画效果 */
+.fade-enter-active, .fade-leave-active {
+    transition: opacity 0.3s ease;
+}
+
+.fade-enter, .fade-leave-to {
+    opacity: 0;
+}
+
+/* 自定义滚动条 */
+
+
+/* 表单增强样式 */
+.form-control:focus {
+    border-color: #007bff;
+    box-shadow: 0 0 0 2px rgba(0,123,255,0.25);
+}
+
+.btn-primary {
+    background: linear-gradient(135deg, #007bff 0%, #0056b3 100%);
+    border: none;
+    font-weight: 500;
+    transition: all 0.3s ease;
+}
+
+.btn-primary:hover {
+    background: linear-gradient(135deg, #0056b3 0%, #004085 100%);
+    transform: translateY(-1px);
+    box-shadow: 0 4px 8px rgba(0,123,255,0.3);
+}
+
+/* Toast 提示样式 */
+#fehelper_alertmsg {
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    z-index: 1000000;
+    background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
+    display: inline-block;
+    color: #fff;
+    text-align: center;
+    padding: 12px 20px;
+    margin: 0 auto;
+    font-size: 14px;
+    font-weight: 500;
+    border-bottom: 1px solid rgba(255,255,255,0.2);
+    box-shadow: 0 2px 10px rgba(0,0,0,0.1);
+}
+
+/* 批量转换器增强样式 */
+.batch-container {
+    margin-top: 25px;
+}
+
+.batch-input-group {
+    display: flex;
+    gap: 15px;
+}
+
+.batch-input {
+    flex: 1;
+    border-radius: 8px;
+    border: 2px solid #cbd5e0;
+    font-family: 'Monaco', 'Consolas', monospace;
+    font-size: 13px;
+}
+
+.batch-actions {
+    display: flex;
+    flex-direction: column;
+    gap: 8px;
+    min-width: 130px;
+}
+
+.batch-actions .btn {
+    white-space: nowrap;
+    border-radius: 6px;
+    font-weight: 500;
+    font-size: 13px;
+}
+
+/* 时区转换器增强样式 */
+.timezone-container {
+    margin-top: 25px;
+}
+
+.timezone-input-group {
+    width: 100%;
+    box-sizing: border-box;
+}
+
+.timezone-input-group .time-input-section {
+    width: 100%;
+    margin-bottom: 12px;
+}
+
+.timezone-input-group .form-control {
+    width: 100%;
+    box-sizing: border-box;
+}
+
+/* 并排布局,两个select各占50% */
+.timezone-selectors {
+    display: flex;
+    gap: 12px;
+    width: 100%;
+}
+
+.timezone-selectors .from-timezone,
+.timezone-selectors .to-timezone {
+    flex: 1 1 0;
+    min-width: 0;
+    display: flex;
+    flex-direction: column;
+}
+
+.timezone-selectors .form-control {
+    width: 100%;
+    min-width: 0;
+    box-sizing: border-box;
+}
+
+.timezone-selectors .form-label {
+    font-size: 14px;
+    font-weight: 600;
+    color: #495057;
+    margin-bottom: 5px;
+}
+
+.timezone-arrow {
+    color: #4a5568;
+    font-size: 16px;
+    padding: 0 5px;
+}
+
+.from-timezone-select, .to-timezone-select {
+    border-radius: 6px;
+    border: 2px solid #cbd5e0;
+    font-size: 13px;
+}
+
+.timezone-convert-btn {
+    white-space: nowrap;
+    padding: 8px 16px;
+    border-radius: 6px;
+    font-weight: 500;
+}
+
+.timezone-results {
+    padding: 15px;
+    background: #f7fafc;
+    border-radius: 8px;
+    border: 2px solid #e2e8f0;
+    min-height: 60px;
+}
+
+/* 数据库工具增强样式 */
+.database-container {
+    margin-top: 25px;
+}
+
+.db-input-group {
+    display: flex;
+    gap: 15px;
+    align-items: center;
+    flex-wrap: wrap;
+    margin-bottom: 20px;
+}
+
+.time-db-input {
+    flex: 2;
+    min-width: 200px;
+}
+
+.db-time-input {
+    border-radius: 8px;
+    border: 2px solid #cbd5e0;
+}
+
+.db-type-selector {
+    flex: 1;
+    min-width: 150px;
+}
+
+.db-type-select {
+    border-radius: 6px;
+    border: 2px solid #cbd5e0;
+}
+
+.db-generate-btn {
+    white-space: nowrap;
+    padding: 8px 16px;
+    border-radius: 6px;
+    font-weight: 500;
+}
+
+.db-results {
+    border: 2px solid #e2e8f0;
+    border-radius: 8px;
+    padding: 15px;
+    background: #f7fafc;
+}

+ 474 - 0
apps/datetime-calc/index.html

@@ -0,0 +1,474 @@
+<!DOCTYPE HTML>
+<html lang="zh-CN">
+    <head>
+        <title>时间戳计算器 - FeHelper</title>
+        <meta charset="UTF-8">
+        <meta name="viewport" content="width=device-width, initial-scale=1.0">
+        <link rel="shortcut icon" href="../static/img/favicon.ico">
+        <link rel="stylesheet" href="index.css" />
+    </head>
+    <body>
+
+    <div class="wrapper" id="app">
+        <div class="panel panel-default" style="margin-bottom: 0px;">
+            <div class="panel-heading">
+                <h3 class="panel-title">
+                    <a href="https://fehelper.com" target="_blank" class="x-a-high">
+                        <img src="../static/img/fe-16.png" alt="fehelper"/> FeHelper</a>:时间戳计算器
+                    <a href="#" class="x-donate-link"><i class="nav-icon">❤&nbsp;</i>打赏鼓励</a>
+                    <a class="x-other-tools"><i class="icon-plus-circle"></i> 探索更多实用工具 <span class="tool-market-badge">工具市场</span></a>
+                </h3>
+            </div>
+        </div>
+
+        <!-- 功能标签页 -->
+        <div class="tab-container">
+            <ul class="nav nav-tabs" role="tablist">
+                <li role="presentation" class="active">
+                    <a href="#">🧠 智能解析</a>
+                </li>
+                <li role="presentation">
+                    <a href="#">💻 代码生成</a>
+                </li>
+                <li role="presentation">
+                    <a href="#">🧮 时间计算</a>
+                </li>
+                <li role="presentation">
+                    <a href="#">📊 批量转换</a>
+                </li>
+                <li role="presentation">
+                    <a href="#">🌍 时区专家</a>
+                </li>
+                <li role="presentation">
+                    <a href="#">🗄️ 数据库</a>
+                </li>
+            </ul>
+
+            <!-- 标签页内容 -->
+            <div class="tab-content">
+                <!-- 智能解析模块 -->
+                <div role="tabpanel" class="tab-pane active" id="smart-parser">
+                    <div class="tool-section">
+                        <h4 class="section-title">🧠 智能时间解析器</h4>
+                        <p class="section-desc">支持20+种时间格式自动识别,包括Unix时间戳、ISO 8601、RFC 3339、数据库格式等</p>
+                        
+                        <div class="parser-main-layout">
+                            <!-- 左侧区域 -->
+                            <div class="left-panel">
+                                <!-- 快捷操作模块 -->
+                                <div class="quick-actions-module">
+                                    <label class="module-label">
+                                        <span class="label-icon">⚡</span>
+                                        快捷操作
+                                    </label>
+                                    <div class="quick-buttons-grid">
+                                        <button class="btn btn-outline-primary btn-sm quick-btn" data-action="now">
+                                            🕐 当前时间
+                                        </button>
+                                        <button class="btn btn-outline-success btn-sm quick-btn" data-action="today">
+                                            📅 今天开始
+                                        </button>
+                                        <button class="btn btn-outline-info btn-sm quick-btn" data-action="yesterday">
+                                            📆 昨天
+                                        </button>
+                                        <button class="btn btn-outline-warning btn-sm quick-btn" data-action="week_start">
+                                            🗓️ 本周开始
+                                        </button>
+                                        <button class="btn btn-outline-secondary btn-sm quick-btn" data-action="month_start">
+                                            📊 本月开始
+                                        </button>
+                                    </div>
+                                </div>
+                                
+                                <!-- 输入时间模块 -->
+                                <div class="input-time-module">
+                                    <label class="module-label">
+                                        <span class="label-icon">📝</span>
+                                        输入时间(支持多种格式)
+                                    </label>
+                                    <div class="input-group-custom">
+                                        <textarea class="form-control smart-input" 
+                                                 placeholder="试试输入:&#10;• 1749722690(时间戳)&#10;• 2025-06-12 18:06:25&#10;• now / today / yesterday&#10;• 2025/06/12&#10;• Jun 12, 2025"
+                                                 rows="6"></textarea>
+                                        <div class="input-actions">
+                                            <button class="btn btn-primary parse-btn">🔍 解析</button>
+                                            <button class="btn btn-secondary clear-input-btn">🗑️ 清空</button>
+                                        </div>
+                                    </div>
+                                    <div class="format-hints"></div>
+                                </div>
+                            </div>
+                            
+                            <!-- 右侧区域 -->
+                            <div class="right-panel">
+                                <!-- 当前时间模块 -->
+                                <div class="current-time-module">
+                                    <label class="module-label">
+                                        <span class="label-icon">🕒</span>
+                                        当前时间
+                                        <div class="time-control-inline">
+                                            <button class="btn btn-sm btn-warning time-toggle-btn">⏸️ 暂停</button>
+                                        </div>
+                                    </label>
+                                    <div class="current-time-grid">
+                                        <div class="time-item">
+                                            <label>当前本地时间</label>
+                                            <input type="text" class="form-control time-display" readonly title="点击复制">
+                                        </div>
+                                        <div class="time-item">
+                                            <label>Unix时间戳(秒)</label>
+                                            <input type="text" class="form-control time-display" readonly title="点击复制">
+                                        </div>
+                                        <div class="time-item">
+                                            <label>Unix时间戳(毫秒)</label>
+                                            <input type="text" class="form-control time-display" readonly title="点击复制">
+                                        </div>
+                                    </div>
+                                </div>
+                                
+                                <!-- 解析结果模块 -->
+                                <div class="parse-results-module">
+                                    <label class="module-label">
+                                        <span class="label-icon">📋</span>
+                                        解析结果
+                                    </label>
+                                    <div class="result-container"></div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+
+                <!-- 代码生成模块 -->
+                <div role="tabpanel" class="tab-pane" id="code-generator" style="display: none;">
+                    <div class="tool-section">
+                        <h4 class="section-title">💻 多语言代码生成器</h4>
+                        <p class="section-desc">根据时间值生成各种编程语言的时间处理代码</p>
+                        
+                        <div class="main-layout">
+                            <!-- 左侧:输入和控制 -->
+                            <div class="left-panel">
+                                <div class="panel-section">
+                                    <label class="input-label">
+                                        <span class="label-icon">⚡</span>
+                                        时间输入与语言选择
+                                    </label>
+                                    <div class="input-group">
+                                        <div class="time-input-section">
+                                            <input type="text" class="form-control time-input" placeholder="输入时间戳或时间字符串(如:1699999999)">
+                                        </div>
+                                        <div class="language-selector-section">
+                                            <label class="form-label">选择语言</label>
+                                            <select class="form-control language-select">
+                                                <option value="all">所有语言</option>
+                                                <option value="javascript">JavaScript</option>
+                                                <option value="python">Python</option>
+                                                <option value="java">Java</option>
+                                                <option value="go">Go</option>
+                                                <option value="php">PHP</option>
+                                                <option value="sql">SQL</option>
+                                            </select>
+                                        </div>
+                                        <button class="btn btn-primary generate-btn">🔨 生成代码</button>
+                                    </div>
+                                </div>
+                            </div>
+                            
+                            <!-- 右侧:结果显示 -->
+                            <div class="right-panel">
+                                <div class="panel-section">
+                                    <label class="result-label">
+                                        <span class="label-icon">💻</span>
+                                        生成的代码(点击代码块可复制)
+                                    </label>
+                                    <div class="code-results"></div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+
+                <!-- 时间计算模块 -->
+                <div role="tabpanel" class="tab-pane" id="time-calculator" style="display: none;">
+                    <div class="tool-section">
+                        <h4 class="section-title">🧮 时间计算器</h4>
+                        <p class="section-desc">进行时间差计算、时间加减运算等复杂时间计算</p>
+                        
+                        <div class="calculator-container">
+                            <div class="calculator-main-layout">
+                                <!-- 左侧:时间差计算 -->
+                                <div class="left-calc-panel">
+                                    <div class="calc-section">
+                                        <label class="input-label">
+                                            <span class="label-icon">⏰</span>
+                                            时间差计算
+                                        </label>
+                                        <div class="calc-input-group">
+                                            <div class="time-inputs">
+                                                <div class="time-input-item">
+                                                    <span class="input-prefix">从</span>
+                                                    <input type="text" class="form-control start-time" placeholder="开始时间(如:2024-01-01 10:00:00)">
+                                                </div>
+                                                <div class="calc-separator">到</div>
+                                                <div class="time-input-item">
+                                                    <input type="text" class="form-control end-time" placeholder="结束时间(如:2024-01-02 15:30:00)">
+                                                </div>
+                                            </div>
+                                            <button class="btn btn-primary calc-btn">📊 计算时间差</button>
+                                        </div>
+                                        <div class="calc-results"></div>
+                                    </div>
+                                </div>
+                                
+                                <!-- 右侧:时间加减运算 -->
+                                <div class="right-calc-panel">
+                                    <div class="calc-section">
+                                        <label class="input-label">
+                                            <span class="label-icon">➕➖</span>
+                                            时间加减运算
+                                        </label>
+                                        <div class="calc-input-group">
+                                            <!-- 单行布局:基准时间 + 操作 + 数量 + 单位 -->
+                                            <div class="single-row-group">
+                                                <div class="base-time-group">
+                                                    <input type="text" class="form-control base-time" placeholder="基准时间(如:2024-01-01 10:00:00)">
+                                                </div>
+                                                <div class="operation-select-group">
+                                                    <select class="form-control operation-select">
+                                                        <option value="add">➕ 增加</option>
+                                                        <option value="subtract">➖ 减去</option>
+                                                    </select>
+                                                </div>
+                                                <div class="amount-group">
+                                                    <input type="number" class="form-control amount-input" placeholder="数量">
+                                                </div>
+                                                <div class="unit-group">
+                                                    <select class="form-control unit-select">
+                                                        <option value="seconds">秒</option>
+                                                        <option value="minutes">分钟</option>
+                                                        <option value="hours">小时</option>
+                                                        <option value="days">天</option>
+                                                        <option value="months">月</option>
+                                                        <option value="years">年</option>
+                                                    </select>
+                                                </div>
+                                            </div>
+                                            
+                                            <button class="btn btn-success calc-add-btn">🧮 计算结果</button>
+                                        </div>
+                                        <div class="calc-results add-subtract-results"></div>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+
+                <!-- 批量转换模块 -->
+                <div role="tabpanel" class="tab-pane" id="batch-converter" style="display: none;">
+                    <div class="tool-section">
+                        <h4 class="section-title">📊 批量转换器</h4>
+                        <p class="section-desc">批量处理时间数据</p>
+                        
+                        <div class="main-layout">
+                            <!-- 左侧:输入和控制 -->
+                            <div class="left-panel">
+                                <div class="panel-section">
+                                    <label class="input-label">
+                                        <span class="label-icon">📝</span>
+                                        批量时间输入(每行一个)
+                                    </label>
+                                    <div class="batch-input-group">
+                                        <textarea class="form-control batch-input" rows="10" 
+                                                 placeholder="输入多个时间值,每行一个:&#10;1699999999&#10;2024-01-15 10:30:45&#10;now&#10;today&#10;2024/01/15"></textarea>
+                                        <div class="batch-actions">
+                                            <button class="btn btn-primary batch-convert-btn">🔄 批量转换</button>
+                                            <button class="btn btn-secondary batch-clear-btn">🗑️ 清空</button>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                            
+                            <!-- 右侧:结果显示 -->
+                            <div class="right-panel">
+                                <div class="panel-section">
+                                    <label class="result-label">
+                                        <span class="label-icon">📊</span>
+                                        转换结果
+                                        <span class="result-stats"></span>
+                                    </label>
+                                    <div class="batch-results"></div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+
+                <!-- 时区专家模块 -->
+                <div role="tabpanel" class="tab-pane" id="timezone-expert" style="display: none;">
+                    <div class="tool-section">
+                        <h4 class="section-title">🌍 时区转换专家</h4>
+                        <p class="section-desc">精确的时区转换,支持夏令时和历史时区数据</p>
+                        
+                        <div class="main-layout">
+                            <!-- 左侧:输入和控制 -->
+                            <div class="left-panel">
+                                <div class="panel-section">
+                                    <label class="input-label">
+                                        <span class="label-icon">🌍</span>
+                                        时区转换设置
+                                    </label>
+                                    <div class="timezone-input-group">
+                                        <div class="time-input-section">
+                                            <label class="form-label">输入时间</label>
+                                            <input type="text" class="form-control timezone-time-input" placeholder="输入时间(如:2024-01-15 10:30:00)">
+                                        </div>
+                                        <div class="timezone-selectors">
+                                            <div class="from-timezone">
+                                                <label class="form-label">从时区</label>
+                                                <select class="form-control from-timezone-select">
+                                                    <option value="Pacific/Midway">🇺🇸 中途岛 (GMT-11)</option>
+                                                    <option value="America/Adak">🇺🇸 阿达克 (GMT-10)</option>
+                                                    <option value="Pacific/Honolulu">🇺🇸 夏威夷 (GMT-10)</option>
+                                                    <option value="America/Anchorage">🇺🇸 安克雷奇 (GMT-9)</option>
+                                                    <option value="America/Los_Angeles">🇺🇸 洛杉矶 (PST, GMT-8)</option>
+                                                    <option value="America/Denver">🇺🇸 丹佛 (MST, GMT-7)</option>
+                                                    <option value="America/Chicago">🇺🇸 芝加哥 (CST, GMT-6)</option>
+                                                    <option value="America/New_York">🇺🇸 纽约 (EST, GMT-5)</option>
+                                                    <option value="America/Caracas">🇻🇪 加拉加斯 (GMT-4)</option>
+                                                    <option value="America/Santiago">🇨🇱 圣地亚哥 (GMT-4)</option>
+                                                    <option value="America/Halifax">🇨🇦 哈利法克斯 (GMT-4)</option>
+                                                    <option value="America/St_Johns">🇨🇦 圣约翰斯 (GMT-3:30)</option>
+                                                    <option value="America/Argentina/Buenos_Aires">🇦🇷 布宜诺斯艾利斯 (GMT-3)</option>
+                                                    <option value="America/Sao_Paulo">🇧🇷 圣保罗 (GMT-3)</option>
+                                                    <option value="Atlantic/Azores">🇵🇹 亚速尔群岛 (GMT-1)</option>
+                                                    <option value="Europe/London">🇬🇧 伦敦 (GMT+0)</option>
+                                                    <option value="Europe/Berlin">🇩🇪 柏林 (CET, GMT+1)</option>
+                                                    <option value="Europe/Paris">🇫🇷 巴黎 (CET, GMT+1)</option>
+                                                    <option value="Europe/Athens">🇬🇷 雅典 (EET, GMT+2)</option>
+                                                    <option value="Europe/Istanbul">🇹🇷 伊斯坦布尔 (GMT+3)</option>
+                                                    <option value="Europe/Moscow">🇷🇺 莫斯科 (GMT+3)</option>
+                                                    <option value="Asia/Dubai">🇦🇪 迪拜 (GMT+4)</option>
+                                                    <option value="Asia/Karachi">🇵🇰 卡拉奇 (GMT+5)</option>
+                                                    <option value="Asia/Dhaka">🇧🇩 达卡 (GMT+6)</option>
+                                                    <option value="Asia/Bangkok">🇹🇭 曼谷 (GMT+7)</option>
+                                                    <option value="Asia/Shanghai">🇨🇳 北京 (GMT+8)</option>
+                                                    <option value="Asia/Tokyo">🇯🇵 东京 (GMT+9)</option>
+                                                    <option value="Australia/Sydney">🇦🇺 悉尼 (GMT+10)</option>
+                                                    <option value="Pacific/Auckland">🇳🇿 奥克兰 (GMT+12)</option>
+                                                </select>
+                                            </div>
+                                            <div class="to-timezone">
+                                                <label class="form-label">到时区</label>
+                                                <select class="form-control to-timezone-select">
+                                                    <option value="Pacific/Midway">🇺🇸 中途岛 (GMT-11)</option>
+                                                    <option value="America/Adak">🇺🇸 阿达克 (GMT-10)</option>
+                                                    <option value="Pacific/Honolulu">🇺🇸 夏威夷 (GMT-10)</option>
+                                                    <option value="America/Anchorage">🇺🇸 安克雷奇 (GMT-9)</option>
+                                                    <option value="America/Los_Angeles">🇺🇸 洛杉矶 (PST, GMT-8)</option>
+                                                    <option value="America/Denver">🇺🇸 丹佛 (MST, GMT-7)</option>
+                                                    <option value="America/Chicago">🇺🇸 芝加哥 (CST, GMT-6)</option>
+                                                    <option value="America/New_York">🇺🇸 纽约 (EST, GMT-5)</option>
+                                                    <option value="America/Caracas">🇻🇪 加拉加斯 (GMT-4)</option>
+                                                    <option value="America/Santiago">🇨🇱 圣地亚哥 (GMT-4)</option>
+                                                    <option value="America/Halifax">🇨🇦 哈利法克斯 (GMT-4)</option>
+                                                    <option value="America/St_Johns">🇨🇦 圣约翰斯 (GMT-3:30)</option>
+                                                    <option value="America/Argentina/Buenos_Aires">🇦🇷 布宜诺斯艾利斯 (GMT-3)</option>
+                                                    <option value="America/Sao_Paulo">🇧🇷 圣保罗 (GMT-3)</option>
+                                                    <option value="Atlantic/Azores">🇵🇹 亚速尔群岛 (GMT-1)</option>
+                                                    <option value="Europe/London">🇬🇧 伦敦 (GMT+0)</option>
+                                                    <option value="Europe/Berlin">🇩🇪 柏林 (CET, GMT+1)</option>
+                                                    <option value="Europe/Paris">🇫🇷 巴黎 (CET, GMT+1)</option>
+                                                    <option value="Europe/Athens">🇬🇷 雅典 (EET, GMT+2)</option>
+                                                    <option value="Europe/Istanbul">🇹🇷 伊斯坦布尔 (GMT+3)</option>
+                                                    <option value="Europe/Moscow">🇷🇺 莫斯科 (GMT+3)</option>
+                                                    <option value="Asia/Dubai">🇦🇪 迪拜 (GMT+4)</option>
+                                                    <option value="Asia/Karachi">🇵🇰 卡拉奇 (GMT+5)</option>
+                                                    <option value="Asia/Dhaka">🇧🇩 达卡 (GMT+6)</option>
+                                                    <option value="Asia/Bangkok">🇹🇭 曼谷 (GMT+7)</option>
+                                                    <option value="Asia/Shanghai">🇨🇳 北京 (GMT+8)</option>
+                                                    <option value="Asia/Tokyo">🇯🇵 东京 (GMT+9)</option>
+                                                    <option value="Australia/Sydney">🇦🇺 悉尼 (GMT+10)</option>
+                                                    <option value="Pacific/Auckland">🇳🇿 奥克兰 (GMT+12)</option>
+                                                </select>
+                                            </div>
+                                        </div>
+                                        <button class="btn btn-primary timezone-convert-btn">🔄 转换时区</button>
+                                    </div>
+                                </div>
+                            </div>
+                            
+                            <!-- 右侧:结果显示 -->
+                            <div class="right-panel">
+                                <div class="panel-section">
+                                    <label class="result-label">
+                                        <span class="label-icon">⏰</span>
+                                        转换结果
+                                    </label>
+                                    <div class="timezone-results"></div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+
+                <!-- 数据库工具模块 -->
+                <div role="tabpanel" class="tab-pane" id="database-tools" style="display: none;">
+                    <div class="tool-section">
+                        <h4 class="section-title">🗄️ 数据库时间工具</h4>
+                        <p class="section-desc">生成各种数据库的时间格式和SQL查询语句</p>
+                        
+                        <div class="main-layout">
+                            <!-- 左侧:输入和控制 -->
+                            <div class="left-panel">
+                                <div class="panel-section">
+                                    <label class="input-label">
+                                        <span class="label-icon">⚙️</span>
+                                        数据库配置
+                                    </label>
+                                    <div class="db-input-group">
+                                        <div class="time-db-input">
+                                            <label class="form-label">输入时间值</label>
+                                            <input type="text" class="form-control db-time-input" placeholder="输入时间值(如:1699999999 或 2024-01-15 10:30:00)">
+                                        </div>
+                                        <div class="db-type-selector">
+                                            <label class="form-label">选择数据库类型</label>
+                                            <select class="form-control db-type-select">
+                                                <option value="mysql">🐬 MySQL</option>
+                                                <option value="postgresql">🐘 PostgreSQL</option>
+                                                <option value="sqlite">📱 SQLite</option>
+                                                <option value="mongodb">🍃 MongoDB</option>
+                                            </select>
+                                        </div>
+                                        <button class="btn btn-primary db-generate-btn">🔧 生成格式</button>
+                                    </div>
+                                </div>
+                            </div>
+                            
+                            <!-- 右侧:结果显示 -->
+                            <div class="right-panel">
+                                <div class="panel-section">
+                                    <label class="result-label">
+                                        <span class="label-icon">📄</span>
+                                        数据库格式与SQL语句
+                                    </label>
+                                    <div class="db-results"></div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+
+    </div>
+
+    <script type="text/javascript" src="../static/vendor/evalCore.min.js"></script>
+    <!-- 引入所需的JavaScript库 -->
+    <script type="text/javascript" src="../static/vendor/jquery/jquery-3.3.1.min.js"></script>
+    <script type="text/javascript" src="index.js"></script>
+
+    </body>
+</html>
+

+ 1152 - 0
apps/datetime-calc/index.js

@@ -0,0 +1,1152 @@
+/**
+ * FeHelper - 专业时间戳工具
+ * 使用原生JavaScript实现,无需Vue依赖,兼容Chrome扩展CSP策略
+ */
+
+// 全局状态管理
+var AppState = {
+    // 当前活跃的标签页
+    activeTab: 'smart-parser',
+    
+    // 当前时间显示
+    currentTime: {
+        local: '',
+        timestamp: 0,
+        timestampMs: 0
+    },
+    isTimeRunning: true,
+    
+    // 智能时间解析器
+    smartParser: {
+        input: '',
+        results: [],
+        error: '',
+        detectedFormat: ''
+    },
+    
+    // 代码生成器
+    codeGenerator: {
+        input: '',
+        codes: [],
+        selectedLang: 'all'
+    },
+    
+    // 时间计算器
+    calculator: {
+        startTime: '',
+        endTime: '',
+        difference: null
+    },
+    
+    // 批量转换器
+    batchConverter: {
+        input: '',
+        results: []
+    },
+    
+    // 时区转换
+    timezoneExpert: {
+        inputTime: '',
+        fromTimezone: 'Asia/Shanghai',
+        toTimezone: 'America/New_York',
+        result: null
+    },
+    
+    // 数据库工具
+    dbTools: {
+        inputTime: '',
+        dbType: 'mysql',
+        formats: []
+    }
+};
+
+// 工具类 - 简易时间处理
+var TimeUtils = {
+    // 解析时间输入
+    parseTimeInput: function(input) {
+        if (!input || !input.trim()) {
+            throw new Error('请输入时间值');
+        }
+        
+        input = input.trim();
+        
+        // Unix时间戳(秒) - 10位数字
+        if (/^\d{10}$/.test(input)) {
+            return {
+                timestamp: parseInt(input) * 1000,
+                format: 'Unix时间戳(秒)'
+            };
+        }
+        
+        // Unix时间戳(毫秒) - 13位数字  
+        if (/^\d{13}$/.test(input)) {
+            return {
+                timestamp: parseInt(input),
+                format: 'Unix时间戳(毫秒)'
+            };
+        }
+        
+        // 特殊关键字
+        var now = new Date();
+        if (input.toLowerCase() === 'now') {
+            return {
+                timestamp: now.getTime(),
+                format: '当前时间(now)'
+            };
+        }
+        
+        if (input.toLowerCase() === 'today') {
+            var today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
+            return {
+                timestamp: today.getTime(),
+                format: '今天开始时间(today)'
+            };
+        }
+        
+        if (input.toLowerCase() === 'yesterday') {
+            var yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
+            return {
+                timestamp: yesterday.getTime(),
+                format: '昨天开始时间(yesterday)'
+            };
+        }
+        
+        // 尝试解析为日期字符串
+        var date = new Date(input);
+        if (!isNaN(date.getTime())) {
+            return {
+                timestamp: date.getTime(),
+                format: '日期字符串'
+            };
+        }
+        
+        throw new Error('无法识别的时间格式');
+    },
+    
+    // 格式化时间戳为各种格式
+    formatTimestamp: function(timestamp) {
+        var date = new Date(timestamp);
+        
+        return [
+            { label: '标准格式', value: this.formatDate(date, 'YYYY-MM-DD HH:mm:ss') },
+            { label: 'Unix时间戳(秒)', value: Math.floor(timestamp / 1000).toString() },
+            { label: 'Unix时间戳(毫秒)', value: timestamp.toString() },
+            { label: 'UTC时间', value: this.formatDate(new Date(date.getTime() + date.getTimezoneOffset() * 60000), 'YYYY-MM-DD HH:mm:ss') + ' UTC' },
+            { label: '本地格式', value: date.toLocaleString('zh-CN') },
+            { label: '相对时间', value: this.getRelativeTime(date) },
+            { label: 'ISO 8601', value: date.toISOString() }
+        ];
+    },
+    
+    // 格式化日期
+    formatDate: function(date, format) {
+        var year = date.getFullYear();
+        var month = (date.getMonth() + 1).toString().padStart(2, '0');
+        var day = date.getDate().toString().padStart(2, '0');
+        var hour = date.getHours().toString().padStart(2, '0');
+        var minute = date.getMinutes().toString().padStart(2, '0');
+        var second = date.getSeconds().toString().padStart(2, '0');
+        
+        return format
+            .replace('YYYY', year)
+            .replace('MM', month)
+            .replace('DD', day)
+            .replace('HH', hour)
+            .replace('mm', minute)
+            .replace('ss', second);
+    },
+    
+    // 获取相对时间
+    getRelativeTime: function(date) {
+        var now = new Date();
+        var diff = now.getTime() - date.getTime();
+        var seconds = Math.floor(diff / 1000);
+        var minutes = Math.floor(seconds / 60);
+        var hours = Math.floor(minutes / 60);
+        var days = Math.floor(hours / 24);
+        
+        if (days > 0) {
+            return days + '天前';
+        } else if (hours > 0) {
+            return hours + '小时前';
+        } else if (minutes > 0) {
+            return minutes + '分钟前';
+        } else if (seconds > 0) {
+            return seconds + '秒前';
+        } else {
+            return '刚刚';
+        }
+    },
+    
+    // 生成各种语言代码
+    generateCode: function(input, lang) {
+        var parsed = this.parseTimeInput(input);
+        var timestamp = Math.floor(parsed.timestamp / 1000); // 转为秒
+        
+        var codes = {
+            javascript: 'var date = new Date(' + parsed.timestamp + ');\nconsole.log(date.toISOString());\n// 输出: ' + new Date(parsed.timestamp).toISOString(),
+            
+            python: 'import datetime\nfrom datetime import timezone\n\ntimestamp = ' + timestamp + '\ndate = datetime.datetime.fromtimestamp(timestamp, timezone.utc)\nprint(date.isoformat())\n# 输出: ' + new Date(parsed.timestamp).toISOString(),
+            
+            java: 'import java.time.Instant;\nimport java.time.ZoneId;\nimport java.time.format.DateTimeFormatter;\n\nInstant instant = Instant.ofEpochSecond(' + timestamp + ');\nString formatted = instant.atZone(ZoneId.systemDefault())\n    .format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);\nSystem.out.println(formatted);',
+            
+            go: 'package main\n\nimport (\n    "fmt"\n    "time"\n)\n\nfunc main() {\n    timestamp := int64(' + timestamp + ')\n    t := time.Unix(timestamp, 0)\n    fmt.Println(t.Format(time.RFC3339))\n    // 输出: ' + new Date(parsed.timestamp).toISOString() + '\n}',
+            
+            php: '<?php\n$timestamp = ' + timestamp + ';\n$date = new DateTime("@$timestamp");\necho $date->format("c");\n// 输出: ' + new Date(parsed.timestamp).toISOString() + '\n?>',
+            
+            sql: '-- MySQL\nSELECT FROM_UNIXTIME(' + timestamp + ') AS formatted_date;\n\n-- PostgreSQL\nSELECT to_timestamp(' + timestamp + ') AS formatted_date;\n\n-- SQLite\nSELECT datetime(' + timestamp + ', "unixepoch") AS formatted_date;'
+        };
+        
+        return codes[lang] || '不支持的语言';
+    }
+};
+
+// DOM操作工具
+var DOMUtils = {
+    // 查找元素
+    $: function(selector) {
+        return document.querySelector(selector);
+    },
+    
+    // 查找所有元素
+    $$: function(selector) {
+        return document.querySelectorAll(selector);
+    },
+    
+    // 设置元素内容
+    setText: function(element, text) {
+        if (element) {
+            element.textContent = text;
+        }
+    },
+    
+    // 设置元素值
+    setValue: function(element, value) {
+        if (element) {
+            element.value = value;
+        }
+    },
+    
+    // 设置元素HTML
+    setHTML: function(element, html) {
+        if (element) {
+            element.innerHTML = html;
+        }
+    },
+    
+    // 添加类
+    addClass: function(element, className) {
+        if (element) {
+            element.classList.add(className);
+        }
+    },
+    
+    // 移除类
+    removeClass: function(element, className) {
+        if (element) {
+            element.classList.remove(className);
+        }
+    },
+    
+    // 切换类
+    toggleClass: function(element, className) {
+        if (element) {
+            element.classList.toggle(className);
+        }
+    },
+    
+    // 显示元素
+    show: function(element) {
+        if (element) {
+            element.style.display = '';
+        }
+    },
+    
+    // 隐藏元素
+    hide: function(element) {
+        if (element) {
+            element.style.display = 'none';
+        }
+    }
+};
+
+// 应用主类
+var TimestampApp = {
+    // 初始化
+    init: function() {
+        console.log('初始化时间戳工具...');
+        
+        // 移除Vue相关的HTML属性
+        this.cleanupVueAttributes();
+        
+        // 初始化事件监听器
+        this.initEventListeners();
+        
+        // 初始化界面
+        this.initUI();
+        
+        // 启动时间更新
+        this.startTimeUpdates();
+        
+        console.log('时间戳工具初始化完成');
+    },
+    
+    // 清理Vue属性
+    cleanupVueAttributes: function() {
+        // 移除所有Vue相关的属性 - 直接遍历所有元素
+        var allElements = document.querySelectorAll('*');
+        allElements.forEach(function(el) {
+            // 移除Vue指令属性
+            var attributes = el.attributes;
+            for (var i = attributes.length - 1; i >= 0; i--) {
+                var attr = attributes[i];
+                if (attr.name.startsWith('v-') || attr.name.startsWith(':') || attr.name.startsWith('@')) {
+                    el.removeAttribute(attr.name);
+                }
+            }
+        });
+    },
+    
+    // 初始化事件监听器
+    initEventListeners: function() {
+        var self = this;
+        
+        // 标签页切换
+        var tabLinks = DOMUtils.$$('.nav-tabs a');
+        tabLinks.forEach(function(link, index) {
+            link.addEventListener('click', function(e) {
+                e.preventDefault();
+                var tabName = ['smart-parser', 'code-generator', 'time-calculator', 'batch-converter', 'timezone-expert', 'database-tools'][index];
+                self.setActiveTab(tabName);
+            });
+        });
+        
+        // 时间控制按钮
+        var timeToggleBtn = DOMUtils.$('.time-toggle-btn');
+        if (timeToggleBtn) {
+            timeToggleBtn.addEventListener('click', function(e) {
+                e.preventDefault();
+                self.toggleTime();
+            });
+        }
+        
+        // 智能解析输入
+        var smartInput = DOMUtils.$('.smart-input');
+        if (smartInput) {
+            smartInput.addEventListener('input', function() {
+                AppState.smartParser.input = this.value;
+                self.parseSmartTime();
+            });
+        }
+        
+        // 代码生成输入
+        var codeInput = DOMUtils.$('.time-input');
+        if (codeInput) {
+            codeInput.addEventListener('input', function() {
+                AppState.codeGenerator.input = this.value;
+                self.generateCodes();
+            });
+        }
+        
+        // 代码语言选择
+        var codeLangSelect = DOMUtils.$('.language-selector select');
+        if (codeLangSelect) {
+            codeLangSelect.addEventListener('change', function() {
+                AppState.codeGenerator.selectedLang = this.value;
+                self.updateCodeDisplay();
+            });
+        }
+        
+        // 时间显示点击复制
+        var timeDisplays = DOMUtils.$$('.time-display');
+        timeDisplays.forEach(function(display) {
+            display.addEventListener('click', function() {
+                self.copyToClipboard(this.value);
+            });
+        });
+        
+        // 快捷操作按钮
+        var quickButtons = DOMUtils.$$('.quick-buttons .btn');
+        quickButtons.forEach(function(btn, index) {
+            btn.addEventListener('click', function(e) {
+                e.preventDefault();
+                var actions = ['now', 'today', 'yesterday', 'week_start', 'month_start', 'clear'];
+                self.handleQuickAction(actions[index]);
+            });
+        });
+        
+        // 智能解析器按钮
+        var parseBtn = DOMUtils.$('.parse-btn');
+        if (parseBtn) {
+            parseBtn.addEventListener('click', function(e) {
+                e.preventDefault();
+                self.parseSmartTime();
+            });
+        }
+        
+        var clearInputBtn = DOMUtils.$('.clear-input-btn');
+        if (clearInputBtn) {
+            clearInputBtn.addEventListener('click', function(e) {
+                e.preventDefault();
+                var smartInput = DOMUtils.$('.smart-input');
+                if (smartInput) {
+                    smartInput.value = '';
+                    AppState.smartParser.input = '';
+                    self.parseSmartTime();
+                }
+            });
+        }
+        
+        // 新的快捷操作按钮
+        var quickBtns = DOMUtils.$$('.quick-btn');
+        quickBtns.forEach(function(btn) {
+            btn.addEventListener('click', function(e) {
+                e.preventDefault();
+                var action = this.getAttribute('data-action');
+                self.handleQuickAction(action);
+            });
+        });
+        
+        // 代码生成器按钮
+        var generateBtn = DOMUtils.$('.generate-btn');
+        if (generateBtn) {
+            generateBtn.addEventListener('click', function(e) {
+                e.preventDefault();
+                self.generateCodes();
+            });
+        }
+        
+        // 时间计算器按钮
+        var calcBtn = DOMUtils.$('.calc-btn');
+        if (calcBtn) {
+            calcBtn.addEventListener('click', function(e) {
+                e.preventDefault();
+                self.calculateTimeDiff();
+            });
+        }
+        
+        var calcAddBtn = DOMUtils.$('.calc-add-btn');
+        if (calcAddBtn) {
+            calcAddBtn.addEventListener('click', function(e) {
+                e.preventDefault();
+                self.calculateTimeAddSubtract();
+            });
+        }
+        
+        // 批量转换器按钮
+        var batchConvertBtn = DOMUtils.$('.batch-convert-btn');
+        if (batchConvertBtn) {
+            batchConvertBtn.addEventListener('click', function(e) {
+                e.preventDefault();
+                self.batchConvert();
+            });
+        }
+        
+        var batchExportBtn = DOMUtils.$('.batch-export-btn');
+        if (batchExportBtn) {
+            batchExportBtn.addEventListener('click', function(e) {
+                e.preventDefault();
+                self.exportBatchResults();
+            });
+        }
+        
+        var batchClearBtn = DOMUtils.$('.batch-clear-btn');
+        if (batchClearBtn) {
+            batchClearBtn.addEventListener('click', function(e) {
+                e.preventDefault();
+                var batchInput = DOMUtils.$('.batch-input');
+                if (batchInput) {
+                    batchInput.value = '';
+                }
+            });
+        }
+        
+        // 时区转换器按钮
+        var timezoneConvertBtn = DOMUtils.$('.timezone-convert-btn');
+        if (timezoneConvertBtn) {
+            timezoneConvertBtn.addEventListener('click', function(e) {
+                e.preventDefault();
+                self.convertTimezone();
+            });
+        }
+        
+        // 数据库工具按钮
+        var dbGenerateBtn = DOMUtils.$('.db-generate-btn');
+        if (dbGenerateBtn) {
+            dbGenerateBtn.addEventListener('click', function(e) {
+                e.preventDefault();
+                self.generateDatabaseFormats();
+            });
+        }
+    },
+    
+    // 初始化UI
+    initUI: function() {
+        // 设置初始标签页
+        this.setActiveTab(AppState.activeTab);
+        
+        // 初始化时间显示
+        this.updateTimeDisplay();
+    },
+    
+    // 设置活跃标签页
+    setActiveTab: function(tabName) {
+        AppState.activeTab = tabName;
+        
+        // 更新标签链接样式
+        var tabLinks = DOMUtils.$$('.nav-tabs li');
+        var tabPanes = DOMUtils.$$('.tab-pane');
+        
+        tabLinks.forEach(function(link, index) {
+            var tabNames = ['smart-parser', 'code-generator', 'time-calculator', 'batch-converter', 'timezone-expert', 'database-tools'];
+            if (tabNames[index] === tabName) {
+                DOMUtils.addClass(link, 'active');
+            } else {
+                DOMUtils.removeClass(link, 'active');
+            }
+        });
+        
+        // 更新标签页内容显示
+        tabPanes.forEach(function(pane) {
+            if (pane.id === tabName) {
+                DOMUtils.addClass(pane, 'active');
+                DOMUtils.show(pane);
+            } else {
+                DOMUtils.removeClass(pane, 'active');
+                DOMUtils.hide(pane);
+            }
+        });
+    },
+    
+    // 启动时间更新
+    startTimeUpdates: function() {
+        var self = this;
+        
+        function updateTime() {
+            var now = new Date();
+            AppState.currentTime.local = TimeUtils.formatDate(now, 'YYYY-MM-DD HH:mm:ss');
+            AppState.currentTime.timestamp = Math.floor(now.getTime() / 1000);
+            AppState.currentTime.timestampMs = now.getTime();
+            
+            self.updateTimeDisplay();
+        }
+        
+        updateTime();
+        setInterval(function() {
+            if (AppState.isTimeRunning) {
+                updateTime();
+            }
+        }, 1000);
+    },
+    
+    // 更新时间显示
+    updateTimeDisplay: function() {
+        var timeInputs = DOMUtils.$$('.time-display');
+        if (timeInputs.length >= 3) {
+            DOMUtils.setValue(timeInputs[0], AppState.currentTime.local);
+            DOMUtils.setValue(timeInputs[1], AppState.currentTime.timestamp);
+            DOMUtils.setValue(timeInputs[2], AppState.currentTime.timestampMs);
+        }
+    },
+    
+    // 切换时间运行状态
+    toggleTime: function() {
+        AppState.isTimeRunning = !AppState.isTimeRunning;
+        var toggleBtn = DOMUtils.$('.time-toggle-btn');
+        if (toggleBtn) {
+            DOMUtils.setText(toggleBtn, AppState.isTimeRunning ? '⏸️ 暂停' : '▶️ 开始');
+            toggleBtn.className = AppState.isTimeRunning ? 'btn btn-sm btn-warning time-toggle-btn' : 'btn btn-sm btn-success time-toggle-btn';
+        }
+    },
+    
+    // 智能解析时间
+    parseSmartTime: function() {
+        var resultContainer = DOMUtils.$('.result-container');
+        var formatHints = DOMUtils.$('.format-hints');
+        
+        if (!AppState.smartParser.input.trim()) {
+            DOMUtils.setHTML(resultContainer, '');
+            DOMUtils.setHTML(formatHints, '');
+            return;
+        }
+        
+        try {
+            var parsed = TimeUtils.parseTimeInput(AppState.smartParser.input);
+            var results = TimeUtils.formatTimestamp(parsed.timestamp);
+            
+            AppState.smartParser.results = results;
+            AppState.smartParser.detectedFormat = parsed.format;
+            AppState.smartParser.error = '';
+            
+            // 显示检测到的格式
+            DOMUtils.setHTML(formatHints, '<span class="badge badge-info">检测到格式: ' + parsed.format + '</span>');
+            
+            // 显示解析结果 - 使用网格布局
+            var html = '<div class="parse-results-grid">';
+            results.forEach(function(result, index) {
+                var isIsoResult = result.label === 'ISO 8601';
+                var resultClass = isIsoResult ? 'result-item iso-result' : 'result-item';
+                
+                html += '<div class="' + resultClass + '">' +
+                    '<label>' + result.label + '</label>' +
+                    '<div class="result-value" onclick="TimestampApp.copyToClipboard(\'' + result.value.replace(/'/g, "\\'") + '\')" title="点击复制">' + 
+                    result.value + 
+                    '</div>' +
+                    '</div>';
+            });
+            html += '</div>';
+            DOMUtils.setHTML(resultContainer, html);
+            
+        } catch (error) {
+            AppState.smartParser.error = error.message;
+            AppState.smartParser.results = [];
+            AppState.smartParser.detectedFormat = '';
+            
+            DOMUtils.setHTML(resultContainer, '<div class="alert alert-danger">❌ ' + error.message + '</div>');
+            DOMUtils.setHTML(formatHints, '');
+        }
+    },
+    
+    // 生成代码
+    generateCodes: function() {
+        if (!AppState.codeGenerator.input.trim()) {
+            AppState.codeGenerator.codes = [];
+            this.updateCodeDisplay();
+            return;
+        }
+        
+        try {
+            var languages = ['javascript', 'python', 'java', 'go', 'php', 'sql'];
+            AppState.codeGenerator.codes = languages.map(function(lang) {
+                return {
+                    lang: lang.charAt(0).toUpperCase() + lang.slice(1),
+                    code: TimeUtils.generateCode(AppState.codeGenerator.input, lang)
+                };
+            });
+            
+            this.updateCodeDisplay();
+            
+        } catch (error) {
+            AppState.codeGenerator.codes = [{
+                lang: 'Error',
+                code: '代码生成失败: ' + error.message
+            }];
+            this.updateCodeDisplay();
+        }
+    },
+    
+    // 更新代码显示
+    updateCodeDisplay: function() {
+        var codeResults = DOMUtils.$('.code-results');
+        if (!codeResults) return;
+        
+        // 过滤代码
+        var filteredCodes = AppState.codeGenerator.codes;
+        if (AppState.codeGenerator.selectedLang !== 'all') {
+            filteredCodes = AppState.codeGenerator.codes.filter(function(code) {
+                return code.lang.toLowerCase().includes(AppState.codeGenerator.selectedLang.toLowerCase());
+            });
+        }
+        
+        // 显示代码
+        var html = '';
+        filteredCodes.forEach(function(code) {
+            html += '<div class="code-block">' +
+                '<div class="code-header">' +
+                '<span class="code-lang">' + code.lang + '</span>' +
+                '<button class="btn btn-xs btn-default" onclick="TimestampApp.copyToClipboard(\'' + code.code.replace(/'/g, "\\'").replace(/\n/g, '\\n') + '\')">📋 复制</button>' +
+                '</div>' +
+                '<pre class="code-content">' + code.code + '</pre>' +
+                '</div>';
+        });
+        DOMUtils.setHTML(codeResults, html);
+    },
+    
+    // 复制到剪贴板
+    copyToClipboard: function(text) {
+        try {
+            if (navigator.clipboard && navigator.clipboard.writeText) {
+                navigator.clipboard.writeText(text).then(function() {
+                    TimestampApp.showToast('已复制到剪贴板');
+                }).catch(function(error) {
+                    console.error('复制失败:', error);
+                    TimestampApp.fallbackCopy(text);
+                });
+            } else {
+                TimestampApp.fallbackCopy(text);
+            }
+        } catch (error) {
+            console.error('复制失败:', error);
+            TimestampApp.fallbackCopy(text);
+        }
+    },
+    
+    // 备用复制方法
+    fallbackCopy: function(text) {
+        var textarea = document.createElement('textarea');
+        textarea.value = text;
+        textarea.style.position = 'fixed';
+        textarea.style.opacity = '0';
+        document.body.appendChild(textarea);
+        textarea.select();
+        
+        try {
+            var successful = document.execCommand('copy');
+            if (successful) {
+                this.showToast('已复制到剪贴板');
+            } else {
+                this.showToast('复制失败');
+            }
+        } catch (error) {
+            console.error('复制失败:', error);
+            this.showToast('复制失败');
+        }
+        
+        document.body.removeChild(textarea);
+    },
+    
+    // 快捷操作处理
+    handleQuickAction: function(action) {
+        var smartInput = DOMUtils.$('.smart-input');
+        if (!smartInput) return;
+        
+        var value = '';
+        var now = new Date();
+        
+        switch(action) {
+            case 'now':
+                value = 'now';
+                break;
+            case 'today':
+                value = 'today';
+                break;
+            case 'yesterday':
+                value = 'yesterday';
+                break;
+            case 'week_start':
+                var weekStart = new Date(now);
+                weekStart.setDate(now.getDate() - now.getDay());
+                weekStart.setHours(0, 0, 0, 0);
+                value = TimeUtils.formatDate(weekStart, 'YYYY-MM-DD HH:mm:ss');
+                break;
+            case 'month_start':
+                var monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
+                value = TimeUtils.formatDate(monthStart, 'YYYY-MM-DD HH:mm:ss');
+                break;
+            case 'clear':
+                value = '';
+                // 清空所有输入框
+                var allInputs = DOMUtils.$$('input[type="text"], textarea');
+                allInputs.forEach(function(input) {
+                    if (!input.readOnly) {
+                        input.value = '';
+                    }
+                });
+                this.showToast('已清空所有输入');
+                return;
+        }
+        
+        smartInput.value = value;
+        AppState.smartParser.input = value;
+        this.parseSmartTime();
+        
+        if (value) {
+            this.showToast('已插入: ' + value);
+        }
+    },
+    
+    // 显示提示信息
+    showToast: function(message) {
+        // 移除已存在的toast
+        var existingToast = DOMUtils.$('.toast-message');
+        if (existingToast) {
+            document.body.removeChild(existingToast);
+        }
+        
+        var toast = document.createElement('div');
+        toast.className = 'toast-message';
+        toast.textContent = message;
+        toast.style.cssText = 'position:fixed;top:20px;right:20px;background:#333;color:#fff;padding:10px 20px;border-radius:4px;z-index:9999;transition:opacity 0.3s;';
+        document.body.appendChild(toast);
+        
+        setTimeout(function() {
+            toast.style.opacity = '0';
+            setTimeout(function() {
+                if (toast.parentNode) {
+                    document.body.removeChild(toast);
+                }
+            }, 300);
+        }, 2000);
+    },
+    
+    // 计算时间差
+    calculateTimeDiff: function() {
+        var startInput = DOMUtils.$('.start-time');
+        var endInput = DOMUtils.$('.end-time');
+        var resultsDiv = DOMUtils.$('.calc-results');
+        
+        if (!startInput || !endInput || !resultsDiv) return;
+        
+        var startTime = startInput.value.trim();
+        var endTime = endInput.value.trim();
+        
+        if (!startTime || !endTime) {
+            DOMUtils.setHTML(resultsDiv, '<div class="text-warning">请输入开始时间和结束时间</div>');
+            return;
+        }
+        
+        try {
+            var start = TimeUtils.parseTimeInput(startTime);
+            var end = TimeUtils.parseTimeInput(endTime);
+            var diff = Math.floor((end.timestamp - start.timestamp) / 1000); // 转换为秒
+            
+            var html = '<div class="result-item">';
+            html += '<strong>时间差:</strong><br>';
+            html += '秒数:' + diff + ' 秒<br>';
+            html += '分钟:' + Math.floor(diff / 60) + ' 分钟<br>';
+            html += '小时:' + Math.floor(diff / 3600) + ' 小时<br>';
+            html += '天数:' + Math.floor(diff / 86400) + ' 天<br>';
+            html += '</div>';
+            
+            DOMUtils.setHTML(resultsDiv, html);
+        } catch (error) {
+            DOMUtils.setHTML(resultsDiv, '<div class="text-danger">错误:' + error.message + '</div>');
+        }
+    },
+    
+    // 计算时间加减
+    calculateTimeAddSubtract: function() {
+        var baseTimeInput = DOMUtils.$('.base-time');
+        var operationSelect = DOMUtils.$('.operation-select');
+        var amountInput = DOMUtils.$('.amount-input');
+        var unitSelect = DOMUtils.$('.unit-select');
+        var resultsDiv = DOMUtils.$('.add-subtract-results');
+        
+        if (!baseTimeInput || !operationSelect || !amountInput || !unitSelect || !resultsDiv) return;
+        
+        var baseTime = baseTimeInput.value.trim();
+        var operation = operationSelect.value;
+        var amount = parseInt(amountInput.value);
+        var unit = unitSelect.value;
+        
+        if (!baseTime || isNaN(amount)) {
+            DOMUtils.setHTML(resultsDiv, '<div class="text-warning">请输入基准时间和数量</div>');
+            return;
+        }
+        
+        try {
+            var base = TimeUtils.parseTimeInput(baseTime);
+            var timestamp = base.timestamp;
+            
+            var multiplier = {
+                'seconds': 1000,
+                'minutes': 60 * 1000,
+                'hours': 3600 * 1000,
+                'days': 86400 * 1000,
+                'months': 30 * 86400 * 1000,
+                'years': 365 * 86400 * 1000
+            };
+            
+            var change = amount * multiplier[unit];
+            if (operation === 'subtract') {
+                change = -change;
+            }
+            
+            var newTimestamp = timestamp + change;
+            var results = TimeUtils.formatTimestamp(newTimestamp);
+            
+            var html = '<div class="result-item">';
+            html += '<strong>计算结果:</strong><br>';
+            results.forEach(function(result) {
+                html += result.label + ':' + result.value + '<br>';
+            });
+            html += '</div>';
+            
+            DOMUtils.setHTML(resultsDiv, html);
+        } catch (error) {
+            DOMUtils.setHTML(resultsDiv, '<div class="text-danger">错误:' + error.message + '</div>');
+        }
+    },
+    
+    // 批量转换
+    batchConvert: function() {
+        var batchInput = DOMUtils.$('.batch-input');
+        var resultsDiv = DOMUtils.$('.batch-results');
+        var statsSpan = DOMUtils.$('.result-stats');
+        
+        if (!batchInput || !resultsDiv) return;
+        
+        var lines = batchInput.value.split('\n').filter(function(line) {
+            return line.trim();
+        });
+        
+        if (lines.length === 0) {
+            DOMUtils.setHTML(resultsDiv, '<div class="text-warning">请输入要转换的时间值</div>');
+            return;
+        }
+        
+        var results = [];
+        var successCount = 0;
+        var errorCount = 0;
+        
+        lines.forEach(function(line, index) {
+            try {
+                var parsed = TimeUtils.parseTimeInput(line.trim());
+                var formatted = TimeUtils.formatTimestamp(parsed.timestamp);
+                results.push({
+                    line: index + 1,
+                    input: line.trim(),
+                    success: true,
+                    results: formatted
+                });
+                successCount++;
+            } catch (error) {
+                results.push({
+                    line: index + 1,
+                    input: line.trim(),
+                    success: false,
+                    error: error.message
+                });
+                errorCount++;
+            }
+        });
+        
+        // 更新统计信息
+        if (statsSpan) {
+            DOMUtils.setHTML(statsSpan, '(成功:' + successCount + ',失败:' + errorCount + ')');
+        }
+        
+        // 显示结果
+        var html = '';
+        results.forEach(function(result) {
+            if (result.success) {
+                html += '<div class="mb-3">';
+                html += '<strong>第' + result.line + '行:</strong> ' + result.input + '<br>';
+                result.results.forEach(function(format) {
+                    html += '• ' + format.label + ':' + format.value + '<br>';
+                });
+                html += '</div>';
+            } else {
+                html += '<div class="mb-3 text-danger">';
+                html += '<strong>第' + result.line + '行错误:</strong> ' + result.input + '<br>';
+                html += '错误:' + result.error + '<br>';
+                html += '</div>';
+            }
+        });
+        
+        DOMUtils.setHTML(resultsDiv, html);
+    },
+    
+    // 导出批量结果
+    exportBatchResults: function() {
+        this.showToast('导出功能开发中...');
+    },
+    
+    // 时区转换
+    convertTimezone: function() {
+        var timeInput = DOMUtils.$('.timezone-time-input');
+        var fromSelect = DOMUtils.$('.from-timezone-select');
+        var toSelect = DOMUtils.$('.to-timezone-select');
+        var resultsDiv = DOMUtils.$('.timezone-results');
+        
+        if (!timeInput || !fromSelect || !toSelect || !resultsDiv) return;
+        
+        var timeValue = timeInput.value.trim();
+        var fromTimezone = fromSelect.value;
+        var toTimezone = toSelect.value;
+        
+        if (!timeValue) {
+            DOMUtils.setHTML(resultsDiv, '<div class="text-warning">请输入时间</div>');
+            return;
+        }
+        
+        try {
+            // 1. 解析为UTC时间戳
+            var utcTimestamp = getUTCTimestampFromLocal(timeValue, fromTimezone);
+
+            // 2. 用Intl.DateTimeFormat格式化为目标时区的本地时间
+            var dt = new Date(utcTimestamp);
+            var fmt = new Intl.DateTimeFormat('zh-CN', {
+                timeZone: toTimezone,
+                year: 'numeric', month: '2-digit', day: '2-digit',
+                hour: '2-digit', minute: '2-digit', second: '2-digit',
+                hour12: false
+            });
+            var parts = fmt.formatToParts(dt);
+            var get = t => parts.find(p => p.type === t).value;
+            var targetStr = `${get('year')}-${get('month')}-${get('day')} ${get('hour')}:${get('minute')}:${get('second')}`;
+
+            var html = '<div class="result-item">';
+            html += '<strong>时区转换结果:</strong><br>';
+            html += '原时间:' + timeValue + ' (' + fromTimezone + ')<br>';
+            html += '目标时区:' + toTimezone + '<br>';
+            html += '转换结果:' + targetStr + '<br>';
+            html += '</div>';
+
+            DOMUtils.setHTML(resultsDiv, html);
+        } catch (error) {
+            DOMUtils.setHTML(resultsDiv, '<div class="text-danger">错误:' + error.message + '</div>');
+        }
+    },
+    
+    // 生成数据库格式
+    generateDatabaseFormats: function() {
+        var timeInput = DOMUtils.$('.db-time-input');
+        var dbSelect = DOMUtils.$('.db-type-select');
+        var resultsDiv = DOMUtils.$('.db-results');
+        
+        if (!timeInput || !dbSelect || !resultsDiv) return;
+        
+        var timeValue = timeInput.value.trim();
+        var dbType = dbSelect.value;
+        
+        if (!timeValue) {
+            DOMUtils.setHTML(resultsDiv, '<div class="text-warning">请输入时间值</div>');
+            return;
+        }
+        
+        try {
+            var parsed = TimeUtils.parseTimeInput(timeValue);
+            var date = new Date(parsed.timestamp);
+            
+            var html = '<div class="result-item">';
+            html += '<strong>' + dbType.toUpperCase() + ' 格式:</strong><br>';
+            
+            switch (dbType) {
+                case 'mysql':
+                    html += 'DATETIME:' + TimeUtils.formatDate(date, 'YYYY-MM-DD HH:mm:ss') + '<br>';
+                    html += 'TIMESTAMP:' + Math.floor(parsed.timestamp / 1000) + '<br>';
+                    html += 'SQL示例:<br>';
+                    html += '<code>SELECT * FROM table WHERE created_at = \'' + TimeUtils.formatDate(date, 'YYYY-MM-DD HH:mm:ss') + '\';</code><br>';
+                    break;
+                case 'postgresql':
+                    html += 'TIMESTAMP:' + TimeUtils.formatDate(date, 'YYYY-MM-DD HH:mm:ss') + '<br>';
+                    html += 'TIMESTAMPTZ:' + date.toISOString() + '<br>';
+                    html += 'EPOCH:' + Math.floor(parsed.timestamp / 1000) + '<br>';
+                    break;
+                case 'sqlite':
+                    html += 'TEXT:' + TimeUtils.formatDate(date, 'YYYY-MM-DD HH:mm:ss') + '<br>';
+                    html += 'INTEGER:' + Math.floor(parsed.timestamp / 1000) + '<br>';
+                    break;
+                case 'mongodb':
+                    html += 'ISODate:ISODate("' + date.toISOString() + '")<br>';
+                    html += 'ObjectId时间戳:' + Math.floor(parsed.timestamp / 1000).toString(16).padStart(8, '0') + '0000000000000000<br>';
+                    break;
+            }
+            html += '</div>';
+            
+            DOMUtils.setHTML(resultsDiv, html);
+        } catch (error) {
+            DOMUtils.setHTML(resultsDiv, '<div class="text-danger">错误:' + error.message + '</div>');
+        }
+    }
+};
+
+function loadPatchHotfix() {
+    // 页面加载时自动获取并注入页面的补丁
+    chrome.runtime.sendMessage({
+        type: 'fh-dynamic-any-thing',
+        thing: 'fh-get-tool-patch',
+        toolName: 'datetime-calc'
+    }, patch => {
+        if (patch) {
+            if (patch.css) {
+                const style = document.createElement('style');
+                style.textContent = patch.css;
+                document.head.appendChild(style);
+            }
+            if (patch.js) {
+                try {
+                    if (window.evalCore && window.evalCore.getEvalInstance) {
+                        window.evalCore.getEvalInstance(window)(patch.js);
+                    }
+                } catch (e) {
+                    console.error('datetime-calc补丁JS执行失败', e);
+                }
+            }
+        }
+    });
+}
+
+// 页面加载完成后初始化
+document.addEventListener('DOMContentLoaded', function() {
+    TimestampApp.init();
+    // 打赏按钮点击事件
+    var donateBtn = document.querySelector('.x-donate-link');
+    if (donateBtn) {
+        donateBtn.addEventListener('click', function(e) {
+            e.preventDefault();
+            e.stopPropagation();
+            chrome.runtime.sendMessage({
+                type: 'fh-dynamic-any-thing',
+                thing: 'open-donate-modal',
+                params: { toolName: 'datetime-calc' }
+            });
+        });
+    }
+    // 更多工具按钮点击事件
+    var moreToolsBtn = document.querySelector('.x-other-tools');
+    if (moreToolsBtn) {
+        moreToolsBtn.addEventListener('click', function(e) {
+            e.preventDefault();
+            e.stopPropagation();
+            chrome.runtime.openOptionsPage();
+        });
+    }
+    loadPatchHotfix();
+});
+
+// 全局暴露主要对象(用于调试)
+window.TimestampApp = TimestampApp;
+window.AppState = AppState;
+window.TimeUtils = TimeUtils;
+
+// === 新增:更准确的原生JS IANA时区转换辅助函数 ===
+function getUTCTimestampFromLocal(timeStr, tz) {
+    // 只支持 yyyy-MM-dd HH:mm:ss
+    var m = timeStr.match(/^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})$/);
+    if (!m) throw new Error('请输入格式为 yyyy-MM-dd HH:mm:ss 的时间');
+    var y = Number(m[1]), mon = Number(m[2]), d = Number(m[3]), h = Number(m[4]), min = Number(m[5]), s = Number(m[6]);
+    // 构造一个"源时区"下的本地时间的UTC时间戳
+    // 1. 先用Date.UTC得到UTC时间戳
+    var utcGuess = Date.UTC(y, mon - 1, d, h, min, s);
+    // 2. 用Intl.DateTimeFormat格式化utcGuess为源时区的本地时间
+    var fmt = new Intl.DateTimeFormat('en-US', {
+        timeZone: tz,
+        year: 'numeric', month: '2-digit', day: '2-digit',
+        hour: '2-digit', minute: '2-digit', second: '2-digit',
+        hour12: false
+    });
+    var parts = fmt.formatToParts(new Date(utcGuess));
+    var get = t => parts.find(p => p.type === t).value;
+    var localStr = `${get('year')}-${get('month')}-${get('day')} ${get('hour')}:${get('minute')}:${get('second')}`;
+    // 3. 计算本地时间和输入时间的差值(毫秒)
+    var input = Date.UTC(y, mon - 1, d, h, min, s);
+    var local = Date.UTC(
+        Number(get('year')),
+        Number(get('month')) - 1,
+        Number(get('day')),
+        Number(get('hour')),
+        Number(get('minute')),
+        Number(get('second'))
+    );
+    var diff = input - local;
+    // 4. 用utcGuess + diff 得到正确的UTC时间戳
+    return utcGuess + diff;
+}
+
+// 事件委托:解析结果区域点击复制
+(function() {
+    document.addEventListener('DOMContentLoaded', function() {
+        var parseResultsModule = document.querySelector('.parse-results-module');
+        if (parseResultsModule) {
+            parseResultsModule.addEventListener('click', function(e) {
+                var target = e.target;
+                if (target.classList.contains('result-value')) {
+                    var text = target.textContent;
+                    TimestampApp.copyToClipboard(text);
+                }
+            });
+        }
+    });
+})(); 

+ 1 - 1
apps/devtools/index.html

@@ -13,7 +13,7 @@
             <div class="panel panel-default" style="margin-bottom: 0px;">
                 <div class="panel-heading">
                     <h3 class="panel-title">
-                        <a href="https://www.baidufe.com/fehelper/index/index.html" target="_blank" class="x-a-high">
+                        <a href="https://fehelper.com" target="_blank" class="x-a-high">
                             <img src="../static/img/fe-16.png" alt="fehelper"/> FeHelper</a>:开发者工具
                     </h3>
                 </div>

+ 97 - 0
apps/edge.json

@@ -0,0 +1,97 @@
+{
+  "name": "FeHelper(前端助手)-Dev",
+  "short_name": "FeHelper",
+  "version": "2025.6.2901",
+  "manifest_version": 3,
+  "description": "JSON自动格式化、手动格式化,支持排序、解码、下载等,更多功能可在配置页按需安装!",
+  "icons": {
+    "16": "static/img/fe-16.png",
+    "48": "static/img/fe-48.png",
+    "128": "static/img/fe-128.png"
+  },
+  "action": {
+    "default_icon": "static/img/fe-16.png",
+    "default_title": "FeHelper(前端助手)",
+    "default_popup": "popup/index.html"
+  },
+  "background": {
+    "service_worker": "background/background.js",
+    "type": "module"
+  },
+  "options_ui": {
+    "page": "options/index.html",
+    "open_in_tab": true
+  },
+  "permissions": [
+    "tabs",
+    "scripting",
+    "contextMenus",
+    "activeTab",
+    "storage",
+    "notifications",
+    "unlimitedStorage"
+  ],
+  "host_permissions": [
+    "http://*/*",
+    "https://*/*",
+    "file://*/*"
+    ],
+  "optional_permissions": [
+    "downloads"
+  ],
+  "commands": {
+    "_execute_action": {
+      "suggested_key": {
+        "default": "Alt+Shift+J"
+      }
+    }
+  },
+  "web_accessible_resources": [
+      {
+          "resources":[
+            "static/img/fe-16.png",
+            "static/img/fe-48.png",
+            "static/img/loading.gif",
+            "json-format/format-lib.js",
+            "json-format/json-abc.js",
+            "json-format/json-bigint.js",
+            "json-format/json-decode.js",
+            "json-format/json-worker.js",
+            "static/vendor/jquery/jquery-3.3.1.min.js",
+            "static/vendor/evalCore.min.js",
+
+            "background/awesome.js",
+            "background/tools.js",
+
+            "code-beautify/beautify.js",
+            "code-beautify/beautify-css.js"
+        ],
+        "matches": ["<all_urls>"]
+      }
+  ],
+  "content_scripts": [
+    {
+      "matches": [
+        "http://*/*",
+        "https://*/*",
+        "file://*/*"
+      ],
+      "exclude_globs": [
+        "https://chrome.google.com/*"
+      ],
+      "js": [
+        "static/vendor/jquery/jquery-3.3.1.min.js",
+        "static/vendor/evalCore.min.js"
+      ],
+      "run_at": "document_start",
+      "all_frames": true
+    }
+  ],
+  "content_security_policy": {
+    "extension_pages": "script-src 'self'; style-src 'self' 'unsafe-inline'; object-src 'self'"
+  },
+  "homepage_url": "https://www.fehelper.com"
+}
+
+
+

+ 83 - 8
apps/en-decode/endecode-lib.js

@@ -172,15 +172,18 @@ let EncodeUtils = (() => {
      * @return {String} 源码
      */
     let _base64Decode = function (str) {
-          // 检测是否包含URL编码的字符,如果有则先进行URL解码
-        if (str.includes("%")) {
-          try {
+        // 首先进行URL解码处理,将%XX格式的字符转换回原始字符
+        try {
+            // 使用decodeURIComponent进行URL解码
             str = decodeURIComponent(str);
-          } catch (e) {
-            // 如果URL解码失败,继续使用原字符串
-            console.warn("URL解码失败,使用原字符串:", e.message);
-          }
+        } catch (e) {
+            // 如果decodeURIComponent失败,尝试手动替换常见的URL编码字符
+            str = str.replace(/%2B/g, '+')
+                    .replace(/%2F/g, '/')
+                    .replace(/%3D/g, '=')
+                    .replace(/%20/g, ' ');
         }
+        
         let c1, c2, c3, c4;
         let i, len, out;
         len = str.length;
@@ -532,6 +535,76 @@ let EncodeUtils = (() => {
         // 返回格式化的JSON
         return cookiesArray;
     }
+
+    /**
+     * 使用gzip压缩文本(返回Base64编码的结果)
+     * @param {string} text - 需要压缩的文本
+     * @returns {Promise<string>} 压缩后的Base64字符串
+     */
+    let _gzipEncode = async function(text) {
+        try {
+            // 检查浏览器是否支持CompressionStream API
+            if (typeof CompressionStream === 'undefined') {
+                throw new Error('当前浏览器不支持CompressionStream API,请使用Chrome 80+、Firefox 113+或Safari 16.4+');
+            }
+            
+            // 将文本转换为Uint8Array
+            const encoder = new TextEncoder();
+            const data = encoder.encode(text);
+            
+            // 创建压缩流
+            const compressionStream = new CompressionStream('gzip');
+            const compressedStream = new Response(data).body.pipeThrough(compressionStream);
+            
+            // 读取压缩后的数据
+            const compressedArrayBuffer = await new Response(compressedStream).arrayBuffer();
+            
+            // 将ArrayBuffer转换为Base64字符串
+            const compressedArray = new Uint8Array(compressedArrayBuffer);
+            let binaryString = '';
+            for (let i = 0; i < compressedArray.length; i++) {
+                binaryString += String.fromCharCode(compressedArray[i]);
+            }
+            
+            return btoa(binaryString);
+        } catch (error) {
+            throw new Error('Gzip压缩失败: ' + error.message);
+        }
+    };
+
+    /**
+     * 使用gzip解压缩Base64编码的数据
+     * @param {string} compressedBase64 - Base64编码的压缩数据
+     * @returns {Promise<string>} 解压缩后的文本
+     */
+    let _gzipDecode = async function(compressedBase64) {
+        try {
+            // 检查浏览器是否支持DecompressionStream API
+            if (typeof DecompressionStream === 'undefined') {
+                throw new Error('当前浏览器不支持DecompressionStream API,请使用Chrome 80+、Firefox 113+或Safari 16.4+');
+            }
+            
+            // 将Base64字符串转换为Uint8Array
+            const binaryString = atob(compressedBase64);
+            const compressedArray = new Uint8Array(binaryString.length);
+            for (let i = 0; i < binaryString.length; i++) {
+                compressedArray[i] = binaryString.charCodeAt(i);
+            }
+            
+            // 创建解压缩流
+            const decompressionStream = new DecompressionStream('gzip');
+            const decompressedStream = new Response(compressedArray).body.pipeThrough(decompressionStream);
+            
+            // 读取解压缩后的数据
+            const decompressedArrayBuffer = await new Response(decompressedStream).arrayBuffer();
+            
+            // 将ArrayBuffer转换为文本
+            const decoder = new TextDecoder();
+            return decoder.decode(decompressedArrayBuffer);
+        } catch (error) {
+            throw new Error('Gzip解压缩失败: ' + error.message);
+        }
+    };
     
 
     return {
@@ -550,7 +623,9 @@ let EncodeUtils = (() => {
         urlParamsDecode: _urlParamsDecode,
         sha1Encode: _sha1Encode,
         jwtDecode,
-        formatCookieStringToJson
+        formatCookieStringToJson,
+        gzipEncode: _gzipEncode,
+        gzipDecode: _gzipDecode
     };
 })();
 

+ 204 - 33
apps/en-decode/index.css

@@ -1,55 +1,226 @@
 @import url("../static/css/bootstrap.min.css");
 
+/* 整体容器样式优化 */
+.mod-endecode {
+    padding: 15px;
+    background: linear-gradient(to bottom, #ffffff, #f8f9ff);
+    border-radius: 8px;
+    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
+}
+
+/* 输入框样式优化 */
 #srcText {
     height: 100px;
+    border: 1px solid #e0e5ff;
+    border-radius: 8px;
+    padding: 12px;
+    font-size: 14px;
+    line-height: 1.5;
+    background-color: #ffffff;
+    transition: all 0.3s ease;
+    resize: vertical;
+    box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05);
+}
+
+#srcText:focus {
+    border-color: #4d89fe;
+    box-shadow: 0 0 0 3px rgba(77, 137, 254, 0.1);
+    outline: none;
 }
+
+/* 结果输出框样式优化 */
 #rstCode {
-    height: 280px;
+    height: 120px;
+    border: 1px solid #e0e5ff;
+    border-radius: 8px;
+    padding: 12px;
+    font-size: 14px;
+    line-height: 1.5;
+    background-color: #ffffff;
+    transition: all 0.3s ease;
+    resize: vertical;
+    box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05);
 }
-.ui-ml-05 {
-    margin-left: 5px;
+
+#rstCode:focus {
+    border-color: #4d89fe;
+    box-shadow: 0 0 0 3px rgba(77, 137, 254, 0.1);
+    outline: none;
+}
+
+/* 选项表格样式优化 */
+.x-opts {
+    width: 100%;
+    font-size: 14px;
+    margin: 12px 0;
+    background: #ffffff;
+    border-radius: 8px;
+    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
+}
+
+.x-opts td {
+    padding: 10px;
+    vertical-align: middle;
+}
+
+.x-opts tr:first-child {
+    border-bottom: 1px solid #eef1ff;
 }
+
+td.td-label {
+    width: 66px;
+    color: #666;
+    font-weight: 500;
+    vertical-align: middle;
+}
+
+/* 单选框组样式优化 */
+.x-opts .radio {
+    margin: 4px 0;
+    transition: all 0.3s ease;
+    display: inline-flex;
+    align-items: center;
+}
+
+.x-opts .radio label {
+    display: inline-flex;
+    align-items: center;
+    padding: 4px 8px;
+    border-radius: 6px;
+    transition: all 0.3s ease;
+    cursor: pointer;
+    margin: 0;
+    line-height: 1;
+}
+
+.x-opts .radio label:hover {
+    background-color: #f5f8ff;
+}
+
+.x-opts .radio input[type="radio"] {
+    margin: 0 4px 0 0;
+    vertical-align: middle;
+    position: relative;
+    top: -1px;
+}
+
+/* 提示文字样式 */
 .x-ps {
-    color:#bbb;
+    color: #a0a8c3;
+    font-size: 12px;
+    margin-left: 4px;
+    display: inline-flex;
+    align-items: center;
 }
-.x-url-infos ul {
-    padding:0;
-    margin:0;
+
+/* 按钮组样式优化 */
+.x-btns {
+    margin-top: 15px;
+    display: flex;
+    gap: 8px;
+    justify-content: flex-end;
 }
-.x-url-infos ul li {
-    list-style: none;
+
+.x-btns .btn {
+    padding: 6px 16px;
     font-size: 14px;
-    line-height: 24px;
-    padding:0;
-    margin:0;
+    border-radius: 6px;
+    transition: all 0.3s ease;
+    border: none;
+    font-weight: 500;
+    height: 32px;
+    line-height: 20px;
 }
-.x-url-infos table {
-    width:700px;
+
+.x-btns .btn-success {
+    background: linear-gradient(135deg, #4d89fe, #3f6ad8);
+    box-shadow: 0 2px 6px rgba(77, 137, 254, 0.2);
+}
+
+.x-btns .btn-success:hover {
+    transform: translateY(-1px);
+    box-shadow: 0 4px 12px rgba(77, 137, 254, 0.3);
+}
+
+.x-btns .btn-warning {
+    background: linear-gradient(135deg, #ff9f43, #ff7a45);
+    box-shadow: 0 2px 6px rgba(255, 159, 67, 0.2);
 }
+
+.x-btns .btn-warning:hover {
+    transform: translateY(-1px);
+    box-shadow: 0 4px 12px rgba(255, 159, 67, 0.3);
+}
+
+/* 结果区域样式优化 */
+#rst {
+    margin-top: 20px;
+    padding: 15px;
+    background: #ffffff;
+    border-radius: 8px;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+}
+
 #rst h5 {
-    padding-bottom: 10px;
-    border-bottom: 1px solid #eee;
+    padding-bottom: 12px;
+    margin-bottom: 15px;
+    border-bottom: 2px solid #eef1ff;
+    color: #333;
+    font-weight: 600;
+    font-size: 15px;
 }
-.x-btns {
-    float: right;
-    top: -6px;
-    position: relative;
+
+/* URL解析结果样式优化 */
+.x-url-infos {
+    background: #f8f9ff;
+    border-radius: 8px;
+    padding: 15px;
 }
-.x-opts {
+
+.x-url-infos ul {
+    padding: 0;
+    margin: 0;
+}
+
+.x-url-infos ul li {
+    list-style: none;
     font-size: 14px;
+    line-height: 1.5;
+    padding: 6px 0;
+    margin: 0;
+    border-bottom: 1px dashed #e0e5ff;
+    display: flex;
+    align-items: center;
 }
-.x-opts .radio,
-.x-opts .checkbox{
-    margin-top:0;
-    margin-bottom: 0;
+
+.x-url-infos ul li:last-child {
+    border-bottom: none;
 }
-td.td-label {
-    width:60px;
+
+.x-url-infos ul li b {
+    color: #666;
+    font-weight: 500;
+    min-width: 60px;
+    display: inline-block;
 }
-.x-opts td {
-    padding:5px 0;
-    vertical-align: middle;
+
+.x-url-infos table {
+    width: 100%;
+    margin-top: 12px;
+    border-radius: 6px;
+    overflow: hidden;
+    border: 1px solid #e0e5ff;
+}
+
+.x-url-infos table th {
+    background: #f5f8ff;
+    padding: 10px 12px;
+    font-weight: 500;
+    color: #666;
+}
+
+.x-url-infos table td {
+    padding: 8px 12px;
+    border-top: 1px solid #eef1ff;
+    word-break: break-all;
 }
-.x-opts tr:first-child {
-    border-bottom: 1px solid #eee;
-}

+ 16 - 6
apps/en-decode/index.html

@@ -14,13 +14,10 @@
         <div class="panel panel-default" style="margin-bottom: 0px;">
             <div class="panel-heading">
                 <h3 class="panel-title">
-                    <a href="https://www.baidufe.com/fehelper/index/index.html" target="_blank" class="x-a-high">
+                    <a href="https://fehelper.com" target="_blank" class="x-a-high">
                         <img src="../static/img/fe-16.png" alt="fehelper"/> FeHelper</a>:信息编解码工具
-
-                    <span class="x-btns">
-                        <input id="btnCodeChange" class="btn btn-sm btn-success" type="button" value="转换" @click="convert()">
-                        <input id="btnCodeClear" class="btn btn-sm btn-warning ui-ml-10" type="button" value="清空" @click="clear()">
-                    </span>
+                    <a href="#" class="x-donate-link" @click="openDonateModal($event)"><i class="nav-icon">❤&nbsp;</i>打赏鼓励</a>
+                    <a class="x-other-tools" @click="openOptionsPage($event)"><i class="icon-plus-circle"></i> 探索更多实用工具 <span class="tool-market-badge">工具市场</span></a>
                 </h3>
             </div>
         </div>
@@ -64,6 +61,9 @@
                             <div class="radio ui-d-ib ui-mr-20">
                                 <label><input type="radio" name="codeType" value="html2js" v-model="selectedType" @click="convert()">HTML转JS</label>
                             </div>
+                            <div class="radio ui-d-ib ui-mr-20">
+                                <label><input type="radio" name="codeType" value="gzipEncode" v-model="selectedType" @click="convert()">Gzip压缩</label>
+                            </div>
                         </td>
                     </tr>
                     <tr>
@@ -96,12 +96,22 @@
                             <div class="radio ui-d-ib ui-mr-20">
                                 <label><input type="radio" name="codeType" value="cookieDecode" v-model="selectedType" @click="convert()">Cookie格式化</label>
                             </div>
+                            <div class="radio ui-d-ib ui-mr-20">
+                                <label><input type="radio" name="codeType" value="gzipDecode" v-model="selectedType" @click="convert()">Gzip解压</label>
+                            </div>
                         </td>
                     </tr>
                 </table>
 
             </div>
 
+            <div class="row">
+                <span class="x-btns">
+                    <input id="btnCodeChange" class="btn btn-sm btn-success" type="button" value="转换" @click="convert()">
+                    <input id="btnCodeClear" class="btn btn-sm btn-warning ui-ml-10" type="button" value="清空" @click="clear()">
+                </span>
+            </div>
+
             <div id="rst" class="row ui-mt-20" v-show="resultContent.length || urlResult">
                 <h5>当前数据解析结果如下:</h5>
                 <textarea class="form-control mod-textarea" id="rstCode" ref="rstCode" v-model="resultContent" @mouseover="getResult()" v-if="!urlResult"></textarea>

+ 135 - 71
apps/en-decode/index.js

@@ -31,82 +31,130 @@ new Vue({
         }
 
         this.$refs.srcText.focus();
+        this.loadPatchHotfix();
     },
     methods: {
-        convert: function () {
-            this.$nextTick(() => {
-                this.urlResult = null;
-
-                if (this.selectedType === 'uniEncode') {
-
-                    this.resultContent = EncodeUtils.uniEncode(this.sourceContent);
-                } else if (this.selectedType === 'uniDecode') {
-
-                    this.resultContent = EncodeUtils.uniDecode(this.sourceContent.replace(/\\U/g, '\\u'));
-                } else if (this.selectedType === 'utf8Encode') {
-
-                    this.resultContent = encodeURIComponent(this.sourceContent);
-                } else if (this.selectedType === 'utf8Decode') {
-
-                    this.resultContent = decodeURIComponent(this.sourceContent);
-                } else if (this.selectedType === 'utf16Encode') {
-
-                    this.resultContent = EncodeUtils.utf8to16(encodeURIComponent(this.sourceContent));
-                } else if (this.selectedType === 'utf16Decode') {
-
-                    this.resultContent = decodeURIComponent(EncodeUtils.utf16to8(this.sourceContent));
-                } else if (this.selectedType === 'base64Encode') {
-
-                    this.resultContent = EncodeUtils.base64Encode(EncodeUtils.utf8Encode(this.sourceContent));
-                } else if (this.selectedType === 'base64Decode') {
-
-                    this.resultContent = EncodeUtils.utf8Decode(EncodeUtils.base64Decode(this.sourceContent));
-                } else if (this.selectedType === 'md5Encode') {
-
-                    this.resultContent = EncodeUtils.md5(this.sourceContent);
-                } else if (this.selectedType === 'hexEncode') {
-
-                    this.resultContent = EncodeUtils.hexEncode(this.sourceContent);
-                } else if (this.selectedType === 'hexDecode') {
-
-                    this.resultContent = EncodeUtils.hexDecode(this.sourceContent);
-                } else if (this.selectedType === 'html2js') {
-
-                    this.resultContent = EncodeUtils.html2js(this.sourceContent);
-                } else if (this.selectedType === 'sha1Encode') {
 
-                    this.resultContent = EncodeUtils.sha1Encode(this.sourceContent);
-                } else if (this.selectedType === 'htmlEntityEncode') {
-
-                    this.resultContent = he.encode(this.sourceContent, {
-                        'useNamedReferences': true,
-                        'allowUnsafeSymbols': true
-                    });
-                } else if (this.selectedType === 'htmlEntityFullEncode') {
-
-                    this.resultContent = he.encode(this.sourceContent, {
-                        'encodeEverything': true,
-                        'useNamedReferences': true,
-                        'allowUnsafeSymbols': true
-                    });
-                } else if (this.selectedType === 'htmlEntityDecode') {
+        loadPatchHotfix() {
+            // 页面加载时自动获取并注入页面的补丁
+            chrome.runtime.sendMessage({
+                type: 'fh-dynamic-any-thing',
+                thing: 'fh-get-tool-patch',
+                toolName: 'en-decode'
+            }, patch => {
+                if (patch) {
+                    if (patch.css) {
+                        const style = document.createElement('style');
+                        style.textContent = patch.css;
+                        document.head.appendChild(style);
+                    }
+                    if (patch.js) {
+                        try {
+                            if (window.evalCore && window.evalCore.getEvalInstance) {
+                                window.evalCore.getEvalInstance(window)(patch.js);
+                            }
+                        } catch (e) {
+                            console.error('en-decode补丁JS执行失败', e);
+                        }
+                    }
+                }
+            });
+        },
+        
+        convert: async function () {
+            this.$nextTick(async () => {
+                this.urlResult = null;
 
-                    this.resultContent = he.decode(this.sourceContent, {
-                        'isAttributeValue': false
-                    });
-                } else if (this.selectedType === 'urlParamsDecode') {
-                    let res = EncodeUtils.urlParamsDecode(this.sourceContent);
-                    if (res.error) {
-                        this.resultContent = res.error;
-                    } else {
-                        this.urlResult = res;
+                try {
+                    if (this.selectedType === 'uniEncode') {
+
+                        this.resultContent = EncodeUtils.uniEncode(this.sourceContent);
+                    } else if (this.selectedType === 'uniDecode') {
+
+                        this.resultContent = EncodeUtils.uniDecode(this.sourceContent.replace(/\\U/g, '\\u'));
+                    } else if (this.selectedType === 'utf8Encode') {
+
+                        this.resultContent = encodeURIComponent(this.sourceContent);
+                    } else if (this.selectedType === 'utf8Decode') {
+
+                        this.resultContent = decodeURIComponent(this.sourceContent);
+                    } else if (this.selectedType === 'utf16Encode') {
+
+                        this.resultContent = EncodeUtils.utf8to16(encodeURIComponent(this.sourceContent));
+                    } else if (this.selectedType === 'utf16Decode') {
+
+                        this.resultContent = decodeURIComponent(EncodeUtils.utf16to8(this.sourceContent));
+                    } else if (this.selectedType === 'base64Encode') {
+
+                        this.resultContent = EncodeUtils.base64Encode(EncodeUtils.utf8Encode(this.sourceContent));
+                    } else if (this.selectedType === 'base64Decode') {
+
+                        this.resultContent = EncodeUtils.utf8Decode(EncodeUtils.base64Decode(this.sourceContent));
+                    } else if (this.selectedType === 'md5Encode') {
+
+                        this.resultContent = EncodeUtils.md5(this.sourceContent);
+                    } else if (this.selectedType === 'hexEncode') {
+
+                        this.resultContent = EncodeUtils.hexEncode(this.sourceContent);
+                    } else if (this.selectedType === 'hexDecode') {
+
+                        this.resultContent = EncodeUtils.hexDecode(this.sourceContent);
+                    } else if (this.selectedType === 'html2js') {
+
+                        this.resultContent = EncodeUtils.html2js(this.sourceContent);
+                    } else if (this.selectedType === 'sha1Encode') {
+
+                        this.resultContent = EncodeUtils.sha1Encode(this.sourceContent);
+                    } else if (this.selectedType === 'htmlEntityEncode') {
+
+                        this.resultContent = he.encode(this.sourceContent, {
+                            'useNamedReferences': true,
+                            'allowUnsafeSymbols': true
+                        });
+                    } else if (this.selectedType === 'htmlEntityFullEncode') {
+
+                        this.resultContent = he.encode(this.sourceContent, {
+                            'encodeEverything': true,
+                            'useNamedReferences': true,
+                            'allowUnsafeSymbols': true
+                        });
+                    } else if (this.selectedType === 'htmlEntityDecode') {
+
+                        this.resultContent = he.decode(this.sourceContent, {
+                            'isAttributeValue': false
+                        });
+                    } else if (this.selectedType === 'urlParamsDecode') {
+                        let res = EncodeUtils.urlParamsDecode(this.sourceContent);
+                        if (res.error) {
+                            this.resultContent = res.error;
+                        } else {
+                            this.urlResult = res;
+                        }
+                    } else if(this.selectedType === 'jwtDecode') {
+                        let {header,payload,sign} = EncodeUtils.jwtDecode(this.sourceContent);
+                        this.resultContent = `Header: ${header}\n\nPayload: ${payload}\n\nSign: ${sign}`;
+                    } else if(this.selectedType === 'cookieDecode') {
+                        let ckJson = EncodeUtils.formatCookieStringToJson(this.sourceContent);
+                        this.resultContent = JSON.stringify(ckJson,null,4);
+                    } else if (this.selectedType === 'gzipEncode') {
+                        // gzip压缩
+                        if (!this.sourceContent.trim()) {
+                            this.resultContent = '请输入需要压缩的文本内容';
+                            return;
+                        }
+                        this.resultContent = '正在压缩...';
+                        this.resultContent = await EncodeUtils.gzipEncode(this.sourceContent);
+                    } else if (this.selectedType === 'gzipDecode') {
+                        // gzip解压缩
+                        if (!this.sourceContent.trim()) {
+                            this.resultContent = '请输入需要解压缩的Base64编码数据';
+                            return;
+                        }
+                        this.resultContent = '正在解压缩...';
+                        this.resultContent = await EncodeUtils.gzipDecode(this.sourceContent);
                     }
-                } else if(this.selectedType === 'jwtDecode') {
-                    let {header,payload,sign} = EncodeUtils.jwtDecode(this.sourceContent);
-                    this.resultContent = `Header: ${header}\n\nPayload: ${payload}\n\nSign: ${sign}`;
-                } else if(this.selectedType === 'cookieDecode') {
-                    let ckJson = EncodeUtils.formatCookieStringToJson(this.sourceContent);
-                    this.resultContent = JSON.stringify(ckJson,null,4);
+                } catch (error) {
+                    this.resultContent = '操作失败: ' + error.message;
                 }
                 this.$forceUpdate();
             });
@@ -119,6 +167,22 @@ new Vue({
 
         getResult: function () {
             this.$refs.rstCode.select();
+        },
+
+        openOptionsPage: function(event) {
+            event.preventDefault();
+            event.stopPropagation();
+            chrome.runtime.openOptionsPage();
+        },
+
+        openDonateModal: function(event ){
+            event.preventDefault();
+            event.stopPropagation();
+            chrome.runtime.sendMessage({
+                type: 'fh-dynamic-any-thing',
+                thing: 'open-donate-modal',
+                params: { toolName: 'en-decode' }
+            });
         }
     }
 });

+ 0 - 286
apps/excel2json/CSVParser.js

@@ -1,286 +0,0 @@
-//
-//  CSVParser.js
-//  Mr-Data-Converter
-//
-//  Input CSV or Tab-delimited data and this will parse it into a Data Grid Javascript object
-//
-//  CSV Parsing Function from Ben Nadel, http://www.bennadel.com/blog/1504-Ask-Ben-Parsing-CSV-Strings-With-Javascript-Exec-Regular-Expression-Command.htm
-
-
-var isDecimal_re     = /^\s*(\+|-)?((\d+([,\.]\d+)?)|([,\.]\d+))\s*$/;
-
-var CSVParser = {
-
-  //---------------------------------------
-  // UTILS
-  //---------------------------------------
-
-  isNumber: function(string) {
-    if( (string == null) || isNaN( new Number(string) ) ) {
-      return false;
-    }
-    return true;
-  },
-
-
-  //---------------------------------------
-  // PARSE
-  //---------------------------------------
-  //var parseOutput = CSVParser.parse(this.inputText, this.headersProvided, this.delimiter, this.downcaseHeaders, this.upcaseHeaders);
-
-  parse: function (input, headersIncluded, delimiterType, downcaseHeaders, upcaseHeaders, decimalSign) {
-
-    var dataArray = [];
-
-    var errors = [];
-
-    //test for delimiter
-    //count the number of commas
-    var RE = new RegExp("[^,]", "gi");
-    var numCommas = input.replace(RE, "").length;
-
-    //count the number of tabs
-    RE = new RegExp("[^\t]", "gi");
-    var numTabs = input.replace(RE, "").length;
-
-    var rowDelimiter = "\n";
-    //set delimiter
-    var columnDelimiter = ",";
-    if (numTabs > numCommas) {
-      columnDelimiter = "\t"
-    };
-
-    if (delimiterType === "comma") {
-      columnDelimiter = ","
-    } else if (delimiterType === "tab") {
-      columnDelimiter = "\t"
-    }
-
-
-    // kill extra empty lines
-    RE = new RegExp("^" + rowDelimiter + "+", "gi");
-    input = input.replace(RE, "");
-    RE = new RegExp(rowDelimiter + "+$", "gi");
-    input = input.replace(RE, "");
-
-    // var arr = input.split(rowDelimiter);
-    //
-    // for (var i=0; i < arr.length; i++) {
-    //   dataArray.push(arr[i].split(columnDelimiter));
-    // };
-
-
-    // dataArray = jQuery.csv(columnDelimiter)(input);
-    dataArray = this.CSVToArray(input, columnDelimiter);
-
-    //escape out any tabs or returns or new lines
-    for (var i = dataArray.length - 1; i >= 0; i--){
-      for (var j = dataArray[i].length - 1; j >= 0; j--){
-        dataArray[i][j] = dataArray[i][j].replace("\t", "\\t");
-        dataArray[i][j] = dataArray[i][j].replace("\n", "\\n");
-        dataArray[i][j] = dataArray[i][j].replace("\r", "\\r");
-      };
-    };
-
-
-    var headerNames = [];
-    var headerTypes = [];
-    var numColumns = dataArray[0].length;
-    var numRows = dataArray.length;
-    if (headersIncluded) {
-
-      //remove header row
-      headerNames = dataArray.splice(0,1)[0];
-      numRows = dataArray.length;
-
-    } else { //if no headerNames provided
-
-      //create generic property names
-      for (var i=0; i < numColumns; i++) {
-        headerNames.push("val"+String(i));
-        headerTypes.push("");
-      };
-
-    }
-
-
-    if (upcaseHeaders) {
-      for (var i = headerNames.length - 1; i >= 0; i--){
-        headerNames[i] = headerNames[i].toUpperCase();
-      };
-    };
-    if (downcaseHeaders) {
-      for (var i = headerNames.length - 1; i >= 0; i--){
-        headerNames[i] = headerNames[i].toLowerCase();
-      };
-    };
-
-    //test all the rows for proper number of columns.
-    for (var i=0; i < dataArray.length; i++) {
-      var numValues = dataArray[i].length;
-      if (numValues != numColumns) {this.log("Error parsing row "+String(i)+". Wrong number of columns.")};
-    };
-
-    //test columns for number data type
-    var numRowsToTest = dataArray.length;
-    var threshold = 0.9;
-    for (var i=0; i < headerNames.length; i++) {
-      var numFloats = 0;
-      var numInts = 0;
-      for (var r=0; r < numRowsToTest; r++) {
-        if (dataArray[r]) {
-          //replace comma with dot if comma is decimal separator
-          if(decimalSign='comma' && isDecimal_re.test(dataArray[r][i])){
-            dataArray[r][i] = dataArray[r][i].replace(",", ".");
-          }
-          if (CSVParser.isNumber(dataArray[r][i])) {
-            numInts++
-            if (String(dataArray[r][i]).indexOf(".") > 0) {
-              numFloats++
-            }
-          };
-        };
-
-      };
-
-      if ((numInts / numRowsToTest) > threshold){
-        if (numFloats > 0) {
-          headerTypes[i] = "float"
-        } else {
-          headerTypes[i] = "int"
-        }
-      } else {
-        headerTypes[i] = "string"
-      }
-    }
-
-
-
-
-
-    return {'dataGrid':dataArray, 'headerNames':headerNames, 'headerTypes':headerTypes, 'errors':this.getLog()}
-
-  },
-
-
-  //---------------------------------------
-  // ERROR LOGGING
-  //---------------------------------------
-  errorLog:[],
-
-  resetLog: function() {
-    this.errorLog = [];
-  },
-
-  log: function(l) {
-    this.errorLog.push(l);
-  },
-
-  getLog: function() {
-    var out = "";
-    if (this.errorLog.length > 0) {
-      for (var i=0; i < this.errorLog.length; i++) {
-        out += ("!!"+this.errorLog[i] + "!!\n");
-      };
-      out += "\n"
-    };
-
-    return out;
-  },
-
-
-
-  //---------------------------------------
-  // UTIL
-  //---------------------------------------
-
-    // This Function from Ben Nadel, http://www.bennadel.com/blog/1504-Ask-Ben-Parsing-CSV-Strings-With-Javascript-Exec-Regular-Expression-Command.htm
-    // This will parse a delimited string into an array of
-    // arrays. The default delimiter is the comma, but this
-    // can be overriden in the second argument.
-    CSVToArray: function( strData, strDelimiter ){
-      // Check to see if the delimiter is defined. If not,
-      // then default to comma.
-      strDelimiter = (strDelimiter || ",");
-
-      // Create a regular expression to parse the CSV values.
-      var objPattern = new RegExp(
-        (
-          // Delimiters.
-          "(\\" + strDelimiter + "|\\r?\\n|\\r|^)" +
-
-          // Quoted fields.
-          "(?:\"([^\"]*(?:\"\"[^\"]*)*)\"|" +
-
-          // Standard fields.
-          "([^\"\\" + strDelimiter + "\\r\\n]*))"
-        ),
-        "gi"
-        );
-
-
-      // Create an array to hold our data. Give the array
-      // a default empty first row.
-      var arrData = [[]];
-
-      // Create an array to hold our individual pattern
-      // matching groups.
-      var arrMatches = null;
-
-
-      // Keep looping over the regular expression matches
-      // until we can no longer find a match.
-      while (arrMatches = objPattern.exec( strData )){
-
-        // Get the delimiter that was found.
-        var strMatchedDelimiter = arrMatches[ 1 ];
-
-        // Check to see if the given delimiter has a length
-        // (is not the start of string) and if it matches
-        // field delimiter. If id does not, then we know
-        // that this delimiter is a row delimiter.
-        if (
-          strMatchedDelimiter.length &&
-          (strMatchedDelimiter != strDelimiter)
-          ){
-
-          // Since we have reached a new row of data,
-          // add an empty row to our data array.
-          arrData.push( [] );
-
-        }
-
-
-        // Now that we have our delimiter out of the way,
-        // let's check to see which kind of value we
-        // captured (quoted or unquoted).
-
-        if (arrMatches[ 2 ]){
-
-          // We found a quoted value. When we capture
-          // this value, unescape any double quotes.
-          var strMatchedValue = arrMatches[ 2 ].replace(
-            new RegExp( "\"\"", "g" ),
-            "\""
-            );
-
-        } else {
-
-          // We found a non-quoted value.
-          var strMatchedValue = arrMatches[ 3 ];
-
-        }
-
-
-        // Now that we have our value string, let's add
-        // it to the data array.
-        arrData[ arrData.length - 1 ].push( strMatchedValue );
-      }
-
-      // Return the parsed data.
-      return( arrData );
-    }
-
-
-
-}

+ 0 - 538
apps/excel2json/DataGridRenderer.js

@@ -1,538 +0,0 @@
-// 
-//  DataGridRenderer.js
-//  Part of Mr-Data-Converter
-//  
-//  Created by Shan Carter on 2010-10-18.
-// 
-
-
-var DataGridRenderer = {
-  
-  //---------------------------------------
-  // Actionscript
-  //---------------------------------------
-  
-  as: function (dataGrid, headerNames, headerTypes, indent, newLine) {
-    //inits...
-    var commentLine = "//";
-    var commentLineEnd = "";
-    var outputText = "[";
-    var numRows = dataGrid.length;
-    var numColumns = headerNames.length;
-    
-    //begin render loops
-    for (var i=0; i < numRows; i++) {
-      var row = dataGrid[i];
-      outputText += "{";
-      for (var j=0; j < numColumns; j++) {
-        if ((headerTypes[j] == "int")||(headerTypes[j] == "float")) {
-          var rowOutput = row[j] || "null";
-        } else {
-          var rowOutput = '"'+( row[j] || "" )+'"';
-        };      
-        outputText += (headerNames[j] + ":" + rowOutput)
-        if (j < (numColumns-1)) {outputText+=","};
-      };
-      outputText += "}";
-      if (i < (numRows-1)) {outputText += ","+newLine};
-    };
-    outputText += "];";
-    
-    
-    return outputText;
-  },
-  
-  
-  //---------------------------------------
-  // ASP / VBScript
-  //---------------------------------------
-  
-  asp: function (dataGrid, headerNames, headerTypes, indent, newLine) {
-    //inits...
-    var commentLine = "'";
-    var commentLineEnd = "";
-    var outputText = "";
-    var numRows = dataGrid.length;
-    var numColumns = headerNames.length;
-    
-    //begin render loop
-    for (var i=0; i < numRows; i++) {
-      var row = dataGrid[i];
-      for (var j=0; j < numColumns; j++) {
-        if ((headerTypes[j] == "int")||(headerTypes[j] == "float")) {
-          var rowOutput = row[j] || "null";
-        } else {
-          var rowOutput = '"'+( row[j] || "" )+'"';
-        };
-      outputText += 'myArray('+j+','+i+') = '+rowOutput+newLine;        
-      };
-    };
-    outputText = 'Dim myArray('+(j-1)+','+(i-1)+')'+newLine+outputText;
-    
-    return outputText;
-  },
-  
-  
-  //---------------------------------------
-  // HTML Table
-  //---------------------------------------
-  
-  html: function (dataGrid, headerNames, headerTypes, indent, newLine) {
-    //inits...
-    var commentLine = "<!--";
-    var commentLineEnd = "-->";
-    var outputText = "";
-    var numRows = dataGrid.length;
-    var numColumns = headerNames.length;
-    
-    //begin render loop
-    outputText += "<table>"+newLine;
-    outputText += indent+"<thead>"+newLine;
-    outputText += indent+indent+"<tr>"+newLine;
-    
-    for (var j=0; j < numColumns; j++) {
-      outputText += indent+indent+indent+'<th class="'+headerNames[j]+'-cell">';          
-      outputText += headerNames[j];
-      outputText += '</th>'+newLine;
-    };
-    outputText += indent+indent+"</tr>"+newLine;
-    outputText += indent+"</thead>"+newLine;
-    outputText += indent+"<tbody>"+newLine;
-    for (var i=0; i < numRows; i++) {
-      var row = dataGrid[i];
-      var rowClassName = ""
-      if (i === numRows-1) {
-        rowClassName = ' class="lastRow"';
-      } else if (i === 0){
-        rowClassName = ' class="firstRow"';
-      }
-      outputText += indent+indent+"<tr"+rowClassName+">"+newLine;
-      for (var j=0; j < numColumns; j++) {
-        outputText += indent+indent+indent+'<td class="'+headerNames[j]+'-cell">';          
-        outputText += row[j]
-        outputText += '</td>'+newLine
-      };
-      outputText += indent+indent+"</tr>"+newLine;
-    };
-    outputText += indent+"</tbody>"+newLine;
-    outputText += "</table>";
-    
-    return outputText;
-  },
-  
-  
-  //---------------------------------------
-  // JSON properties
-  //---------------------------------------
-  
-  json: function (dataGrid, headerNames, headerTypes, indent, newLine) {
-    //inits...
-    var commentLine = "//";
-    var commentLineEnd = "";
-    var outputText = "[";
-    var numRows = dataGrid.length;
-    var numColumns = headerNames.length;
-    
-    //begin render loop
-    for (var i=0; i < numRows; i++) {
-      var row = dataGrid[i];
-      outputText += "{";
-      for (var j=0; j < numColumns; j++) {
-        if ((headerTypes[j] == "int")||(headerTypes[j] == "float")) {
-          var rowOutput = row[j] || "null";
-        } else {
-          var rowOutput = '"' + ( row[j] || "" ) + '"';
-        };
-  
-      outputText += ('"'+headerNames[j] +'"' + ":" + rowOutput );
-  
-        if (j < (numColumns-1)) {outputText+=","};
-      };
-      outputText += "}";
-      if (i < (numRows-1)) {outputText += ","+newLine};
-    };
-    outputText += "]";
-    
-    return outputText;
-  },
-  
-  //---------------------------------------
-  // JSON Array of Columns
-  //---------------------------------------
-  jsonArrayCols: function (dataGrid, headerNames, headerTypes, indent, newLine) {
-    //inits...
-    var commentLine = "//";
-    var commentLineEnd = "";
-    var outputText = "";
-    var numRows = dataGrid.length;
-    var numColumns = headerNames.length;
-    
-    //begin render loop
-    outputText += "{"+newLine;
-    for (var i=0; i < numColumns; i++) {
-      outputText += indent+'"'+headerNames[i]+'":[';
-      for (var j=0; j < numRows; j++) {
-        if ((headerTypes[i] == "int")||(headerTypes[i] == "float")) {
-          outputText += dataGrid[j][i] || 0;
-        } else {
-          outputText += '"'+(dataGrid[j][i] || "")+'"' ;
-        }
-        if (j < (numRows-1)) {outputText+=","};
-      };
-      outputText += "]";
-      if (i < (numColumns-1)) {outputText += ","+newLine};
-    };
-    outputText += newLine+"}";
-    
-    
-    return outputText;
-  },
-  
-  
-  //---------------------------------------
-  // JSON Array of Rows
-  //---------------------------------------
-  jsonArrayRows: function (dataGrid, headerNames, headerTypes, indent, newLine) {
-    //inits...
-    var commentLine = "//";
-    var commentLineEnd = "";
-    var outputText = "";
-    var numRows = dataGrid.length;
-    var numColumns = headerNames.length;
-    
-    //begin render loop
-    outputText += "["+newLine;
-    for (var i=0; i < numRows; i++) {
-      outputText += indent+"[";
-      for (var j=0; j < numColumns; j++) {
-        if ((headerTypes[j] == "int")||(headerTypes[j] == "float")) {
-          outputText += dataGrid[i][j] || 0;
-        } else {
-          outputText += '"'+(dataGrid[i][j] || "")+'"' ;
-        }
-        if (j < (numColumns-1)) {outputText+=","};
-      };
-      outputText += "]";
-      if (i < (numRows-1)) {outputText += ","+newLine};
-    };
-    outputText += newLine+"]";
-    
-    
-    return outputText;
-  },
-  
-  
-
-  //---------------------------------------
-  // JSON Dictionary
-  //---------------------------------------
-  jsonDict: function(dataGrid, headerNames, headerTypes, indent, newLine) {
-    //inits...
-    var commentLine = "//";
-    var commentLineEnd = "";
-    var outputText = "";
-    var numRows = dataGrid.length;
-    var numColumns = headerNames.length;
-
-    //begin render loop
-    outputText += "{" + newLine;
-    for (var i = 0; i < numRows; i++) {
-      outputText += indent + '"' + dataGrid[i][0] + '": ';
-      if (numColumns == 2) {
-        outputText += _fmtVal(i, 1, dataGrid);
-      } else {
-        outputText += '{ ';
-        for (var j = 1; j < numColumns; j++) {
-          if (j > 1) outputText += ', ';
-          outputText += '"' + headerNames[j] + '"' + ":" + _fmtVal(i, j, dataGrid);
-        }
-        outputText += '}';
-      }
-      if (i < (numRows - 1)) {
-        outputText += "," + newLine;
-      }
-    }
-    outputText += newLine + "}";
-
-    function _fmtVal(i, j) {
-      if ((headerTypes[j] == "int")||(headerTypes[j] == "float")) {
-        return dataGrid[i][j] || 0;
-      } else {
-        return '"'+(dataGrid[i][j] || "")+'"' ;
-      }
-    }
-
-    return outputText;
-  },
-
-
-  //---------------------------------------
-  // MYSQL
-  //---------------------------------------
-  mysql: function (dataGrid, headerNames, headerTypes, indent, newLine) {
-    //inits...
-    var commentLine = "/*";
-    var commentLineEnd = "*/";
-    var outputText = "";
-    var numRows = dataGrid.length;
-    var numColumns = headerNames.length;
-    var tableName = "MrDataConverter"
-    
-    //begin render loop
-    outputText += 'CREATE TABLE '+tableName+' (' + newLine;
-    outputText += indent+"id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,"+newLine;
-    for (var j=0; j < numColumns; j++) {
-      var dataType = "VARCHAR(255)";
-      if ((headerTypes[j] == "int")||(headerTypes[j] == "float")) {
-        dataType = headerTypes[j].toUpperCase();
-      };
-      outputText += indent+""+headerNames[j]+" "+dataType;
-      if (j < numColumns - 1) {outputText += ","};
-      outputText += newLine;
-    };
-    outputText += ');' + newLine;
-    outputText += "INSERT INTO "+tableName+" "+newLine+indent+"(";
-    for (var j=0; j < numColumns; j++) {
-      outputText += headerNames[j];
-      if (j < numColumns - 1) {outputText += ","};
-    };
-    outputText += ") "+newLine+"VALUES "+newLine;
-    for (var i=0; i < numRows; i++) {
-      outputText += indent+"(";
-      for (var j=0; j < numColumns; j++) {
-        if ((headerTypes[j] == "int")||(headerTypes[j] == "float"))  {
-          outputText += dataGrid[i][j] || "null";
-        } else {
-          outputText += "'"+( dataGrid[i][j] || "" )+"'";
-        };
-        
-        if (j < numColumns - 1) {outputText += ","};
-      };
-      outputText += ")";
-      if (i < numRows - 1) {outputText += ","+newLine;};
-    };
-    outputText += ";";
-    
-    return outputText;
-  },
-  
-  
-  //---------------------------------------
-  // PHP
-  //---------------------------------------
-  php: function (dataGrid, headerNames, headerTypes, indent, newLine) {
-    //inits...
-    var commentLine = "//";
-    var commentLineEnd = "";
-    var outputText = "";
-    var numRows = dataGrid.length;
-    var numColumns = headerNames.length;
-    var tableName = "MrDataConverter"
-    
-    //begin render loop
-    outputText += "array(" + newLine;
-    for (var i=0; i < numRows; i++) {
-      var row = dataGrid[i];
-      outputText += indent + "array(";
-      for (var j=0; j < numColumns; j++) {
-        if ((headerTypes[j] == "int")||(headerTypes[j] == "float"))  {
-          var rowOutput = row[j] || "null";
-        } else {
-          var rowOutput = '"'+(row[j] || "")+'"';
-        };          
-        outputText += ('"'+headerNames[j]+'"' + "=>" + rowOutput)
-        if (j < (numColumns-1)) {outputText+=","};
-      };
-      outputText += ")";
-      if (i < (numRows-1)) {outputText += ","+newLine};
-    };
-    outputText += newLine + ");";
-    
-    return outputText;
-  },
-  
-  //---------------------------------------
-  // Python dict
-  //---------------------------------------
-  
-  python: function (dataGrid, headerNames, headerTypes, indent, newLine) {
-    //inits...
-    var commentLine = "//";
-    var commentLineEnd = "";
-    var outputText = "[";
-    var numRows = dataGrid.length;
-    var numColumns = headerNames.length;
-    
-    //begin render loop
-    for (var i=0; i < numRows; i++) {
-      var row = dataGrid[i];
-      outputText += "{";
-      for (var j=0; j < numColumns; j++) {
-        if ((headerTypes[j] == "int")||(headerTypes[j] == "float")) {
-          var rowOutput = row[j] || "None";
-        } else {
-          var rowOutput = '"'+(row[j] || "")+'"';
-        };
-  
-      outputText += ('"'+headerNames[j] +'"' + ":" + rowOutput );
-  
-        if (j < (numColumns-1)) {outputText+=","};
-      };
-      outputText += "}";
-      if (i < (numRows-1)) {outputText += ","+newLine};
-    };
-    outputText += "];";
-    
-    return outputText;
-  },
-  
-  
-  //---------------------------------------
-  // Ruby
-  //---------------------------------------
-  ruby: function (dataGrid, headerNames, headerTypes, indent, newLine) {
-    //inits...
-    var commentLine = "#";
-    var commentLineEnd = "";
-    var outputText = "";
-    var numRows = dataGrid.length;
-    var numColumns = headerNames.length;
-    var tableName = "MrDataConverter"
-    
-    //begin render loop
-    outputText += "[";
-    for (var i=0; i < numRows; i++) {
-      var row = dataGrid[i];
-      outputText += "{";
-      for (var j=0; j < numColumns; j++) {
-        if ((headerTypes[j] == "int")||(headerTypes[j] == "float")) {
-          var rowOutput = row[j] || "nil"
-        } else {
-          var rowOutput = '"'+(row[j] || "")+'"';
-        };         
-        outputText += ('"'+headerNames[j]+'"' + "=>" + rowOutput)
-        if (j < (numColumns-1)) {outputText+=","};
-      };
-      outputText += "}";
-      if (i < (numRows-1)) {outputText += ","+newLine};
-    };
-    outputText += "];";
-    
-    return outputText;
-  },
-  
-  
-  //---------------------------------------
-  // XML Nodes
-  //---------------------------------------
-  xml: function (dataGrid, headerNames, headerTypes, indent, newLine) {
-    //inits...
-    var commentLine = "<!--";
-    var commentLineEnd = "-->";
-    var outputText = "";
-    var numRows = dataGrid.length;
-    var numColumns = headerNames.length;
-    
-    //begin render loop
-    outputText = '<?xml version="1.0" encoding="UTF-8"?>' + newLine;
-    outputText += "<rows>"+newLine;
-    for (var i=0; i < numRows; i++) {
-      var row = dataGrid[i];
-      outputText += indent+"<row>"+newLine;
-      for (var j=0; j < numColumns; j++) {
-        outputText += indent+indent+'<'+headerNames[j]+'>';          
-        outputText += row[j] || ""
-        outputText += '</'+headerNames[j]+'>'+newLine
-      };
-      outputText += indent+"</row>"+newLine;
-    };
-    outputText += "</rows>";
-    
-    return outputText;
-    
-  },
-  
-  
-  
-  //---------------------------------------
-  // XML properties
-  //---------------------------------------
-  xmlProperties: function (dataGrid, headerNames, headerTypes, indent, newLine) {
-    //inits...
-    var commentLine = "<!--";
-    var commentLineEnd = "-->";
-    var outputText = "";
-    var numRows = dataGrid.length;
-    var numColumns = headerNames.length;
-  
-    //begin render loop
-    outputText = '<?xml version="1.0" encoding="UTF-8"?>' + newLine;
-    outputText += "<rows>"+newLine;
-    for (var i=0; i < numRows; i++) {
-      var row = dataGrid[i];
-      outputText += indent+"<row ";
-      for (var j=0; j < numColumns; j++) {
-        outputText += headerNames[j]+'=';          
-        outputText += '"' + row[j] + '" ';
-      };
-      outputText += "></row>"+newLine;
-    };
-    outputText += "</rows>";
-    
-    return outputText;
-    
-  },
-  
-  //---------------------------------------
-  // XML Illustrator
-  //---------------------------------------
-  xmlIllustrator: function (dataGrid, headerNames, headerTypes, indent, newLine) {
-    //inits...
-    var commentLine = "<!--";
-    var commentLineEnd = "-->";
-    var outputText = "";
-    var numRows = dataGrid.length;
-    var numColumns = headerNames.length;
-    
-    //begin render loop
-    outputText = '<?xml version="1.0" encoding="utf-8"?>' + newLine;
-    outputText += '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20001102//EN"    "http://www.w3.org/TR/2000/CR-SVG-20001102/DTD/svg-20001102.dtd" [' + newLine;
-    outputText += indent+'<!ENTITY ns_graphs "http://ns.adobe.com/Graphs/1.0/">' + newLine;
-    outputText += indent+'<!ENTITY ns_vars "http://ns.adobe.com/Variables/1.0/">' + newLine;
-    outputText += indent+'<!ENTITY ns_imrep "http://ns.adobe.com/ImageReplacement/1.0/">' + newLine;
-    outputText += indent+'<!ENTITY ns_custom "http://ns.adobe.com/GenericCustomNamespace/1.0/">' + newLine;
-    outputText += indent+'<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">' + newLine;
-    outputText += indent+'<!ENTITY ns_extend "http://ns.adobe.com/Extensibility/1.0/">' + newLine;
-    outputText += ']>' + newLine;
-    outputText += '<svg>' + newLine;
-    outputText += '<variableSets  xmlns="&ns_vars;">' + newLine;
-    outputText += indent+'<variableSet  varSetName="binding1" locked="none">' + newLine;
-    outputText += indent+indent+'<variables>' + newLine;
-    for (var i=0; i < numColumns; i++) {
-      outputText += indent+indent+indent+'<variable varName="'+headerNames[i]+'" trait="textcontent" category="&ns_flows;"></variable>' + newLine;
-    };
-    outputText += indent+indent+'</variables>' + newLine;
-    outputText += indent+indent+'<v:sampleDataSets  xmlns:v="http://ns.adobe.com/Variables/1.0/" xmlns="http://ns.adobe.com/GenericCustomNamespace/1.0/">' + newLine;
-    
-    for (var i=0; i < numRows; i++) {
-      var row = dataGrid[i];
-      outputText += indent+indent+indent+'<v:sampleDataSet dataSetName="' + row[0] + '">'+newLine;
-      for (var j=0; j < numColumns; j++) {
-        outputText += indent+indent+indent+indent+'<'+headerNames[j]+'>'+newLine;          
-        outputText += indent+indent+indent+indent+indent+'<p>' + row[j] + '</p>' +newLine;
-        outputText += indent+indent+indent+indent+'</'+headerNames[j]+'>'+newLine
-      };
-      outputText += indent+indent+indent+'</v:sampleDataSet>'+newLine;
-    };
-    
-    outputText += indent+indent+'</v:sampleDataSets>' + newLine;
-    outputText += indent+'</variableSet>' + newLine;
-    outputText += '</variableSets>' + newLine;
-    outputText += '</svg>' + newLine;
-    
-    
-    return outputText;
-    
-  },
-  
-}

+ 0 - 190
apps/excel2json/converter.js

@@ -1,190 +0,0 @@
-//
-//  converter.js
-//  Mr-Data-Converter
-//
-//  Created by Shan Carter on 2010-09-01.
-//
-
-
-
-function DataConverter(nodeId) {
-
-  //---------------------------------------
-  // PUBLIC PROPERTIES
-  //---------------------------------------
-
-  this.nodeId                 = nodeId;
-  this.node                   = $("#"+nodeId);
-
-  this.outputDataTypes        = [
-                                {"text":"Actionscript",           "id":"as",               "notes":""},
-                                {"text":"ASP/VBScript",           "id":"asp",              "notes":""},
-                                {"text":"HTML",                   "id":"html",             "notes":""},
-                                {"text":"JSON - Properties",      "id":"json",             "notes":""},
-                                {"text":"JSON - Column Arrays",   "id":"jsonArrayCols",    "notes":""},
-                                {"text":"JSON - Row Arrays",      "id":"jsonArrayRows",    "notes":""},
-                                {"text":"JSON - Dictionary",      "id":"jsonDict",         "notes":""},
-                                {"text":"MySQL",                  "id":"mysql",            "notes":""},
-                                {"text":"PHP",                    "id":"php",              "notes":""},
-                                {"text":"Python - Dict",          "id":"python",           "notes":""},
-                                {"text":"Ruby",                   "id":"ruby",             "notes":""},
-                                {"text":"XML - Properties",       "id":"xmlProperties",    "notes":""},
-                                {"text":"XML - Nodes",            "id":"xml",              "notes":""},
-                                {"text":"XML - Illustrator",      "id":"xmlIllustrator",   "notes":""}];
-  this.outputDataType         = "json";
-
-  this.columnDelimiter        = "\t";
-  this.rowDelimiter           = "\n";
-
-  this.inputTextArea          = {};
-  this.outputTextArea         = {};
-
-  this.inputHeader            = {};
-  this.outputHeader           = {};
-  this.dataSelect             = {};
-
-  this.inputText              = "";
-  this.outputText             = "";
-
-  this.newLine                = "\n";
-  this.indent                 = "  ";
-
-  this.root                   = "rows";
-  this.child                  = "row";
-
-  this.commentLine            = "//";
-  this.commentLineEnd         = "";
-  this.tableName              = "MrDataConverter"
-
-  this.useUnderscores         = true;
-  this.headersProvided        = true;
-  this.downcaseHeaders        = true;
-  this.upcaseHeaders          = false;
-  this.includeWhiteSpace      = true;
-  this.useTabsForIndent       = false;
-
-}
-
-//---------------------------------------
-// PUBLIC METHODS
-//---------------------------------------
-
-DataConverter.prototype.create = function(w,h) {
-  var self = this;
-
-  //build HTML for converter
-  this.inputHeader = $('<div class="groupHeader" id="inputHeader"><p class="groupHeadline">数据源输入<span class="subhead">(可以直接从Excel/CVS中拷贝内容到这里! <a href="#" id="insertSample">简单示例!</a>)</span></p></div>');
-  this.inputTextArea = $('<textarea class="textInputs" id="dataInput" placeholder="输入CVS或者以tab为间隔的数据"></textarea>');
-  var outputHeaderText = '<div class="groupHeader" id="outputHeader"><p class="groupHeadline">结果转换为<select name="Data Types" id="dataSelector" class="form-control">';
-    for (var i=0; i < this.outputDataTypes.length; i++) {
-
-      outputHeaderText += '<option value="'+this.outputDataTypes[i]["id"]+'" '
-              + (this.outputDataTypes[i]["id"] == this.outputDataType ? 'selected="selected"' : '')
-              + '>'
-              + this.outputDataTypes[i]["text"]+'</option>';
-    };
-    outputHeaderText += '</select><span class="subhead" id="outputNotes"></span></p></div>';
-  this.outputHeader = $(outputHeaderText);
-  this.outputTextArea = $('<textarea class="textInputs" id="dataOutput" placeholder="转换后的结果会显示在这里"></textarea>');
-
-  this.node.append(this.inputHeader);
-  this.node.append(this.inputTextArea);
-  this.node.append(this.outputHeader);
-  this.node.append(this.outputTextArea);
-
-  this.dataSelect = this.outputHeader.find("#dataSelector");
-
-
-  //add event listeners
-
-  // $("#convertButton").bind('click',function(evt){
-  //   evt.preventDefault();
-  //   self.convert();
-  // });
-
-  this.outputTextArea.click(function(evt){this.select();});
-
-
-  $("#insertSample").bind('click',function(evt){
-    evt.preventDefault();
-    self.insertSampleData();
-    self.convert();
-    _gaq.push(['_trackEvent', 'SampleData','InsertGeneric']);
-  });
-
-  $("#dataInput").keyup(function() {self.convert()});
-  $("#dataInput").change(function() {
-    self.convert();
-    _gaq.push(['_trackEvent', 'DataType',self.outputDataType]);
-  });
-
-  $("#dataSelector").bind('change',function(evt){
-       self.outputDataType = $(this).val();
-       self.convert();
-     });
-
-  this.resize(w,h);
-}
-
-DataConverter.prototype.resize = function(w,h) {
-
-  var paneWidth = w;
-  var paneHeight = (h-90)/2-20;
-
-  this.node.css({width:paneWidth});
-  this.inputTextArea.css({width:paneWidth-20,height:paneHeight});
-  this.outputTextArea.css({width: paneWidth-20, height:paneHeight});
-
-}
-
-DataConverter.prototype.convert = function() {
-
-  this.inputText = this.inputTextArea.val();
-  this.outputText = "";
-
-
-  //make sure there is input data before converting...
-  if (this.inputText.length > 0) {
-
-    if (this.includeWhiteSpace) {
-      this.newLine = "\n";
-    } else {
-      this.indent = "";
-      this.newLine = "";
-    }
-
-    CSVParser.resetLog();
-    var parseOutput = CSVParser.parse(this.inputText, this.headersProvided, this.delimiter, this.downcaseHeaders, this.upcaseHeaders);
-
-    var dataGrid = parseOutput.dataGrid;
-    var headerNames = parseOutput.headerNames;
-    var headerTypes = parseOutput.headerTypes;
-    var errors = parseOutput.errors;
-
-    this.outputText = DataGridRenderer[this.outputDataType](dataGrid, headerNames, headerTypes, this.indent, this.newLine);
-
-
-    //验证成功,将会对其中的节点进行替换
-    //否者,直接对数据进行输出
-    if(this.root
-        && this.child
-        && (this.outputDataType === "xmlProperties"
-            || this.outputDataType === "xml")){
-
-      //替换其中的根节点与字节点
-      this.outputText = this.outputText.replace(/rows/g,this.root)
-      this.outputText = this.outputText.replace(/row/g,this.child);
-    }
-    this.outputTextArea.val(errors + this.outputText);
-
-
-
-  }; //end test for existence of input text
-}
-
-
-DataConverter.prototype.insertSampleData = function() {
-  this.inputTextArea.val("NAME\tVALUE\tCOLOR\tDATE\nAlan\t12\tblue\tSep. 25, 2009\nShan\t13\t\"green\tblue\"\tSep. 27, 2009\nJohn\t45\torange\tSep. 29, 2009\nMinna\t27\tteal\tSep. 30, 2009");
-}
-
-

+ 565 - 89
apps/excel2json/index.css

@@ -1,143 +1,619 @@
-@import url("../static/css/bootstrap.min.css");
-
-#pageContainer > .panel-body {
+body {
+    background-color: #f8f9fb;
+    font-family: 'Microsoft YaHei', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
+    margin: 0;
+    padding: 0;
+    color: #2c3e50;
+}
+.wrapper {
     margin: 0 auto;
+    padding: 0;
+    min-height: 100vh;
+    background: #f8f9fb;
+    display: flex;
+    flex-direction: column;
+}
+.main-navbar {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    background: #fff;
+    border-bottom: 1px solid #e5e5e5;
+    padding: 12px 30px;
+    box-shadow: 0 2px 10px rgba(0,0,0,0.03);
+}
+.navbar-brand .brand-link {
+    display: flex;
+    align-items: center;
+    text-decoration: none;
+}
+.navbar-brand img {
+    width: 24px;
+    height: 24px;
+    margin-right: 10px;
+}
+.brand-text {
+    font-size: 20px;
+    font-weight: bold;
+    color: #333;
+    margin-right: 10px;
+}
+.brand-subtitle {
+    font-size: 14px;
+    color: #667790;
+    padding-left: 10px;
+    border-left: 1px solid #e0e0e0;
+}
+.navbar-actions {
+    display: flex;
+    align-items: center;
+}
+.nav-item {
+    margin-left: 20px;
+    color: #e74c3c;
+    font-size: 16px;
+    text-decoration: none;
+    cursor: pointer;
+    transition: all 0.2s;
+}
+.nav-item:hover {
+    color: #c0392b;
+}
+.nav-item .nav-icon {
+    font-style: normal;
+    margin-right: 6px;
 }
 
-body {
-    overflow: hidden;
+/* 主容器样式 */
+.main-container {
+    width: 98%;
+    margin: 20px auto;
+    display: flex;
+    flex-direction: column;
+    gap: 20px;
 }
 
-#base {
-    padding: 15px;
-    position: relative;
+/* 工具头部样式 */
+.tool-header {
+    display: flex;
+    align-items: center;
+    padding: 15px 20px;
+    background: linear-gradient(135deg, #3498db, #2980b9);
+    border-radius: 10px;
+    box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
+    margin-bottom: 10px;
+    color: white;
+}
+.tool-icon {
+    font-size: 32px;
+    background: rgba(255, 255, 255, 0.2);
+    width: 60px;
+    height: 60px;
+    border-radius: 50%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    margin-right: 20px;
+    box-shadow: 0 0 0 5px rgba(255, 255, 255, 0.1);
+}
+.tool-title {
+    flex-grow: 1;
+}
+.tool-title h1 {
+    margin: 0 0 5px 0;
+    font-size: 24px;
+    font-weight: 600;
+}
+.tool-title p {
+    margin: 0;
+    font-size: 14px;
+    opacity: 0.9;
 }
 
-/*header*/
+/* 面板样式 */
 
-#header {
-    width: 300px;
-    overflow: auto;
+/* 操作区域样式 */
+.operation-zone {
+    margin-bottom: 20px;
+}
+.input-flex-row {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    gap: 10px;
+    flex-wrap: wrap;
+    margin-bottom: 15px;
+}
+.input-flex-row > * {
+    margin-bottom: 0 !important;
+}
+.input-section label {
+    font-weight: 600;
+    color: #2c3e50;
+    display: flex;
+    align-items: center;
+    gap: 6px;
+}
+.input-section label i {
+    color: #3498db;
 }
 
-#header a {
-    color: #99FFFF;
+/* 文件输入样式 */
+.file-input-wrapper {
+    position: relative;
+    overflow: hidden;
+}
+.file-input-wrapper input[type="file"] {
+    padding: 8px 12px;
+    border: 1px solid #d1d9e6;
+    border-radius: 6px;
+    background: #f7fafd;
+    font-size: 14px;
+    color: #2c3e50;
+    transition: all 0.3s;
+    box-shadow: 0 2px 5px rgba(0,0,0,0.02);
+}
+.file-input-wrapper input[type="file"]:hover,
+.file-input-wrapper input[type="file"]:focus {
+    border: 1px solid #3498db;
+    background: #f0f7ff;
+    box-shadow: 0 3px 10px rgba(52, 152, 219, 0.1);
 }
 
-#description p {
-    margin-bottom: 18px;
+/* 示例区域样式 */
+.example-section {
+    display: flex;
+    align-items: center;
+    background: #f6f9fe;
+    border-radius: 6px;
+    padding: 8px 15px;
+    box-shadow: 0 2px 6px rgba(0,0,0,0.03);
+    border-left: 3px solid #3498db;
+}
+.example-section span {
+    color: #667790;
+    display: flex;
+    align-items: center;
+    gap: 6px;
+}
+.example-section span i {
+    color: #f39c12;
+}
+.example-btn {
+    display: inline-block;
+    background: #ecf5fe;
+    color: #3498db;
+    border: 1px solid #c4e1ff;
+    margin-right: 4px;
+    font-size: 13px;
+    border-radius: 5px;
+    padding: 5px 12px;
+    transition: all 0.2s;
+    font-weight: 500;
+    text-decoration: none;
+    cursor: pointer;
+    line-height: 1.6;
+}
+.example-btn:last-child {
+    margin-right: 0;
+}
+.example-btn:hover, .example-btn:focus {
+    background: #d9ebfd;
+    color: #2980b9;
+    border-color: #3498db;
+    transform: translateY(-1px);
+    box-shadow: 0 2px 5px rgba(52, 152, 219, 0.15);
+    text-decoration: none;
 }
 
-#header h3 {
-    font-size: 16px;
-    margin: 15px 0 10px 0;
-    padding: 15px 0 0 0;
-    border-top: solid 1px #CCC;
-    text-transform: uppercase;
+
+/* 按钮区域样式 */
+.action-zone {
+    display: flex;
+    align-items: center;
+    margin: 20px 0;
+}
+.btn {
+    padding: 9px 18px;
+    font-size: 14px;
+    border-radius: 6px;
+    cursor: pointer;
+    font-weight: 600;
+    display: inline-flex;
+    align-items: center;
+    gap: 6px;
+    transition: all 0.2s;
+}
+.btn-primary {
+    background: linear-gradient(135deg, #3498db, #2980b9);
+    color: #fff;
+    border: none;
+    box-shadow: 0 4px 10px rgba(52, 152, 219, 0.3);
+}
+.btn-primary:hover {
+    background: linear-gradient(135deg, #2980b9, #2573a7);
+    transform: translateY(-1px);
+    box-shadow: 0 6px 15px rgba(52, 152, 219, 0.4);
+}
+.btn-outline {
+    background: transparent;
+    color: #3498db;
+    border: 1px solid #3498db;
+}
+.btn-outline:hover {
+    background: #f0f7ff;
+    box-shadow: 0 2px 5px rgba(52, 152, 219, 0.15);
+}
+.btn-secondary {
+    background: #ecf5fe;
+    color: #3498db;
+    border: 1px solid #c4e1ff;
+    font-size: 13px;
+    border-radius: 5px;
+    padding: 5px 12px;
+    transition: all 0.2s;
+    font-weight: 500;
+    cursor: pointer;
+    text-decoration: none;
+    line-height: 1.6;
+}
+.btn-secondary:hover, .btn-secondary:focus {
+    background: #d9ebfd;
+    color: #2980b9;
+    border-color: #3498db;
+    transform: translateY(-1px);
+    box-shadow: 0 2px 5px rgba(52, 152, 219, 0.15);
+    text-decoration: none;
 }
 
-h1 {
-    color: #DEDEDE;
-    font-family: palatino, Georgia;
-    font-size: 40px;
-    line-height: 40px;
-    font-weight: bold;
-    margin-bottom: 8px;
-    text-shadow: 1px 1px 3px #000;
+/* 输出区域样式 */
+.output-section {
+    border-top: 1px solid #eaeef3;
+    padding-top: 20px;
+}
+.output-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 10px;
+}
+.output-header label {
+    font-weight: 600;
+    color: #2c3e50;
+    display: flex;
+    align-items: center;
+    gap: 6px;
+}
+.output-header label i {
+    color: #16a085;
 }
 
-p {
-    font-size: 15px;
-    line-height: 22px;
+/* 粘贴输入框样式 */
+#pasteInput,#jsonOutput {
+    font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
+    font-size: 12px;
+    padding: 10px;
+    border-radius: 6px;
+    border: 1px solid #d1d9e6;
+    background: #f8fafc;
+    resize: vertical;
+    transition: all 0.3s;
+    box-shadow: inset 0 2px 4px rgba(0,0,0,0.02);
+}
+#pasteInput:focus,#jsonOutput:focus {
+    border-color: #3498db;
+    box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.15);
+    outline: none;
 }
 
-/*settings*/
-#settings {
-    font-size:12px;
-    line-height: 30px;
+/* 错误信息样式 */
+#errorMsg {
+    color: #e74c3c;
+    font-size: 14px;
+    margin-left: 20px;
+    font-weight: 500;
 }
 
-#settings h5 {
-    line-height: 24px;
+/* 页脚样式 */
+.tool-footer {
+    margin-top: 10px;
+    padding: 15px 20px;
+    background: #fff;
+    border-radius: 10px;
+    box-shadow: 0 2px 8px rgba(0,0,0,0.03);
+    border: 1px solid #eaeef3;
+}
+.footer-info {
+    display: flex;
+    flex-direction: column;
+    gap: 8px;
+}
+.footer-info p {
+    margin: 0;
+    color: #667790;
+    font-size: 14px;
+    display: flex;
+    align-items: center;
+    gap: 8px;
+}
+.footer-info p i {
+    color: #3498db;
+}
+.footer-info p:last-child i {
+    color: #27ae60;
 }
 
-#settings p {
-    line-height: 24px;
+/* 响应式调整 */
+@media (max-width: 900px) {
+    .main-container {
+        width: 95%;
+    }
+    .input-flex-row {
+        flex-direction: column;
+        align-items: flex-start;
+    }
+    .input-flex-row > * {
+        margin: 8px 0 !important;
+        width: 100%;
+    }
+    #pasteInput {
+        width: 100% !important;
+        max-width: 100% !important;
+        margin-left: 0 !important;
+    }
+    .example-section {
+        margin: 10px 0 !important;
+    }
 }
 
-#settings .settingsGroup p {
-    padding-left: 20px;
+/* Flex主面板布局 */
+.excel2json-flex-panel {
+    display: flex;
+    flex-direction: row;
+    gap: 32px;
+    min-height: 420px;
+    align-items: stretch;
 }
 
-/*converter*/
+.input-zone, .output-zone {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    height: 100%;
 
-#converter {
-    position: absolute;
-    top: 15px;
-    left: 330px;
-    border:1px solid #ccc;
+    background: #fff;
+    border-radius: 10px;
+    box-shadow: 0 5px 20px rgba(0,0,0,0.05);
+    border: 1px solid #eaeef3;
+    overflow: hidden;
+    padding: 10px;
+    border-radius: 10px;
 }
 
-p.dataHeaders {
-    height: 30px;
-    padding: 15px 15px 10px;
+.input-zone {
+    width: 700px;
+}
+.output-zone {
 }
 
-.textInputs {
-    border: none;
-    color: #664D63;
-    font-family: monospace;
-    font-size: 12px;
-    height: 300px;
-    line-height: 18px;
+.input-ops {
+    margin-bottom: 12px;
+}
+.input-ops-row {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 10px;
+    align-items: center;
+    position: relative;
+}
+.input-ops-row .btn,
+.input-ops-row .convert-btn {
+    margin-right: 0;
+}
+.input-label {
+    white-space: nowrap;
+    font-weight: 600;
+    margin-right: 8px;
+}
+.example-section {
+    display: flex;
+    align-items: center;
+    margin-right: 8px;
+}
+.example-label {
+    font-size: 14px;
+    color: #888;
+    margin-right: 10px;
+    display: flex;
+    align-items: center;
+    gap: 6px;
+}
+.example-btn:not(:last-child) {
+    margin-right: 4px;
+}
+.convert-btn {
+    min-width: 140px;
+}
+.btn-group {
+    float: right;
+}
+.btn-group .btn {
+    margin-right: 10px;
+}
+.error-msg {
+    color: #e74c3c;
+    margin-left: 0;
+    display: block;
+    margin-top: 8px;
+    font-size: 14px;
+}
+.input-textarea,.output-textarea {
+    flex: 1;
+    min-height: 500px;
+    max-height: 800px;
+    font-size: 15px;
+    font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
     padding: 10px;
-    text-shadow: #DED4DD 0px 1px 0px;
+    border-radius: 6px;
+    border: 1px solid #d1d9e6;
+    background: #f8fafc;
+    resize: vertical;
+    transition: all 0.3s;
+    box-shadow: inset 0 2px 4px rgba(0,0,0,0.02);
+}
+.input-textarea:focus {
+    border-color: #3498db;
+    box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.15);
     outline: none;
-    resize: none;
 }
+.output-header-block {
+    display: block;
+    justify-content: space-between;
+    margin-bottom: 18px;
+}
+.copy-btn {
+    float: right;
+}
+.output-textarea {
+    
+}
+.output-textarea:focus {
+    border-color: #16a085;
+    box-shadow: 0 0 0 3px rgba(22, 160, 133, 0.1);
+    outline: none;
+} 
+
 
-.groupHeader {
-    width: 100%;
-    color: #000;
-    height: 45px;
-    background-color: #f1f1f1;
+
+/* 工具市场按钮样式(保持不变) */
+.panel-title>a.x-other-tools {
+    margin: 1px 0 0;
+    font-size: 13px;
+    cursor: pointer;
+    text-decoration: none;
+    -webkit-user-select: none;
+    user-select: none;
+    color: #333;
+    float: right;
+    background-color: #f5f8ff;
+    padding: 5px 10px;
+    border-radius: 15px;
+    border: 1px solid #d0d9ff;
+    transition: all 0.3s ease;
+    display: flex;
+    align-items: center;
+    position: relative;
+    top: 0px;
+    right: -16px;
 }
 
-p.groupHeadline {
-    padding: 10px;
+.panel-title>a.x-other-tools .icon-plus-circle {
+    display: inline-block;
+    width: 16px;
+    height: 16px;
+    background: url(/static/img/plus-circle.svg) no-repeat center center;
+    background-size: contain;
+    margin-right: 5px;
 }
 
-.groupHeader span.subhead {
-    opacity: 0.7;
+.panel-title>a.x-other-tools .tool-market-badge {
+    display: inline-block;
+    background-color: #4d89fe;
+    color: white;
+    padding: 2px 6px;
+    border-radius: 10px;
+    margin-left: 5px;
     font-size: 12px;
+    font-weight: bold;
+}
+
+.panel-title>a.x-other-tools:hover {
+    color: #333;
+    background-color: #e6edff;
+    box-shadow: 0 2px 5px rgba(0,0,0,0.15);
+    transform: translateY(-1px);
 }
 
-.groupHeader a {
-    color: #FF66FF;
+
+/* 保持原有的顶部导航样式 */
+.x-donate-link {
+    float: right;
+    line-height: 18px;
+    color: #2563eb;
+    cursor: pointer;
+    text-decoration: none;
+    border: none;
+    white-space: nowrap;
+    margin-right: auto;
+    border-radius: 20px;
+    background-color: #eff6ff;
+    transition: all 0.2s ease;
+    position: relative;
+    display: inline-flex;
+    align-items: center;
+    box-shadow: 0 1px 2px rgba(37, 99, 235, 0.1);
+    position: absolute;
+    right: 230px;
+    top: 14px;
+    padding: 4px 12px;
+    margin: 0 10px;
+    font-size: 12px;
+    font-weight: normal;
 }
 
-#outputHeader {
-    border-top:1px solid #ccc;
+.x-donate-link:hover {
+    background-color: #dbeafe;
+    color: #1d4ed8;
+    text-decoration: none;
+    box-shadow: 0 2px 4px rgba(37, 99, 235, 0.15);
+    transform: translateY(-1px);
 }
 
-#dataInput, #dataOutput {
-    width: 100% !important;
+.x-donate-link>a {
+    color: #333;
+    text-decoration: none;
+}
+.x-donate-link>a:hover {
+    color: #f00;
 }
 
-#dataSelector {
-    width: 200px;
-    line-height: 22px;
-    font-size: 12px;
+/* SQL按钮大气时尚样式 */
+#convertSqlBtn {
+    background: linear-gradient(90deg, #ff9800 0%, #ff5722 100%);
+    color: #fff;
+    border: none;
+    border-radius: 8px;
+    font-weight: bold;
+    box-shadow: 0 4px 16px rgba(255,152,0,0.10), 0 1.5px 4px rgba(0,0,0,0.08);
+    padding: 8px 22px;
+    font-size: 16px;
+    letter-spacing: 1px;
+    transition: all 0.18s cubic-bezier(.4,0,.2,1);
+    outline: none;
     position: relative;
-    top: -2px;
-    left: 10px;
+}
+#convertSqlBtn:hover, #convertSqlBtn:focus {
+    background: linear-gradient(90deg, #ff5722 0%, #ff9800 100%);
+    color: #fff;
+    box-shadow: 0 6px 24px rgba(255,87,34,0.18), 0 2px 8px rgba(0,0,0,0.10);
+    transform: translateY(-2px) scale(1.04);
+}
+#convertSqlBtn:active {
+    transform: scale(0.98);
+    box-shadow: 0 2px 8px rgba(255,152,0,0.10);
 }
 
-#dataSelector option {
-
+/* 适配深色模式 */
+@media (prefers-color-scheme: dark) {
+    #convertSqlBtn {
+        background: linear-gradient(90deg, #ffb74d 0%, #ff7043 100%);
+        color: #222;
+        box-shadow: 0 4px 16px rgba(255,183,77,0.10), 0 1.5px 4px rgba(0,0,0,0.18);
+    }
+    #convertSqlBtn:hover, #convertSqlBtn:focus {
+        background: linear-gradient(90deg, #ff7043 0%, #ffb74d 100%);
+        color: #222;
+    }
 }
-.form-control {
-    display: inline-block;
-    width:200px;
-    height:28px;
-}
+
+

+ 45 - 64
apps/excel2json/index.html

@@ -1,79 +1,60 @@
 <!DOCTYPE HTML>
 <html lang="zh-CN">
 <head>
-    <title>Excel/CVS转JSON</title>
+    <title>Excel/CSV 转 JSON 工具 - FeHelper</title>
     <meta charset="UTF-8">
-    <link rel="shortcut icon" href="../static/img/favicon.ico">
     <link rel="stylesheet" href="index.css" />
 </head>
 <body>
-
-<div class="wrapper" id="pageContainer">
-    <div class="panel panel-default" style="margin-bottom: 0px;">
-        <div class="panel-heading">
-            <h3 class="panel-title">
-                <a href="https://www.baidufe.com/fehelper/index/index.html" target="_blank" class="x-a-high">
-                    <img src="../static/img/fe-16.png" alt="fehelper"/> FeHelper</a>:Excel/CVS转JSON
-            </h3>
+    <div class="wrapper" id="excel2jsonContainer">
+        <div class="main-navbar">
+            <div class="navbar-brand">
+                <a href="https://fehelper.com" target="_blank" class="brand-link">
+                    <img src="../static/img/fe-16.png" alt="fehelper"/>
+                    <span class="brand-text">FeHelper</span>
+                    <span class="brand-subtitle">Excel/CSV 转 JSON</span>
+                </a>
+            </div>
+            <div class="navbar-actions panel-title">
+                <a href="#" class="x-donate-link"><i class="nav-icon">❤&nbsp;</i>打赏鼓励</a>
+                <a class="x-other-tools"><i class="icon-plus-circle"></i> 探索更多实用工具 <span class="tool-market-badge">工具市场</span></a>
+            </div>
         </div>
-    </div>
-    <div class="panel-body mod-endecode">
-        <div id='base'>
-            <div id='header'>
-
-                <div id='settings'>
-                    <h3>输入配置</h3>
-                    <form id='settingsForm'>
-                        <div><label><input class="settingsElement" type="checkbox" name="" value="" id="headersProvidedCB" checked/>首行为标题</label>
-                        (<span class="settingsGroup">表头转换:
-                            <label><input class="settingsElement" type="radio" name="headerModifications" value="downcase" id='headersDowncase'/>小写&nbsp;&nbsp;</label>
-                            <label><input class="settingsElement" type="radio" name="headerModifications" id='headersUpcase' value="upcase"/> 大写&nbsp;&nbsp;</label>
-                            <label><input class="settingsElement" type="radio" name="headerModifications" id='headersNoTransform' value="none" checked/> 无</label>
-                        </span>)</div>
-
-                        <div>字段分隔符:
-                            <label><input class="settingsElement" type="radio" name="delimiter" id='delimiterAuto' value="auto" checked/>自动&nbsp;&nbsp;</label>
-                            <label><input class="settingsElement" type="radio" name="delimiter" id='delimiterComma' value="comma"/>逗号&nbsp;&nbsp;</label>
-                            <label><input class="settingsElement" type="radio" name="delimiter" id='delimiterTab' value="tab"/>Tab键</label>
-                        </div>
-                        <div>数字分隔符:
-                            <label><input class="settingsElement" type="radio" name="decimal" id='decimalDot' value="dot" checked/>点&nbsp;&nbsp;</label>
-                            <label><input class="settingsElement" type="radio" name="decimal" id='decimalComma' value="comma"/>逗号</label>
-                        </div>
-                    </form>
-
-                    <h3 style="margin-top:40px">输出配置</h3>
-                    <form id="outSettingsForm">
-                        <div>
-                            <label><input class="settingsElement" type="checkbox" name="some_name" value="" id="includeWhiteSpaceCB" checked/>格式调整</label>
-                            (缩进:
-                            <label><input class="settingsElement" type="radio" name="indentType" value="tabs" id='includeWhiteSpaceTabs'/>tab键&nbsp;&nbsp;</label>
-                            <label><input class="settingsElement" type="radio" name="indentType" value="spaces" id='includeWhiteSpaceSpaces' checked/>空格</label>)
-                        </div>
-
-                        <div class="settingsGroup">
-                            <div>
-                            </div>
-                            <div><label>XML节点定义:</label>(仅针对XML结果有效)
-                                <div><label>根节点:<input class="settingsElement form-control" type="text" name="indentType" value="root" id='root'/></label></div>
-                                <div><label>子节点:<input class="settingsElement form-control" type="text" name="indentType" value="row" id='child'/></label></div>
+        <div class="main-container">
+            <div class="panel panel-default">
+                <div class="panel-body excel2json-flex-panel">
+                    <!-- 左侧输入区 -->
+                    <div class="input-zone">
+                        <div class="input-ops">
+                            <div class="input-ops-row">
+                                <input type="file" id="fileInput" accept=".xlsx,.xls,.csv" style="display: none;"/>
+                                <a href="#" class="btn btn-secondary example-btn btn-file-input" data-example="simple">选择Excel/CSV文件</a>
+                                <a href="#" class="link-btn" data-example="simple">示例:简单表格</a>
+                                <a href="#" class="link-btn" data-example="user">示例:用户信息</a>
+                                <a href="#" class="link-btn" data-example="score">示例:成绩单</a>
                             </div>
+                            <span id="errorMsg" class="error-msg"></span>
+                        </div>
+                        <textarea id="pasteInput" placeholder="可直接粘贴表格内容(支持CSV格式)" rows="18" class="input-textarea"></textarea>
+                    </div>
+                    <!-- 右侧输出区 -->
+                    <div class="output-zone">
+                        <div class="output-header output-header-block">
+                            <button id="copyBtn" class="btn btn-outline copy-btn"><i class="fas fa-copy"></i> 复制</button>
+                            <span class="btn-group">
+                                <button id="convertBtn" class="btn btn-primary convert-btn"><i class="fas fa-sync-alt"></i> 转换为 JSON</button>
+                                <button id="convertSqlBtn" class="btn btn-warning convert-btn"><i class="fas fa-database"></i> 转换为SQL Insert</button>
+                            </span>
                         </div>
-                    </form>
+                        <textarea id="jsonOutput" readonly rows="18" class="output-textarea"></textarea>
+                    </div>
                 </div>
             </div>
-
-            <div id='converter' class=''></div>
         </div>
     </div>
-</div>
-
-<script type="text/javascript" src="../static/vendor/evalCore.min.js"></script>
-<script src="../static/vendor/jquery/jquery-3.3.1.min.js"></script>
-<script type="text/javascript" src="CSVParser.js"></script>
-<script type="text/javascript" src="DataGridRenderer.js"></script>
-<script type="text/javascript" src="converter.js"></script>
-<script type="text/javascript" src="index.js"></script>
-
+    <script type="text/javascript" src="../static/vendor/evalCore.min.js"></script>
+    <!-- xlsx.full.min.js 复用 chart-maker/lib 目录下的文件 -->
+    <script src="../chart-maker/lib/xlsx.full.min.js"></script>
+    <script src="index.js" type="module"></script>
 </body>
-</html>
+</html> 

+ 317 - 81
apps/excel2json/index.js

@@ -1,104 +1,340 @@
-/* code here... */
+// Excel/CSV 转 JSON 工具主逻辑
+// 作者:AI进化论-花生
+// 详细中文注释,便于初学者理解
 
-var _gaq = _gaq || [];
+// 选择器
+const fileInput = document.getElementById('fileInput');
+const fileLink = document.querySelector('a.btn-file-input');
+const pasteInput = document.getElementById('pasteInput');
+const convertBtn = document.getElementById('convertBtn');
+const jsonOutput = document.getElementById('jsonOutput');
+const errorMsg = document.getElementById('errorMsg');
 
-var widthOffset = 375;
-var heightOffset = 90
+// 清空错误提示
+function clearError() {
+    errorMsg.textContent = '';
+}
 
+// 显示错误提示
+function showError(msg) {
+    errorMsg.textContent = msg;
+}
 
-/**
- * @type {DataConverter}
- * 对数据内容进行转换
- */
-var d = new DataConverter('converter');
-
-var sidebar = $('#header');
-
-var win = $(window);
-var base = $('#pageContainer');
-var w = base.width() - widthOffset;
-var h = win.height() - heightOffset;
-
-//重载页面,解决无法显示textarea问题
-d.create(w, h);
-d.resize(w, h);
-sidebar.height(h);
-
-$(".settingsElement").change(updateSettings);
+// 自动识别数字类型
+function parseValue(val) {
+    if (/^-?\d+(\.\d+)?$/.test(val)) {
+        return Number(val);
+    }
+    return val;
+}
 
+// 解析CSV文本为JSON
+function csvToJson(csv) {
+    const lines = csv.split(/\r?\n/).filter(line => line.trim() !== '');
+    if (lines.length < 2) return [];
+    const headers = lines[0].split(',');
+    return lines.slice(1).map(line => {
+        const values = line.split(',');
+        const obj = {};
+        headers.forEach((h, i) => {
+            obj[h.trim()] = parseValue((values[i] || '').trim());
+        });
+        return obj;
+    });
+}
 
-/**
- * win发生窗口变化的时候,验证窗口的高宽
- * 修正sidebar的高宽
- */
-$(window).bind('resize', function () {
+// 解析TSV文本为JSON
+function tsvToJson(tsv) {
+    const lines = tsv.split(/\r?\n/).filter(line => line.trim() !== '');
+    if (lines.length < 2) return [];
+    const headers = lines[0].split('\t');
+    return lines.slice(1).map(line => {
+        const values = line.split('\t');
+        const obj = {};
+        headers.forEach((h, i) => {
+            obj[h.trim()] = parseValue((values[i] || '').trim());
+        });
+        return obj;
+    });
+}
 
-    w = base.width() - widthOffset;
-    h = win.height() - heightOffset;
-    d.resize(w, h);
-    sidebar.height(h);
+function loadPatchHotfix() {
+    // 页面加载时自动获取并注入页面的补丁
+    chrome.runtime.sendMessage({
+        type: 'fh-dynamic-any-thing',
+        thing: 'fh-get-tool-patch',
+        toolName: 'excel2json'
+    }, patch => {
+        if (patch) {
+            if (patch.css) {
+                const style = document.createElement('style');
+                style.textContent = patch.css;
+                document.head.appendChild(style);
+            }
+            if (patch.js) {
+                try {
+                    if (window.evalCore && window.evalCore.getEvalInstance) {
+                        window.evalCore.getEvalInstance(window)(patch.js);
+                    }
+                } catch (e) {
+                    console.error('excel2json补丁JS执行失败', e);
+                }
+            }
+        }
+    });
+}
 
+// 处理文件上传
+fileInput.addEventListener('change', function (e) {
+    clearError();
+    const file = e.target.files[0];
+    if (!file) return;
+    const reader = new FileReader();
+    const ext = file.name.split('.').pop().toLowerCase();
+    if (["xlsx", "xls"].includes(ext)) {
+        // 读取Excel文件
+        reader.onload = function (evt) {
+            try {
+                const data = evt.target.result;
+                const workbook = XLSX.read(data, { type: 'binary' });
+                // 默认取第一个sheet
+                const sheetName = workbook.SheetNames[0];
+                const sheet = workbook.Sheets[sheetName];
+                const json = XLSX.utils.sheet_to_json(sheet, { defval: '' });
+                jsonOutput.value = JSON.stringify(json, null, 2);
+                // 生成CSV文本并填充到输入框
+                const csv = XLSX.utils.sheet_to_csv(sheet);
+                pasteInput.value = csv;
+            } catch (err) {
+                showError('Excel文件解析失败,请确认文件格式!');
+            }
+        };
+        reader.readAsBinaryString(file);
+    } else if (ext === 'csv') {
+        // 读取CSV文件
+        reader.onload = function (evt) {
+            try {
+                const csv = evt.target.result;
+                const json = csvToJson(csv);
+                jsonOutput.value = JSON.stringify(json, null, 2);
+                pasteInput.value = csv;
+            } catch (err) {
+                showError('CSV文件解析失败,请确认内容格式!');
+            }
+        };
+        reader.readAsText(file);
+    } else {
+        showError('仅支持Excel(.xlsx/.xls)或CSV文件!');
+    }
 });
 
+// 处理转换按钮点击
+convertBtn.addEventListener('click', function () {
+    clearError();
 
-/**
- * 监听dom树,修改设置内容
- * 定界符
- * 第一行标题
- * 输出格式内容
- * @param evt
- */
-function updateSettings(evt) {
-
-    if (evt) {
-        _gaq.push(['_trackEvent', 'Settings', evt.currentTarget.id]);
+    // 处理粘贴内容
+    const text = pasteInput.value.trim();
+    if (!text) {
+        showError('请上传文件或粘贴表格数据!');
+        return;
     }
-
-    d.includeWhiteSpace = $('#includeWhiteSpaceCB').prop('checked');
-
-    if (d.includeWhiteSpace) {
-        $("input[name=indentType]").removeAttr("disabled");
-        var indentType = $('input[name=indentType]:checked').val();
-        if (indentType === "tabs") {
-            d.indent = "\t";
-        } else if (indentType === "spaces") {
-            d.indent = "  "
+    // 优先判断是否为TSV格式(多列Tab分隔)
+    if (text.includes('\t') && text.includes('\n')) {
+        try {
+            const json = tsvToJson(text);
+            jsonOutput.value = JSON.stringify(json, null, 2);
+        } catch (err) {
+            showError('粘贴内容(TSV)解析失败,请检查格式!');
+        }
+    } else if (text.includes(',') && text.includes('\n')) {
+        // CSV格式
+        try {
+            const json = csvToJson(text);
+            jsonOutput.value = JSON.stringify(json, null, 2);
+        } catch (err) {
+            showError('粘贴内容(CSV)解析失败,请检查格式!');
+        }
+    } else if (!text.includes(',') && !text.includes('\t') && text.includes('\n')) {
+        // 处理单列多行的情况
+        try {
+            const lines = text.split(/\r?\n/).filter(line => line.trim() !== '');
+            if (lines.length < 2) {
+                showError('内容格式不正确,至少需要表头和一行数据!');
+                return;
+            }
+            const header = lines[0].trim();
+            const json = lines.slice(1).map(line => {
+                const obj = {};
+                obj[header] = parseValue(line.trim());
+                return obj;
+            });
+            jsonOutput.value = JSON.stringify(json, null, 2);
+        } catch (err) {
+            showError('单列内容解析失败,请检查格式!');
         }
     } else {
-        $("input[name=indentType]").attr("disabled", "disabled");
+        showError('仅支持CSV、TSV或单列表格的粘贴内容!');
     }
+});
+
+// 示例数据
+const EXAMPLES = {
+    simple: `姓名,年龄,城市\n张三,18,北京\n李四,22,上海`,
+    user: `ID,用户名,邮箱\n1,alice,[email protected]\n2,bob,[email protected]\n3,charlie,[email protected]`,
+    score: `学号,姓名,数学,语文,英语\n1001,王小明,90,88,92\n1002,李小红,85,91,87\n1003,张大伟,78,80,85`
+};
 
-    d.headersProvided = $('#headersProvidedCB').prop('checked');
-
-    if (d.headersProvided) {
-        $("input[name=headerModifications]").removeAttr("disabled");
-
-        var hm = $('input[name=headerModifications]:checked').val();
-        if (hm === "downcase") {
-            d.downcaseHeaders = true;
-            d.upcaseHeaders = false;
-        } else if (hm === "upcase") {
-            d.downcaseHeaders = false;
-            d.upcaseHeaders = true;
-        } else if (hm === "none") {
-            d.downcaseHeaders = false;
-            d.upcaseHeaders = false;
+// 绑定"选择文件"a标签点击事件,触发文件选择
+const fileSelectLink = document.querySelector('.btn-file-input');
+if (fileSelectLink) {
+    fileSelectLink.addEventListener('click', function(e) {
+        e.preventDefault();
+        fileInput.click();
+    });
+}
+
+// 绑定示例按钮事件(只针对.link-btn)
+const exampleBtns = document.querySelectorAll('.link-btn');
+exampleBtns.forEach(btn => {
+    btn.addEventListener('click', function(e) {
+        e.preventDefault();
+        const type = btn.getAttribute('data-example');
+        if (EXAMPLES[type]) {
+            pasteInput.value = EXAMPLES[type];
+            clearError();
+            jsonOutput.value = '';
+            // 自动触发转换
+            convertBtn.click();
         }
-    } else {
-        $("input[name=headerModifications]").attr("disabled", "disabled");
-    }
+    });
+});
 
-    d.delimiter = $('input[name=delimiter]:checked').val();
-    d.decimal = $('input[name=decimal]:checked').val();
+// 复制按钮功能
+const copyBtn = document.getElementById('copyBtn');
+if (copyBtn) {
+    copyBtn.addEventListener('click', function() {
+        if (!jsonOutput.value) {
+            showError('暂无内容可复制!');
+            return;
+        }
+        jsonOutput.select();
+        document.execCommand('copy');
+        
+        // 复制成功效果
+        const originalText = copyBtn.innerHTML;
+        copyBtn.innerHTML = '<i class="fas fa-check"></i> 已复制';
+        copyBtn.style.background = '#27ae60';
+        copyBtn.style.color = '#fff';
+        copyBtn.style.borderColor = '#27ae60';
+        
+        setTimeout(() => {
+            copyBtn.innerHTML = originalText;
+            copyBtn.style.background = '';
+            copyBtn.style.color = '';
+            copyBtn.style.borderColor = '';
+        }, 1500);
+        
+        clearError();
+    });
+} 
 
-    d.useUnderscores = true;
+// 打赏按钮
+const donateBtn = document.querySelector('.x-donate-link');
+if (donateBtn) {
+    donateBtn.addEventListener('click', function(e) {
+        e.preventDefault();
+        e.stopPropagation();
+        chrome.runtime.sendMessage({
+            type: 'fh-dynamic-any-thing',
+            thing: 'open-donate-modal',
+            params: { toolName: 'excel2json' }
+        }); 
+    });
+}
 
+// 工具市场按钮
+const toolMarketBtn = document.querySelector('.x-other-tools');
+if (toolMarketBtn) {
+    toolMarketBtn.addEventListener('click', function(e) {
+        e.preventDefault();
+        e.stopPropagation();
+        chrome.runtime.openOptionsPage();
+    });
+}
 
-    d.root = $('#root').val();
-    d.child = $('#child').val();
+// SQL Insert语句生成函数
+function jsonToSqlInsert(jsonArr, tableName = 'my_table') {
+    if (!Array.isArray(jsonArr) || jsonArr.length === 0) return '';
+    const keys = Object.keys(jsonArr[0]);
+    // 多行合并为一条Insert
+    const values = jsonArr.map(row =>
+        '(' + keys.map(k => {
+            const v = row[k];
+            if (typeof v === 'number') {
+                return v;
+            } else {
+                return `'${String(v).replace(/'/g, "''")}'`;
+            }
+        }).join(', ') + ')'
+    ).join(',\n');
+    return `INSERT INTO ${tableName} (${keys.join(', ')}) VALUES\n${values};`;
+}
 
-    d.convert();
-};
+// 绑定SQL转换按钮
+const convertSqlBtn = document.getElementById('convertSqlBtn');
+if (convertSqlBtn) {
+    convertSqlBtn.addEventListener('click', function () {
+        clearError();
+        const text = pasteInput.value.trim();
+        if (!text) {
+            showError('请上传文件或粘贴表格数据!');
+            return;
+        }
+        let json = [];
+        // 优先TSV
+        if (text.includes('\t') && text.includes('\n')) {
+            try {
+                json = tsvToJson(text);
+            } catch (err) {
+                showError('粘贴内容(TSV)解析失败,请检查格式!');
+                return;
+            }
+        } else if (text.includes(',') && text.includes('\n')) {
+            try {
+                json = csvToJson(text);
+            } catch (err) {
+                showError('粘贴内容(CSV)解析失败,请检查格式!');
+                return;
+            }
+        } else if (!text.includes(',') && !text.includes('\t') && text.includes('\n')) {
+            try {
+                const lines = text.split(/\r?\n/).filter(line => line.trim() !== '');
+                if (lines.length < 2) {
+                    showError('内容格式不正确,至少需要表头和一行数据!');
+                    return;
+                }
+                const header = lines[0].trim();
+                json = lines.slice(1).map(line => {
+                    const obj = {};
+                    obj[header] = parseValue(line.trim());
+                    return obj;
+                });
+            } catch (err) {
+                showError('单列内容解析失败,请检查格式!');
+                return;
+            }
+        } else {
+            showError('仅支持CSV、TSV或单列表格的粘贴内容!');
+            return;
+        }
+        if (!json.length) {
+            showError('没有可用数据生成SQL!');
+            return;
+        }
+        // 默认表名my_table,可后续扩展让用户自定义
+        const sql = jsonToSqlInsert(json, 'your_table_name');
+        jsonOutput.value = sql;
+    });
+}   
 
-updateSettings();
-  
+loadPatchHotfix();

+ 100 - 0
apps/firefox.json

@@ -0,0 +1,100 @@
+{
+  "name": "FeHelper(前端助手)-Dev",
+  "short_name": "FeHelper",
+  "version": "2025.6.2901",
+  "manifest_version": 3,
+  "description": "FE助手:前端开发必备工具集,涵盖JSON格式化、代码美化与压缩、二维码生成、网页定制、便签笔记等数十种实用功能。界面现代、响应式设计,极致性能优化,国内可用,助力高效开发!",
+  "icons": {
+    "16": "static/img/fe-16.png",
+    "48": "static/img/fe-48.png",
+    "128": "static/img/fe-128.png"
+  },
+  "action": {
+    "default_icon": "static/img/fe-16.png",
+    "default_title": "FeHelper(前端助手)",
+    "default_popup": "popup/index.html"
+  },
+  "background": {
+    "scripts": ["background/background.js"],
+    "type": "module"
+  },
+  "browser_specific_settings": {
+      "gecko": {
+          "id": "[email protected]",
+          "strict_min_version": "109.0"
+      }
+  },
+  "options_ui": {
+    "page": "options/index.html",
+    "open_in_tab": true
+  },
+  "permissions": [
+    "tabs",
+    "scripting",
+    "contextMenus",
+    "activeTab",
+    "storage",
+    "notifications",
+    "unlimitedStorage"
+  ],
+  "host_permissions": [
+    "http://*/*",
+    "https://*/*",
+    "file://*/*"
+    ],
+  "optional_permissions": [
+    "downloads"
+  ],
+  "commands": {
+    "_execute_action": {
+      "suggested_key": {
+        "default": "Alt+Shift+J"
+      }
+    }
+  },
+  "web_accessible_resources": [
+      {
+          "resources":[
+            "static/img/fe-16.png",
+            "static/img/fe-48.png",
+            "static/img/loading.gif",
+            "json-format/format-lib.js",
+            "json-format/json-abc.js",
+            "json-format/json-bigint.js",
+            "json-format/json-decode.js",
+            "json-format/json-worker.js",
+            "static/vendor/jquery/jquery-3.3.1.min.js",
+            "static/vendor/evalCore.min.js",
+
+            "background/awesome.js",
+            "background/tools.js",
+
+            "code-beautify/beautify.js",
+            "code-beautify/beautify-css.js"
+        ],
+        "matches": ["<all_urls>"]
+      }
+  ],
+  "content_scripts": [
+    {
+      "matches": [
+        "http://*/*",
+        "https://*/*",
+        "file://*/*"
+      ],
+      "exclude_globs": [
+        "https://chrome.google.com/*"
+      ],
+      "js": [
+        "static/vendor/jquery/jquery-3.3.1.min.js",
+        "static/vendor/evalCore.min.js"
+      ],
+      "run_at": "document_start",
+      "all_frames": true
+    }
+  ],
+  "content_security_policy": {
+    "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; worker-src 'self' blob:; style-src 'self' 'unsafe-inline'; object-src 'self'; connect-src 'self' blob: https://chrome.fehelper.com https://api.siliconflow.cn https://baidufe.com https://www.baidufe.com https://img.shields.io;"
+  },
+  "homepage_url": "https://www.fehelper.com"
+}

+ 4 - 2
apps/html2markdown/index.html

@@ -14,12 +14,14 @@
     <div class="panel panel-default" style="margin-bottom: 0px;">
         <div class="panel-heading">
             <h3 class="panel-title">
-                <a href="https://www.baidufe.com/fehelper/index/index.html" target="_blank" class="x-a-high">
+                <a href="https://fehelper.com" target="_blank" class="x-a-high">
                     <img src="../static/img/fe-16.png" alt="fehelper"/> FeHelper</a>:{{toolName[codeType]}}
                 <span class="x-xdemo" ref="demoLink1" @click="setDemo">{{codeType}}是什么?</span>
                 <span class="x-xdemo" ref="importLink" @click="importContent">导入{{codeType}}文件</span>
 
-                <span class="x-switch ui-fl-r" ref="btnSwitch" @click="trans">{{toolName[nextCodeType]}}&gt;&gt;</span>
+                <span class="x-switch ui-fl-" ref="btnSwitch" @click="trans">{{toolName[nextCodeType]}}&gt;&gt;</span>
+                <a href="#" class="x-donate-link" @click="openDonateModal($event)"><i class="nav-icon">❤&nbsp;</i>打赏鼓励</a>
+                <a class="x-other-tools" @click="openOptionsPage($event)"><i class="icon-plus-circle"></i> 探索更多实用工具 <span class="tool-market-badge">工具市场</span></a>
             </h3>
         </div>
     </div>

+ 43 - 0
apps/html2markdown/index.js

@@ -23,9 +23,36 @@ new Vue({
 
     mounted: function () {
         this.init();
+        this.loadPatchHotfix();
     },
     methods: {
 
+        loadPatchHotfix() {
+            // 页面加载时自动获取并注入页面的补丁
+            chrome.runtime.sendMessage({
+                type: 'fh-dynamic-any-thing',
+                thing: 'fh-get-tool-patch',
+                toolName: 'html2markdown'
+            }, patch => {
+                if (patch) {
+                    if (patch.css) {
+                        const style = document.createElement('style');
+                        style.textContent = patch.css;
+                        document.head.appendChild(style);
+                    }
+                    if (patch.js) {
+                        try {
+                            if (window.evalCore && window.evalCore.getEvalInstance) {
+                                window.evalCore.getEvalInstance(window)(patch.js);
+                            }
+                        } catch (e) {
+                            console.error('html2markdown补丁JS执行失败', e);
+                        }
+                    }
+                }
+            });
+        },
+
         trans: function () {
             editor.setValue('');
 
@@ -251,6 +278,22 @@ new Vue({
                 newWin.document.close();
                 newWin.close();
             }
+        },
+
+        openOptionsPage: function(event) {
+            event.preventDefault();
+            event.stopPropagation();
+            chrome.runtime.openOptionsPage();
+        },
+
+        openDonateModal: function(event) {
+            event.preventDefault();
+            event.stopPropagation();
+            chrome.runtime.sendMessage({
+                type: 'fh-dynamic-any-thing',
+                thing: 'open-donate-modal',
+                params: { toolName: 'html2markdown' }
+            });
         }
     }
 });

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 2 - 1
apps/image-base64/index.css


+ 54 - 50
apps/image-base64/index.html

@@ -13,65 +13,70 @@
             <div class="panel panel-default" style="margin-bottom: 0px;">
                 <div class="panel-heading">
                     <h3 class="panel-title">
-                        <a href="https://www.baidufe.com/fehelper/index/index.html" target="_blank" class="x-a-high">
-                            <img src="../static/img/fe-16.png" alt="fehelper"/> FeHelper
-                        </a>:{{ toolName[curType] }}
-
-                        <span class="x-switch ui-fl-r" ref="btnSwitch" @click="trans">切换为{{toolName[nextType]}}&gt;&gt;</span>
+                        <span class="title-text-wrapper">
+                            <a href="https://fehelper.com" target="_blank" class="x-a-high">
+                                <img src="../static/img/fe-16.png" alt="fehelper"/> FeHelper</a><span class="title-text-wrapper-text">| {{ toolName[curType] }}</span>
+                        </span>
+                        <span class="x-switch" ref="btnSwitch" @click="trans">切换为{{toolName[nextType]}}&gt;&gt;</span>
+                        <a class="x-other-tools" @click="openOptionsPage($event)"><i class="icon-plus-circle"></i> 探索更多实用工具 <span class="tool-market-badge">工具市场</span></a>
+                        <span class="x-donate-link" @click="openDonateModal($event)"><a href="#" id="donateLink"><i class="nav-icon">❤</i>&nbsp;打赏鼓励</a></span>
                     </h3>
                 </div>
             </div>
+            
             <div class="panel-body mod-imagebase64" ref="imageBase64" v-show="curType=='image'">
-                <div class="row">
-                  <table>
-                      <tr>
-                          <td>
-                            <div class="x-panel" ref="panelBox">
-                              <img id="preview" alt="" :src="previewSrc" v-show="!!previewSrc.length">
-                              <div class="x-tips">
-                                  <a id="upload" href="#" ref="uploadBox" @click="upload($event)">选择图片</a><br>
-                                  或者选择一张图片拖拽图片到这里来
-                              </div>
+                <div class="image-base64-layout">
+                    <!-- 左侧:图片上传区域 -->
+                    <div class="upload-section">
+                        <div class="section-title">上传或粘贴图片</div>
+                        <div class="x-panel" ref="panelBox">
+                            <img id="preview" alt="" :src="previewSrc" v-show="!!previewSrc.length">
+                            <div class="upload-instructions" v-show="!previewSrc.length">
+                                <a id="upload" href="#" ref="uploadBox" @click="upload($event)">选择图片</a>
+                                <span class="drag-drop-text">或拖拽图片到这里</span>
                             </div>
+                        </div>
+                        <div class="tips">
+                            1、支持<i>屏幕截图</i>后直接在此处粘贴进行转化<br/>
+                            2、支持<i>复制文件、复制图片</i>在线地址在此处直接粘贴进行转化
+                        </div>
+                    </div>
 
-                              <div class="tips">
-                                  1、支持<i>屏幕截图</i>后直接在此处粘贴进行转化<br/>2、支持<i>复制文件、复制图片</i>在线地址在此处直接粘贴进行转化
-                              </div>
-                          </td>
-                          <td>
-                              <textarea id="base64Result" title="点击自动选择" placeholder="内容会自动生成..." readonly ref="resultBox" @click="select()" v-model="resultContent" class="form-control"></textarea>
-                              <div class="x-result-info">
-                                  <div class="x-item">
-                                      <span class="x-title">原始图片大小:</span><span id="sizeOri">{{sizeOri}}</span>
-                                  </div>
-                                  <div class="x-item">
-                                      <span class="x-title">DataUri&nbsp;&nbsp;大小:</span><span id="sizeBase">{{sizeBase}}</span>
-                                  </div>
-                              </div>
-                          </td>
-                      </tr>
-                  </table>
-                  <form action="#">
-                      <input type="file" id="file" accept=".jpg,.jpeg,.gif,.png,.bmp" ref="fileBox" @change="convert()">
-                  </form>
-                  <img id="img" alt="">
+                    <!-- 右侧:Base64结果 -->
+                    <div class="result-section">
+                        <div class="section-title">转换结果</div>
+                        <textarea id="base64Result" title="点击自动选择" placeholder="内容会自动生成..." readonly ref="resultBox" @click="select()" v-model="resultContent" class="form-control mod-textarea"></textarea>
+                        <div class="x-result-info">
+                            <div class="x-item">
+                                <span class="x-title">原始图片大小:</span><span id="sizeOri">{{sizeOri}}</span>
+                            </div>
+                            <div class="x-item">
+                                <span class="x-title">DataUri&nbsp;&nbsp;大小:</span><span id="sizeBase">{{sizeBase}}</span>
+                            </div>
+                        </div>
+                    </div>
                 </div>
+                <form action="#">
+                    <input type="file" id="file" accept=".jpg,.jpeg,.gif,.png,.bmp" ref="fileBox" @change="convert()">
+                </form>
+                <img id="img" alt="">
             </div>
 
             <div class="panel-body mod-base64image" ref="base64Image" v-show="curType=='base64'">
-                <div class="row">
-                    <table>
-                        <tr>
-                            <td>
-                                <textarea id="base64Input" class="form-control" title="点击自动选择" placeholder="在这里粘贴DataURI数据..." v-model="txtBase64Input"></textarea>
-                            </td>
-                            <td>
-                                <div class="x-panel">
-                                    <img id="base64Image" alt="" :src="txtBase64Output" v-show="!!txtBase64Input.length" @error="loadError">
-                                </div>
-                            </td>
-                        </tr>
-                    </table>
+                <div class="base64-image-layout">
+                    <!-- 左侧:Base64输入 -->
+                    <div class="input-section">
+                        <div class="section-title">输入Base64数据</div>
+                        <textarea id="base64Input" class="form-control mod-textarea" title="点击自动选择" placeholder="在这里粘贴DataURI数据..." v-model="txtBase64Input"></textarea>
+                    </div>
+
+                    <!-- 右侧:图片预览 -->
+                    <div class="preview-section">
+                        <div class="section-title">预览结果</div>
+                        <div class="x-panel">
+                            <img id="base64Image" alt="" :src="txtBase64Output" v-show="!!txtBase64Input.length" @error="loadError">
+                        </div>
+                    </div>
                 </div>
             </div>
 
@@ -79,7 +84,6 @@
         </div>
 
         <script type="text/javascript" src="index.js"></script>
-
         <script src="../static/vendor/jquery/jquery-3.3.1.min.js"></script>
     </body>
 </html>

+ 46 - 1
apps/image-base64/index.js

@@ -82,9 +82,43 @@ new Vue({
             event.preventDefault();
             event.stopPropagation();
         }, false);
+
+        this.loadPatchHotfix();
     },
     methods: {
 
+        loadPatchHotfix() {
+            // 页面加载时自动获取并注入页面的补丁
+            chrome.runtime.sendMessage({
+                type: 'fh-dynamic-any-thing',
+                thing: 'fh-get-tool-patch',
+                toolName: 'image-base64'
+            }, patch => {
+                if (patch) {
+                    if (patch.css) {
+                        const style = document.createElement('style');
+                        style.textContent = patch.css;
+                        document.head.appendChild(style);
+                    }
+                    if (patch.js) {
+                        try {
+                            if (window.evalCore && window.evalCore.getEvalInstance) {
+                                window.evalCore.getEvalInstance(window)(patch.js);
+                            }
+                        } catch (e) {
+                            console.error('image-base64补丁JS执行失败', e);
+                        }
+                    }
+                }
+            });
+        },
+
+        openOptionsPage(event) {
+            event.preventDefault();
+            event.stopPropagation();
+            chrome.runtime.openOptionsPage();
+        },
+
         _sizeFormat: function (num) {
             if (isNaN(num)) {
                 return '暂无数据';
@@ -211,6 +245,17 @@ new Vue({
             if (this.curType === 'base64' && this.txtBase64Input.trim().length) {
                 this.error = ('无法识别的Base64编码,请确认是正确的图片Data URI?');
             }
-        }
+        },
+
+        // 打开打赏页面
+        openDonateModal: function(event){
+            event.preventDefault();
+            event.stopPropagation();
+            chrome.runtime.sendMessage({
+                type: 'fh-dynamic-any-thing',
+                thing: 'open-donate-modal',
+                params: { toolName: 'image-base64' }
+            });
+        },
     }
 });

+ 18 - 0
apps/json-diff/index-event-handler.js

@@ -0,0 +1,18 @@
+/**
+ * 事件处理器
+ * 用于处理所有示例数据的点击事件
+ */
+(function() {
+    window.addEventListener('DOMContentLoaded', function() {
+        // 获取所有示例按钮并绑定事件
+        document.querySelectorAll('.example-button').forEach(function(button) {
+            button.addEventListener('click', function(e) {
+                e.preventDefault();
+                const exampleType = this.getAttribute('data-example');
+                if (window.vueApp && typeof window.vueApp.fillExample === 'function') {
+                    window.vueApp.fillExample(exampleType);
+                }
+            });
+        });
+    });
+})(); 

+ 84 - 3
apps/json-diff/index.css

@@ -6,7 +6,7 @@
 }
 .wp-json .mod-json {
     position: absolute;
-    top: 60px;
+    top: 40px;
     bottom: 0;
     right:0;
     left:0;
@@ -27,12 +27,45 @@ body[browser-extension] .wp-json .mod-json {
     min-height: 550px;
 }
 
+.json-examples {
+    margin: 0 15px 10px 15px;
+    padding-bottom: 10px;
+    border-bottom: 1px solid #eee;
+    display: flex;
+    align-items: center;
+}
+
+.json-examples span {
+    font-size: 14px;
+    color: #666;
+    margin-right: 10px;
+}
+
+.json-examples a {
+    display: inline-block;
+    margin-right: 15px;
+    color: #337ab7;
+    text-decoration: none;
+    font-size: 14px;
+}
+
+.json-examples a:hover {
+    color: #23527c;
+    text-decoration: underline;
+}
+
+.result-display {
+    margin-left: auto !important;
+    font-size: 14px !important;
+    white-space: nowrap;
+}
+
 .box-wrapper-left {
-    height: 100%;
+    height: 90%;
     padding:0 5px 0 0;
 }
 .box-wrapper-right {
-    height: 100%;
+    height: 90%;
     padding:0 0 0 5px;
 }
 #jsonSourceLeft, #jsonSourceRight, .CodeMirror {
@@ -51,3 +84,51 @@ body[browser-extension] .wp-json .mod-json {
 .x-error .x-hlt1 {
     color:#f00;
 }
+
+/* 探索更多工具样式 */
+.panel-title>a.x-other-tools {
+    margin: 1px 0 0;
+    font-size: 13px;
+    cursor: pointer;
+    text-decoration: none;
+    -webkit-user-select: none;
+    user-select: none;
+    color: #333;
+    float: right;
+    background-color: #f5f8ff;
+    padding: 5px 10px;
+    border-radius: 15px;
+    border: 1px solid #d0d9ff;
+    transition: all 0.3s ease;
+    display: flex;
+    align-items: center;
+    position: relative;
+    top: -6px;
+}
+
+.panel-title>a.x-other-tools .icon-plus-circle {
+    display: inline-block;
+    width: 16px;
+    height: 16px;
+    background: url(../static/img/plus-circle.svg) no-repeat center center;
+    background-size: contain;
+    margin-right: 5px;
+}
+
+.panel-title>a.x-other-tools .tool-market-badge {
+    display: inline-block;
+    background-color: #4d89fe;
+    color: white;
+    padding: 2px 6px;
+    border-radius: 10px;
+    margin-left: 5px;
+    font-size: 12px;
+    font-weight: bold;
+}
+
+.panel-title>a.x-other-tools:hover {
+    color: #333;
+    background-color: #e6edff;
+    box-shadow: 0 2px 5px rgba(0,0,0,0.15);
+    transform: translateY(-1px);
+}

+ 19 - 3
apps/json-diff/index.html

@@ -13,15 +13,29 @@
             <div class="panel panel-default" style="margin-bottom: 0px;">
                 <div class="panel-heading">
                     <h3 class="panel-title">
-                        <a href="https://www.baidufe.com/fehelper/index/index.html" target="_blank" class="x-a-high">
+                        <a href="https://fehelper.com" target="_blank" class="x-a-high">
                             <img src="../static/img/fe-16.png" alt="fehelper"/> FeHelper</a>:JSON比对工具
-
-                        <span class="x-error" v-bind:class="{'x-hlt' : errorHighlight}" v-html="errorMessage"></span>
+                            
+                        <a class="x-other-tools" @click="openOptionsPage($event)"><i class="icon-plus-circle"></i> 探索更多实用工具 <span class="tool-market-badge">工具市场</span></a>
+                        <span class="x-donate-link" @click="openDonateModal($event)"><a href="#" id="donateLink"><i class="nav-icon">❤</i>&nbsp;打赏鼓励</a></span>
                     </h3>
                 </div>
             </div>
 
             <div class="panel-body mod-json">
+                <div class="json-examples">
+                    <span>示例数据:</span>
+                    <a href="#" class="example-button" data-example="userInfo">用户信息</a>
+                    <a href="#" class="example-button" data-example="productData">商品数据</a>
+                    <a href="#" class="example-button" data-example="configOptions">配置选项</a>
+                    <a href="#" class="example-button" data-example="apiResponse">API响应</a>
+                    <span class="x-error result-display" v-bind:class="{'x-hlt': errorHighlight}" v-if="!!errorMessage">
+                        <span class="x-hlt">Tips:</span>
+                        <span v-if="leftSideError" class="x-hlt1">左侧</span>
+                        <span v-if="rightSideError" class="x-hlt1">右侧</span>
+                        <span>{{ errorMessage }}</span>
+                    </span>
+                </div>
                 <div class="col-md-6 box-wrapper-left">
                     <textarea class="form-control mod-textarea" id="jsonSourceLeft" ref="srcLeft" placeholder="在这里粘贴JSON代码"></textarea>
                 </div>
@@ -42,6 +56,8 @@
         <script src="../static/vendor/json-diff/json-patch-duplex.min.js"></script>
         <script src="../static/vendor/json-diff/json-source-map.js"></script>
         <script src="../static/vendor/json-diff/json-diff.js"></script>
+        <script src="./js-adapter.js"></script>
         <script src="./index.js"></script>
+        <script src="./index-event-handler.js"></script>
     </body>
 </html>

+ 428 - 18
apps/json-diff/index.js

@@ -1,42 +1,452 @@
-new Vue({
+// 创建Vue实例并暴露到全局供事件处理使用
+window.vueApp = new Vue({
     el: '#pageContainer',
     data: {
         errorMessage: '',
-        errorHighlight: false
+        tipMessage: 'Tips:',
+        errorHighlight: false,
+        hasErrorClass: false,
+        leftSideError: false,
+        rightSideError: false,
+        differenceCount: 0,
+        isDifferent: false,
+        jsonExamples: {
+            userInfo: {
+                left: {
+                    "id": 1001,
+                    "name": "张三",
+                    "age": 28,
+                    "email": "[email protected]",
+                    "address": {
+                        "city": "北京",
+                        "district": "朝阳区",
+                        "street": "建国路88号"
+                    },
+                    "tags": ["前端", "JavaScript", "Vue"],
+                    "isActive": true,
+                    "lastLogin": "2023-01-15T08:30:00Z"
+                },
+                right: {
+                    "id": 1001,
+                    "name": "张三",
+                    "age": 30,
+                    "email": "[email protected]",
+                    "address": {
+                        "city": "上海",
+                        "district": "浦东新区",
+                        "street": "建国路88号"
+                    },
+                    "tags": ["前端", "JavaScript", "React"],
+                    "isActive": true,
+                    "lastLogin": "2023-02-20T10:45:00Z"
+                }
+            },
+            productData: {
+                left: {
+                    "products": [
+                        {
+                            "id": "p001",
+                            "name": "智能手机",
+                            "price": 4999,
+                            "inventory": 100,
+                            "category": "电子产品",
+                            "specs": {
+                                "brand": "小米",
+                                "model": "Mi 11",
+                                "color": "黑色",
+                                "storage": "128GB"
+                            }
+                        },
+                        {
+                            "id": "p002",
+                            "name": "笔记本电脑",
+                            "price": 6999,
+                            "inventory": 50,
+                            "category": "电子产品",
+                            "specs": {
+                                "brand": "联想",
+                                "model": "ThinkPad",
+                                "color": "银色",
+                                "storage": "512GB"
+                            }
+                        }
+                    ]
+                },
+                right: {
+                    "products": [
+                        {
+                            "id": "p001",
+                            "name": "智能手机",
+                            "price": 5299,
+                            "inventory": 85,
+                            "category": "电子产品",
+                            "specs": {
+                                "brand": "小米",
+                                "model": "Mi 11 Pro",
+                                "color": "蓝色",
+                                "storage": "256GB"
+                            }
+                        },
+                        {
+                            "id": "p002",
+                            "name": "笔记本电脑",
+                            "price": 6999,
+                            "inventory": 50,
+                            "category": "电子产品",
+                            "specs": {
+                                "brand": "联想",
+                                "model": "ThinkPad",
+                                "color": "银色",
+                                "storage": "512GB"
+                            }
+                        }
+                    ]
+                }
+            },
+            configOptions: {
+                left: {
+                    "appConfig": {
+                        "theme": "light",
+                        "language": "zh-CN",
+                        "notifications": {
+                            "email": true,
+                            "push": true,
+                            "sms": false
+                        },
+                        "security": {
+                            "twoFactorAuth": true,
+                            "passwordExpiry": 90,
+                            "ipRestriction": false
+                        },
+                        "performance": {
+                            "cacheEnabled": true,
+                            "compressionLevel": "high",
+                            "preload": ["home", "dashboard"]
+                        }
+                    }
+                },
+                right: {
+                    "appConfig": {
+                        "theme": "dark",
+                        "language": "zh-CN",
+                        "notifications": {
+                            "email": true,
+                            "push": false,
+                            "sms": true
+                        },
+                        "security": {
+                            "twoFactorAuth": true,
+                            "passwordExpiry": 60,
+                            "ipRestriction": true
+                        },
+                        "performance": {
+                            "cacheEnabled": true,
+                            "compressionLevel": "medium",
+                            "preload": ["home", "profile", "dashboard"]
+                        }
+                    }
+                }
+            },
+            apiResponse: {
+                left: {
+                    "status": "success",
+                    "code": 200,
+                    "data": {
+                        "users": [
+                            {"id": 1, "name": "李明", "role": "admin"},
+                            {"id": 2, "name": "王芳", "role": "user"},
+                            {"id": 3, "name": "赵强", "role": "editor"}
+                        ],
+                        "pagination": {
+                            "total": 25,
+                            "page": 1,
+                            "limit": 10
+                        },
+                        "timestamp": 1642558132,
+                        "version": "1.0.0"
+                    }
+                },
+                right: {
+                    "status": "success",
+                    "code": 200,
+                    "data": {
+                        "users": [
+                            {"id": 1, "name": "李明", "role": "admin"},
+                            {"id": 2, "name": "王芳", "role": "user"},
+                            {"id": 3, "name": "赵强", "role": "moderator"}
+                        ],
+                        "pagination": {
+                            "total": 28,
+                            "page": 1,
+                            "limit": 10
+                        },
+                        "timestamp": 1652558132,
+                        "version": "1.2.0"
+                    }
+                }
+            }
+        }
     },
-    mounted: function () {
-        // 错误处理器
-        let errorHandler = (which, ok) => {
-            let message = '';
+    computed: {
+        // 显示的消息,计算属性替代v-html
+        displayMessage: function() {
+            return this.tipMessage + this.errorMessage;
+        }
+    },
+    methods: {
+        fillExample: function(exampleType) {
+            if (this.jsonExamples[exampleType]) {
+                const example = this.jsonExamples[exampleType];
+                jsonBox.left.setValue(JSON.stringify(example.left, null, 4));
+                jsonBox.right.setValue(JSON.stringify(example.right, null, 4));
+                
+                // 触发比对
+                setTimeout(() => {
+                    jsonBox.left.refresh();
+                    jsonBox.right.refresh();
+                    this.compareJson(); // 使用Vue实例的方法进行比对
+                }, 100);
+            }
+        },
+        // 添加比对JSON的方法
+        compareJson: function() {
+            // 使用全局变量中的实例
+            let leftText = jsonBox.left.getValue();
+            let rightText = jsonBox.right.getValue();
+            let leftJson, rightJson;
+            
+            try {
+                if (leftText) {
+                    leftJson = JSON.parse(leftText);
+                }
+                this.errorHandler('left', true);
+            } catch (e) {
+                console.log('left ==>', e);
+                this.errorHandler('left', false);
+                return;
+            }
+            
+            try {
+                if (rightText) {
+                    rightJson = JSON.parse(rightText);
+                }
+                this.errorHandler('right', true);
+            } catch (e) {
+                console.log('right ==>', e);
+                this.errorHandler('right', false);
+                return;
+            }
+            
+            if (!leftJson || !rightJson) {
+                if (!leftJson && !rightJson) {
+                    this.errorHandler('left-right', false);
+                } else if (!leftJson) {
+                    this.errorHandler('left', false);
+                } else {
+                    this.errorHandler('right', false);
+                }
+                return;
+            }
+            
+            try {
+                // 调用jsonpatch的compare方法进行比对
+                let diffs = jsonpatch.compare(leftJson, rightJson);
+                this.diffHandler(diffs);
+                
+                // 清除所有之前的标记
+                this.clearMarkers();
+                
+                // 高亮差异
+                diffs.forEach((diff) => {
+                    try {
+                        if (diff.op === 'remove') {
+                            this.highlightDiff(diff, 'remove');
+                        } else if (diff.op === 'add') {
+                            this.highlightDiff(diff, 'add');
+                        } else if (diff.op === 'replace') {
+                            this.highlightDiff(diff, 'replace');
+                        }
+                    } catch (e) {
+                        console.warn('error while trying to highlight diff', e);
+                    }
+                });
+            } catch (e) {
+                console.error('比对过程出错:', e);
+            }
+        },
+        // 清除所有标记
+        clearMarkers: function() {
+            jsonBox.left.getAllMarks().forEach(function(marker) {
+                marker.clear();
+            });
+            jsonBox.right.getAllMarks().forEach(function(marker) {
+                marker.clear();
+            });
+        },
+        // 高亮差异
+        highlightDiff: function(diff, op) {
+            if (op === 'remove') {
+                this.highlightRemoval(jsonBox.left, diff);
+            } else if (op === 'add') {
+                this.highlightAddition(jsonBox.right, diff);
+            } else if (op === 'replace') {
+                this.highlightChange(jsonBox.left, diff);
+                this.highlightChange(jsonBox.right, diff);
+            }
+        },
+        // 高亮删除
+        highlightRemoval: function(editor, diff) {
+            this._highlight(editor, diff, '#DD4444');
+        },
+        // 高亮添加
+        highlightAddition: function(editor, diff) {
+            this._highlight(editor, diff, '#4ba2ff');
+        },
+        // 高亮修改
+        highlightChange: function(editor, diff) {
+            this._highlight(editor, diff, '#E5E833');
+        },
+        // 高亮辅助方法
+        _highlight: function(editor, diff, color) {
+            try {
+                let textValue = editor.getValue();
+                // 使用全局jsonSourceMap对象
+                let result = jsonSourceMap.parse(textValue);
+                let pointers = result.pointers;
+                let path = diff.path;
+                
+                if (!pointers[path]) {
+                    console.warn('找不到路径的指针:', path);
+                    return;
+                }
+                
+                let start = {
+                    line: pointers[path].key ? pointers[path].key.line : pointers[path].value.line,
+                    ch: pointers[path].key ? pointers[path].key.column : pointers[path].value.column
+                };
+                let end = {
+                    line: pointers[path].valueEnd.line,
+                    ch: pointers[path].valueEnd.column
+                };
+                
+                editor.markText(start, end, {
+                    css: 'background-color: ' + color
+                });
+            } catch (e) {
+                console.error('高亮过程出错:', e);
+            }
+        },
+        // 错误处理
+        errorHandler: function(which, ok) {
             if (ok) {
-                message = '两侧JSON比对完成!';
+                this.errorMessage = '两侧JSON比对完成!';
                 this.errorHighlight = false;
+                this.leftSideError = false;
+                this.rightSideError = false;
             } else {
                 let side = {'left': '左', 'right': '右', 'left-right': '两'}[which];
                 if(!jsonBox.left.getValue().trim().length) {
-                    message = '请在<span class="x-hlt1">左侧</span>填入待比对的JSON内容!'
+                    this.errorMessage = '请在左侧填入待比对的JSON内容!';
+                    this.leftSideError = true;
+                    this.rightSideError = false;
                 }else if(!jsonBox.right.getValue().trim().length) {
-                    message = '请在<span class="x-hlt1">右侧</span>填入待比对的JSON内容!'
+                    this.errorMessage = '请在右侧填入待比对的JSON内容!';
+                    this.leftSideError = false;
+                    this.rightSideError = true;
                 }else{
-                    message = '<span class="x-hlt1">' + side + '侧</span>JSON不合法!';
+                    this.errorMessage = side + '侧JSON不合法!';
+                    if (which === 'left') {
+                        this.leftSideError = true;
+                        this.rightSideError = false;
+                    } else if (which === 'right') {
+                        this.leftSideError = false;
+                        this.rightSideError = true;
+                    } else {
+                        this.leftSideError = true;
+                        this.rightSideError = true;
+                    }
                 }
                 this.errorHighlight = true;
             }
-            this.errorMessage = '<span class="x-hlt">Tips:</span>' + message;
-        };
-
+        },
         // diff处理器
-        let diffHandler = (diffs) => {
+        diffHandler: function(diffs) {
             if (!this.errorHighlight) {
+                this.differenceCount = diffs.length;
+                this.isDifferent = diffs.length > 0;
                 if (diffs.length) {
-                    this.errorMessage += '共有 <span class="x-hlt">' + diffs.length + '</span> 处不一致!';
+                    this.errorMessage += '共有 ' + diffs.length + ' 处不一致!';
                 } else {
                     this.errorMessage += '且JSON内容一致!';
                 }
             }
-        };
+        },
+
+        // 打开工具市场页面
+        openOptionsPage: function(event){
+            event.preventDefault();
+            event.stopPropagation();
+            chrome.runtime.openOptionsPage();
+        },
+
+        openDonateModal: function(event){
+            event.preventDefault();
+            event.stopPropagation();
+            chrome.runtime.sendMessage({
+                type: 'fh-dynamic-any-thing',
+                thing: 'open-donate-modal',
+                params: { toolName: 'json-diff' }
+            });
+        },
+
+
+        loadPatchHotfix() {
+            // 页面加载时自动获取并注入页面的补丁
+            chrome.runtime.sendMessage({
+                type: 'fh-dynamic-any-thing',
+                thing: 'fh-get-tool-patch',
+                toolName: 'json-diff'
+            }, patch => {
+                if (patch) {
+                    if (patch.css) {
+                        const style = document.createElement('style');
+                        style.textContent = patch.css;
+                        document.head.appendChild(style);
+                    }
+                    if (patch.js) {
+                        try {
+                            if (window.evalCore && window.evalCore.getEvalInstance) {
+                                window.evalCore.getEvalInstance(window)(patch.js);
+                            }
+                        } catch (e) {
+                            console.error('json-diff补丁JS执行失败', e);
+                        }
+                    }
+                }
+            });
+        },
+    },
+    mounted: function () {
+        // 初始化JSON编辑器
+        let jsonBox = JsonDiff.init(this.$refs.srcLeft, this.$refs.srcRight, 
+            this.errorHandler.bind(this), 
+            this.diffHandler.bind(this)
+        );
+        
+        // 添加比较方法
+        jsonBox.compare = this.compareJson.bind(this);
+        
+        // 初始化文本变更监听
+        jsonBox.left.on('change', () => {
+            setTimeout(() => this.compareJson(), 300);
+        });
+        jsonBox.right.on('change', () => {
+            setTimeout(() => this.compareJson(), 300);
+        });
+        
+        // 暴露到全局,供示例数据使用
+        window.jsonBox = jsonBox;
 
-        // 代码比对
-        let jsonBox = JsonDiff.init(this.$refs.srcLeft, this.$refs.srcRight, errorHandler, diffHandler);
+        this.loadPatchHotfix();
     }
 });

+ 10 - 0
apps/json-diff/js-adapter.js

@@ -0,0 +1,10 @@
+/**
+ * JSON工具适配器
+ * 用于桥接json-source-map库和主应用
+ */
+(function() {
+    // 确保json-source-map.js中的parse函数在全局可用
+    window.jsonSourceMap = {
+        parse: parse
+    };
+})(); 

+ 297 - 39
apps/json-format/content-script.css

@@ -87,13 +87,38 @@ html.fh-jf .item {
     padding-bottom: 1px;
 }
 
+/* 行悬停效果 - 使用JavaScript控制的类 */
+html.fh-jf .item.fh-hover {
+    background-color: rgba(0, 0, 0, 0.02);
+    border-radius: 2px;
+}
+
+/* 深色主题悬停效果 */
+html.fh-jf .theme-dark .item.fh-hover {
+    background-color: rgba(255, 255, 255, 0.02);
+}
+
+/* VS Code主题悬停效果 */
+html.fh-jf .theme-vscode .item.fh-hover {
+    background-color: rgba(255, 255, 255, 0.02);
+}
+
+/* GitHub主题悬停效果 */
+html.fh-jf .theme-github .item.fh-hover {
+    background-color: rgba(0, 0, 0, 0.02);
+}
+
+/* Light主题悬停效果 */
+html.fh-jf .theme-light .item.fh-hover {
+    background-color: rgba(0, 0, 0, 0.02);
+}
+
 html.fh-jf .item .kv-list {
     display: block;
     padding-left: 24px;
     border-left: 1px dashed #bbb;
     margin-left: 2px
 }
-
 html.fh-jf .item .string {
     word-wrap: break-word;
     white-space: pre-wrap;
@@ -114,16 +139,39 @@ html.fh-jf .item .brace {
 html.fh-jf .item .expand {
     width: 20px;
     height: 18px;
-    display: block;
+    display: inline-block;
     position: absolute;
-    left: -2px;
-    top: 4px;
+    left: -20px;
+    top: 2px;
     z-index: 5;
     opacity: 0.35;
     -webkit-user-select: none;
     cursor: pointer;
 }
 
+/* 当展开按钮在key前面时 */
+html.fh-jf .item > .expand:first-child {
+    position: absolute;
+    left: 4px;
+    top: 2px;
+}
+
+/* 根级对象和数组的展开按钮 */
+html.fh-jf .item-object > .expand:first-child,
+html.fh-jf .item-array > .expand:first-child {
+    position: absolute;
+    left: 4px;
+    top: 2px;
+}
+
+/* 数组块元素的展开按钮 */
+html.fh-jf .item-block > .expand:first-child {
+    position: absolute;
+    left: 4px;
+    top: 2px;
+}
+
+
 html.fh-jf .item .expand:after {
     content: "\25bc";
 }
@@ -164,11 +212,17 @@ html.fh-jf .item.collapsed .item {
 
 html.fh-jf .item.collapsed > .expand {
     -webkit-transform: rotate(-90deg);
-    top: -1px
+    top: -4px;
+    left: -2px;
 }
 html.fh-jf .remove-quote .quote {
     display: none;
 }
+
+/* 展开按钮在key前面时不影响后续元素 */
+html.fh-jf .item > .expand:first-child + .quote {
+    margin-left: 0;
+}
 /*================json format style end===================*/
 
 
@@ -278,7 +332,7 @@ html.fh-jf  body {
     margin: 0;
 }
 
-html..fh-jf body {
+html.fh-jf body {
     padding: 0 8px;
 }
 
@@ -292,7 +346,7 @@ html.fh-jf .x-toolbar {
     border-radius: 4px;
     -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, .05);
     box-shadow: 0 1px 1px rgba(0, 0, 0, .05);
-    padding: 5px 15px;
+    padding: 10px 15px;
     position: sticky;
     top: 0;
     z-index: 10000;
@@ -300,8 +354,13 @@ html.fh-jf .x-toolbar {
     user-select: none;
 }
 
+html.fh-jf .x-toolbar .x-stitle {
+    font-weight: 700;
+}
 html.fh-jf .x-toolbar .x-sort input {
     margin-right: 10px;
+    position: relative;
+    top: 2px;
 }
 html.fh-jf .x-toolbar span {
     white-space: normal !important;
@@ -317,7 +376,7 @@ html.fh-jf  .x-toolbar .x-sort {
 }
 
 html.fh-jf  .x-toolbar .x-split {
-    margin: 0 15px;
+    margin: 0 5px;
     color: #ccc;
     display: inline;
 }
@@ -421,8 +480,49 @@ html.fh-jf .hide-status-bar #statusBar {
     height:0;
     opacity: 0;
 }
+/* JSON Path 语言选择器容器 */
+#jsonPathContainer {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    flex-wrap: wrap;
+}
+
+/* 语言选择下拉框样式 */
+#jsonPathLangSelector {
+    background: #fff;
+    border: 1px solid #ccc;
+    border-radius: 3px;
+    padding: 2px 5px;
+    font-size: 11px;
+    color: #333;
+    outline: none;
+    cursor: pointer;
+    min-width: 80px;
+}
+
+#jsonPathLangSelector:hover {
+    border-color: #999;
+}
+
+#jsonPathLangSelector:focus {
+    border-color: #0066cc;
+    box-shadow: 0 0 3px rgba(0, 102, 204, 0.3);
+}
+
+/* JSON路径显示样式优化 */
 #jsonPath {
     cursor: default;
+    font-family: 'Courier New', 'Consolas', 'Monaco', monospace;
+    background: #f8f8f8;
+    padding: 3px 8px;
+    border-radius: 3px;
+    border: 1px solid #e0e0e0;
+    font-size: 11px;
+    word-break: break-all;
+    max-width: calc(100vw - 200px);
+    overflow-x: auto;
+    white-space: nowrap;
 }
 
 html.fh-jf .boxOpt {
@@ -456,6 +556,8 @@ html.fh-jf .fe-feedback {
     float: right;
     cursor: pointer;
     font-weight: bold;
+    position: relative;
+    top: -2px;
 }
 
 .fe-feedback a {
@@ -514,7 +616,7 @@ html.fh-jf svg:not(:root) {
 /*设置面板*/
 .mod-setting-panel {
     position: absolute;
-    top: 32px;
+    top: 43px;
     right: 0;
     z-index:10000;
     width:300px;
@@ -565,6 +667,91 @@ html.fh-jf svg:not(:root) {
     right: 8px;
 }
 
+.fe-feedback>a.x-other-tools {
+    margin: 1px 0 0;
+    font-size: 12px;
+    cursor: pointer;
+    text-decoration: none;
+    -webkit-user-select: none;
+    user-select: none;
+    color: #333;
+    float: right;
+    background-color: #f5f8ff;
+    padding: 3px 10px;
+    border-radius: 15px;
+    border: 1px solid #d0d9ff;
+    transition: all 0.3s ease;
+    display: flex;
+    align-items: center;
+    position: relative;
+    top: -2px;
+}
+.fe-feedback>a.x-other-tools-us {
+    display: none;
+}
+.fe-feedback>.x-donate-link {
+    line-height: 18px;
+    color: #2563eb;
+    cursor: pointer;
+    text-decoration: none;
+    border: none;
+    white-space: nowrap;
+    margin-right: auto;
+    border-radius: 20px;
+    background-color: #eff6ff;
+    transition: all 0.2s ease;
+    display: inline-flex;
+    align-items: center;
+    box-shadow: 0 1px 2px rgba(37, 99, 235, 0.1);
+    padding: 4px 12px;
+    margin: 0 10px;
+    font-size: 12px;
+    font-weight: normal;
+}
+.fe-feedback>.x-donate-link-us {
+    display: none;
+}
+.fe-feedback>.x-donate-link a{
+    color: #333;
+    text-decoration: none;
+}
+.fe-feedback>.x-donate-link:hover {
+    background-color: #dbeafe;
+    color: #1d4ed8;
+    text-decoration: none;
+    box-shadow: 0 2px 4px rgba(37, 99, 235, 0.15);
+    transform: translateY(-1px);
+}
+
+
+.fe-feedback>a.x-other-tools .icon-plus-circle {
+    display: inline-block;
+    width: 16px;
+    height: 16px;
+    background-size: contain;
+    font-size: 16px;
+    font-style: normal;
+    line-height: 14px;
+    margin-right: 0px;
+}
+
+.fe-feedback>a.x-other-tools .tool-market-badge {
+    display: inline-block;
+    background-color: #4d89fe;
+    color: white;
+    padding: 2px 6px;
+    border-radius: 10px;
+    margin-left: 5px;
+    font-size: 12px;
+    font-weight: bold;
+}
+.fe-feedback>a.x-other-tools:hover {
+    color: #333;
+    background-color: #e6edff;
+    box-shadow: 0 2px 5px rgba(0,0,0,0.15);
+    transform: translateY(-1px);
+}
+
 /*================ 皮肤:theme-default ===================*/
 html.fh-jf body.theme-default {
     background-color: #fff;
@@ -683,107 +870,157 @@ html.fh-jf body.theme-default {
 .theme-dark {
     background:#000;
 }
-.theme-dark .x-toolbar {
+html.fh-jf .theme-dark .x-toolbar {
     background: -webkit-linear-gradient(#222,#222,#111);
     border: 1px solid #333;
     border-bottom-color: #363636;
     color:#ddd;
 }
-.theme-dark .x-toolbar .x-a-title {
+html.fh-jf .theme-dark .x-toolbar .x-a-title {
     color:#48b;
 }
-.theme-dark .xjf-btn {
+html.fh-jf .theme-dark .xjf-btn {
     background:-webkit-linear-gradient(#222,#222,#111);
     border:1px solid #444;
     color:#ddd;
 }
-.theme-dark .xjf-btn:hover {
+html.fh-jf .theme-dark .xjf-btn:hover {
     background:-webkit-linear-gradient(#333,#333,#222);
 }
-.theme-dark #statusBar {
+html.fh-jf .theme-dark #statusBar {
     background: #333;
     color: #ddd;
     border-top: 1px solid #555;
 }
-.theme-dark .boxOpt {
+
+/* 深色主题的JSON路径组件适配 */
+html.fh-jf .theme-dark #jsonPathLangSelector {
+    background: #333;
+    border-color: #555;
+    color: #ccc;
+}
+
+html.fh-jf .theme-dark #jsonPathLangSelector:hover {
+    border-color: #777;
+}
+
+html.fh-jf .theme-dark #jsonPathLangSelector:focus {
+    border-color: #0066cc;
+}
+
+html.fh-jf .theme-dark #jsonPath {
+    background: #2a2a2a;
+    border-color: #444;
+    color: #ccc;
+}
+html.fh-jf .theme-dark .boxOpt {
     background: -webkit-linear-gradient(#222,#222,#111);
     border: 1px solid #333;
 }
-.theme-dark .boxOpt a:hover {
+html.fh-jf .theme-dark .boxOpt a:hover {
     color:#eee;
 }
-.theme-dark .mod-setting-panel {
+html.fh-jf .theme-dark .mod-setting-panel {
     background: #222;
     color: #fff;
     border-color: #444;
     box-shadow: 1px 1px #111;
 }
-.theme-dark .mod-setting-panel li:hover {
+html.fh-jf .theme-dark .mod-setting-panel li:hover {
     color:#ff0;
 }
-.theme-dark .mod-setting-panel input[name="maxlength"] {
+html.fh-jf .theme-dark .mod-setting-panel input[name="maxlength"] {
     background: #000;
     border: 1px solid #555;
     color: #fff;
 }
 
-.theme-dark .item .key {
+html.fh-jf .theme-dark .item .key {
     color: #fff;
 }
 
-.theme-dark .item .kv-list {
+html.fh-jf .theme-dark .item .kv-list {
     border-left: 1px dashed #222;
 }
-.theme-dark .item .string {
+html.fh-jf .theme-dark .item .string {
     color: #0f0;
 }
 
-.theme-dark .item .string a {
+html.fh-jf .theme-dark .item .string a {
     color: #369ad6;
     text-decoration: underline;
 }
 
-.theme-dark .item .string a:hover {
+html.fh-jf .theme-dark .item .string a:hover {
     color: #b00;
 }
-.theme-dark .item .brace,
-.theme-dark .item .colon,
-.theme-dark .item .comma{
+html.fh-jf .theme-dark .item .brace,
+html.fh-jf .theme-dark .item .colon,
+html.fh-jf .theme-dark .item .comma{
     color:#eaeaea;
 }
 
-.theme-dark .item .bool,
-.theme-dark .item .null,
-.theme-dark .item .number {
+html.fh-jf .theme-dark .item .bool,
+html.fh-jf .theme-dark .item .null,
+html.fh-jf .theme-dark .item .number {
     font-weight: bold;
     color: #f00
 }
-.theme-dark .item .item-object:hover > span,
-.theme-dark .item .item-array:hover > span,
-.theme-dark .item .item-block:hover > span{
+html.fh-jf .theme-dark .item .item-object:hover > span,
+html.fh-jf .theme-dark .item .item-array:hover > span,
+html.fh-jf .theme-dark .item .item-block:hover > span{
     font-weight: bold;
     color: orange;
 }
 
-.theme-dark .item .item-line:hover,
-.theme-dark .item-line.x-selected {
+html.fh-jf .theme-dark .item .item-line:hover,
+html.fh-jf .theme-dark .item-line.x-selected {
     background: #111;
 }
 
-.theme-dark .item .item-line:hover span ,
-.theme-dark .item.x-selected>span {
+html.fh-jf .theme-dark .item .item-line:hover span ,
+html.fh-jf .theme-dark .item.x-selected>span {
     font-weight: bolder;
     background: #444;
 }
-.theme-dark .item .quote {
+html.fh-jf .theme-dark .item .quote {
     color:#eaeaea;
 }
-.theme-dark .item .expand {
+html.fh-jf .theme-dark .item .expand {
     color:#fff;
 }
+html.fh-jf .theme-dark .x-donate-link {
+    background-color: #222;
+    border: 1px solid #444;
+    color: #ddd;
+}
+html.fh-jf .theme-dark .x-donate-link:hover {
+    background-color: #333;
+}
 
+html.fh-jf .theme-dark .x-donate-link a {
+    color: #ddd;
+}
+html.fh-jf .theme-dark .x-other-tools {
+    background-color: #222;
+    border: 1px solid #444;
+    color: #ddd;
+}
 
 /*================ 皮肤:theme-vscode ===================*/
+/* VSCode主题的JSON路径组件适配 */
+.theme-vscode #jsonPathLangSelector {
+    background: #f8f8f8;
+    border-color: #ccc;
+    color: #333;
+}
+
+.theme-vscode #jsonPath {
+    background: #f8f8f8;
+    border-color: #ddd;
+    color: #333;
+}
+
 .theme-vscode .item .key {
     color: #f00;
 }
@@ -827,6 +1064,19 @@ html.fh-jf body.theme-default {
 
 
 /*================ 皮肤:theme-github ===================*/
+/* GitHub主题的JSON路径组件适配 */
+.theme-github #jsonPathLangSelector {
+    background: #fff;
+    border-color: #d1d5da;
+    color: #24292e;
+}
+
+.theme-github #jsonPath {
+    background: #f6f8fa;
+    border-color: #d1d5da;
+    color: #24292e;
+}
+
 .theme-github {
     background: #f8f8f8;
 }
@@ -876,3 +1126,11 @@ html.fh-jf body.theme-default {
 .theme-vegetarian .item-line.x-selected {
     background: #f9f9f9;
 }
+.fh-jf .json-formatter-container {display: none;}
+.t-collapse .fe-feedback {
+    height: 20px;
+}
+.t-collapse .fe-feedback #toggleBtn{
+    padding-bottom: 20px;
+}
+

+ 153 - 71
apps/json-format/content-script.js

@@ -14,16 +14,37 @@ window.JsonAutoFormat = (() => {
         if (location.protocol === 'chrome-extension:' || chrome.runtime && chrome.runtime.getURL) {
             url = chrome.runtime.getURL('json-format/' + filename);
         }
-        fetch(url).then(resp => resp.text()).then(jsText => {
-            if(window.evalCore && window.evalCore.getEvalInstance){
-                return window.evalCore.getEvalInstance(window)(jsText);
+        
+        // 使用chrome.runtime.sendMessage向background请求加载脚本
+        chrome.runtime.sendMessage({
+            type: 'fh-dynamic-any-thing',
+            thing: 'load-local-script',
+            script: url
+        }, (scriptContent) => {
+            if (!scriptContent) {
+                return;
+            }
+            
+            // 如果有evalCore则使用它
+            if (window.evalCore && window.evalCore.getEvalInstance) {
+                try {
+                    window.evalCore.getEvalInstance(window)(scriptContent);
+                } catch(e) {
+                }
+            } else {
+                // 创建一个函数来执行脚本
+                try {
+                    // 使用Function构造函数创建一个函数,并在当前窗口上下文中执行
+                    // 这比动态创建script元素更安全,因为它不涉及DOM操作
+                    const executeScript = new Function(scriptContent);
+                    executeScript.call(window);
+                } catch(e) {
+                }
             }
-            let el = document.createElement('script');
-            el.textContent = jsText;
-            document.head.appendChild(el);
         });
     };
 
+    // 加载所需脚本
     __importScript('json-bigint.js');
     __importScript('format-lib.js');
     __importScript('json-abc.js');
@@ -71,6 +92,9 @@ window.JsonAutoFormat = (() => {
     let fnTry = null;
     let fnCatch = null;
 
+    // 工具栏是否显示
+    let showToolBar = true;
+
     // 格式化的配置
     let formatOptions = {
         JSON_FORMAT_THEME: 0,
@@ -89,9 +113,24 @@ window.JsonAutoFormat = (() => {
     };
 
     let _getHtmlFragment = () => {
+
+        // 判断当前地区是否在美国
+        const isInUSA = () => {
+            // 通过时区判断是否在美国
+            const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
+            const isUSTimeZone = /^America\/(New_York|Chicago|Denver|Los_Angeles|Anchorage|Honolulu)/.test(timeZone);
+
+            // 通过语言判断
+            const language = navigator.language || navigator.userLanguage;
+            const isUSLanguage = language.toLowerCase().indexOf('en-us') > -1;
+
+            // 如果时区和语言都符合美国特征,则认为在美国
+            return (isUSTimeZone && isUSLanguage);
+        };
+
         return [
             '<div id="jfToolbar" class="x-toolbar" style="display:none">' +
-            '    <a href="https://www.baidufe.com/fehelper/index.html" target="_blank" class="x-a-title">' +
+            '    <a href="https://fehelper.com" target="_blank" class="x-a-title">' +
             '        <img src="' + chrome.runtime.getURL('static/img/fe-16.png') + '" alt="fehelper"/> FeHelper</a>' +
             '    <span class="x-b-title"></span>' +
             '    <span class="x-sort">' +
@@ -108,6 +147,8 @@ window.JsonAutoFormat = (() => {
             '           <path fill-rule="evenodd" d="M14 8.77v-1.6l-1.94-.64-.45-1.09.88-1.84-1.13-1.13-1.81.91-1.09-.45-.69-1.92h-1.6l-.63 1.94-1.11.45-1.84-.88-1.13 1.13.91 1.81-.45 1.09L0 7.23v1.59l1.94.64.45 1.09-.88 1.84 1.13 1.13 1.81-.91 1.09.45.69 1.92h1.59l.63-1.94 1.11-.45 1.84.88 1.13-1.13-.92-1.81.47-1.09L14 8.75v.02zM7 11c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3z"></path>' +
             '       </svg>高级定制</span>' +
             '       <a id="toggleBtn" title="展开或收起工具栏">隐藏&gt;&gt;</a>' +
+            '       <span class="x-donate-link' + (isInUSA() ? ' x-donate-link-us' : '') + '"><a href="#" id="donateLink"><i class="nav-icon">❤</i>&nbsp;打赏鼓励</a></span>' +
+            '       <a class="x-other-tools' + (isInUSA() ? ' x-other-tools-us' : '') + '" style="cursor:pointer"><i class="icon-plus-circle">+</i>探索 <span class="tool-market-badge">工具市场</span></a>' +
             '    </span>' +
             '</div>',
             '<div id="formattingMsg"><span class="x-loading"></span>格式化中...</div>',
@@ -172,11 +213,20 @@ window.JsonAutoFormat = (() => {
                 formData.MAX_JSON_KEYS_NUMBER = sPanel.find('input[name="maxlength"]').val();
                 formData.JSON_FORMAT_THEME = sPanel.find('input[name="skinId"]:checked').val();
 
+                // 同步更新当前页面的formatOptions对象
+                Object.keys(formData).forEach(key => {
+                    formatOptions[key] = formData[key];
+                });
+
                 chrome.runtime.sendMessage({
                     type: 'fh-dynamic-any-thing',
                     thing: 'save-jsonformat-options',
                     params: formData
-                }, result => sPanel.hide());
+                }, result => {
+                    sPanel.hide();
+                    // 重新应用格式化以展示最新设置
+                    _didFormat();
+                });
             });
 
             sPanel.find('input[name="alwaysShowToolbar"]').on('click', function (e) {
@@ -219,6 +269,7 @@ window.JsonAutoFormat = (() => {
                     elBody.addClass('remove-quote');
                 }
             });
+
         } else if (sPanel[0].offsetHeight) {
             return sPanel.hide();
         } else {
@@ -251,7 +302,7 @@ window.JsonAutoFormat = (() => {
     };
 
     let _initToolbar = () => {
-
+        showToolBar = formatOptions.JSON_TOOL_BAR_ALWAYS_SHOW;
         let cspSafe = _checkContentSecurityPolicy();
         if (cspSafe) {
             // =============================排序:获取上次记录的排序方式
@@ -297,24 +348,38 @@ window.JsonAutoFormat = (() => {
             e.preventDefault();
             e.stopPropagation();
 
+            let toolBarClassList = document.querySelector('#jfToolbar').classList;
+            showToolBar = !showToolBar;
+            if (showToolBar) {
+                toolBarClassList.remove('t-collapse');
+                tgBtn.html('隐藏&gt;&gt;');
+                formatOptions.JSON_TOOL_BAR_ALWAYS_SHOW = true;
+            } else {
+                toolBarClassList.add('t-collapse');
+                tgBtn.html('&lt;&lt;');
+                formatOptions.JSON_TOOL_BAR_ALWAYS_SHOW = false;
+            }
+            $('#jfToolbar input[name="alwaysShowToolbar"]').prop('checked', showToolBar);
+        });
+        
+        $('.fe-feedback .x-other-tools').on('click', function (e) {
             chrome.runtime.sendMessage({
                 type: 'fh-dynamic-any-thing',
-                thing: 'toggle-jsonformat-options'
-            }, show => {
-                let toolBarClassList = document.querySelector('#jfToolbar').classList;
-                if (show) {
-                    toolBarClassList.remove('t-collapse');
-                    tgBtn.html('隐藏&gt;&gt;');
-                } else {
-                    toolBarClassList.add('t-collapse');
-                    tgBtn.html('&lt;&lt;');
-                }
-                $('#jfToolbar input[name="alwaysShowToolbar"]').prop('checked', show);
+                thing: 'open-options-page'
             });
         });
 
         $('.fe-feedback .x-settings').click(e => _createSettingPanel());
         $('#jsonGetCorrectCnt').click(e => _getCorrectContent());
+
+        $('.x-toolbar .x-donate-link').on('click', function (e) {
+            chrome.runtime.sendMessage({
+                type: 'fh-dynamic-any-thing',
+                thing: 'open-donate-modal',
+                params: { toolName: 'json-format' }
+            });
+        });
+        
     };
 
     let _didFormat = function () {
@@ -352,21 +417,22 @@ window.JsonAutoFormat = (() => {
 
                 // 格式化
                 try {
-                    Formatter.format(source, theme);
+                    await Formatter.format(source, theme);
                 } catch (e) {
-                    Formatter.formatSync(source, theme)
+                    await Formatter.formatSync(source, theme)
                 }
-                $('#jfToolbar').fadeIn(500);
+                $('#jfToolbar').show();
             })();
         } else {
-            // 格式化
-            try {
-                Formatter.format(source, theme);
-            } catch (e) {
-                Formatter.formatSync(source, theme)
-            }
-
-            $('#jfToolbar').fadeIn(500);
+            (async () => {
+                // 格式化
+                try {
+                    await Formatter.format(source, theme);
+                } catch (e) {
+                    await Formatter.formatSync(source, theme)
+                }
+                $('#jfToolbar').show();
+            })();
         }
 
 
@@ -380,6 +446,17 @@ window.JsonAutoFormat = (() => {
                 $('#jfCallbackName_end').html(')');
             }
         }
+        
+        // 埋点:自动触发json-format-auto
+        chrome.runtime.sendMessage({
+            type: 'fh-dynamic-any-thing',
+            thing: 'statistics-tool-usage',
+            params: {
+                tool_name: 'json-format',
+                url: location.href
+            }
+        });
+        
     };
 
     let _getCorrectContent = function () {
@@ -395,7 +472,7 @@ window.JsonAutoFormat = (() => {
     let _getJsonContentFromDOM = function (dom) {
         let source = dom.textContent.trim();
 
-        if (!source) {
+        if (!source && document.body) {
             source = (document.body.textContent || '').trim()
         }
 
@@ -463,7 +540,7 @@ window.JsonAutoFormat = (() => {
         }
 
         // 如果是 HTML 页面,也要看一下内容是不是明显就是个JSON,如果不是,则也不进行 json 格式化
-        if (document.contentType === 'text/html') {
+        if (document.contentType === 'text/html' && document.body) {
             // 使用 DOMParser 解析 HTML
             const parser = new DOMParser();
             const doc = parser.parseFromString(document.body.outerHTML, "text/html");
@@ -522,22 +599,25 @@ window.JsonAutoFormat = (() => {
 
         // 下面校验给定字符串是否为一个合法的json
         try {
-
             // 再看看是不是jsonp的格式
-            let reg = /^([\w\.]+)\(\s*([\s\S]*)\s*\)$/gm;
-            let reTry = /^(try\s*\{\s*)?/g;
-            let reCatch = /([;\s]*\}\s*catch\s*\(\s*\S+\s*\)\s*\{([\s\S])*\})?[;\s]*$/g;
-
-            // 检测是否有try-catch包裹
-            let sourceReplaced = source.replace(reTry, function () {
-                fnTry = fnTry ? fnTry : arguments[1];
-                return '';
-            }).replace(reCatch, function () {
-                fnCatch = fnCatch ? fnCatch : arguments[1];
-                return '';
-            }).trim();
-
-            let matches = reg.exec(sourceReplaced);
+            let reg = /^([\w\.]+)\(\s*([\s\S]*)\s*\)$/m;
+            // 优化后的 try/catch 包裹处理
+            fnTry = null;
+            fnCatch = null;
+            // 处理开头
+            if (source.startsWith('try {')) {
+                fnTry = 'try {';
+                source = source.slice(5).trimStart();
+            }
+            // 处理结尾
+            let catchIdx = source.lastIndexOf('} catch');
+            if (catchIdx !== -1) {
+                // 找到最后一个 } catch,截取到末尾
+                fnCatch = source.slice(catchIdx);
+                source = source.slice(0, catchIdx).trimEnd();
+            }
+            // 只做一次正则匹配
+            let matches = reg.exec(source);
             if (matches != null && (fnTry && fnCatch || !fnTry && !fnCatch)) {
                 funcName = matches[1];
                 source = matches[2];
@@ -547,11 +627,9 @@ window.JsonAutoFormat = (() => {
                     return;
                 }
             }
-
             // 这里可能会throw exception
             jsonObj = JSON.parse(source);
         } catch (ex) {
-
             // new Function的方式,能自动给key补全双引号,但是不支持bigint,所以是下下策,放在try-catch里搞
             try {
                 jsonObj = new Function("return " + source)();
@@ -573,7 +651,6 @@ window.JsonAutoFormat = (() => {
                 }
             }
         }
-
         try {
             // 要尽量保证格式化的东西一定是一个json,所以需要把内容进行JSON.stringify处理
             source = JSON.stringify(jsonObj);
@@ -602,6 +679,7 @@ window.JsonAutoFormat = (() => {
                     thing:'inject-content-css',
                     tool: 'json-format'
                 });
+                cssInjected = true;
             }
 
             // JSON的所有key不能超过预设的值,比如 10000 个,要不然自动格式化会比较卡
@@ -624,8 +702,12 @@ window.JsonAutoFormat = (() => {
 
             formatOptions.originalSource = JSON.stringify(jsonObj);
 
-            _initToolbar();
-            _didFormat();
+            // 确保从storage加载最新设置
+            _getAllOptions(options => {
+                _extendsOptions(options);
+                _initToolbar();
+                _didFormat();
+            });
         }
     };
 
@@ -638,33 +720,30 @@ window.JsonAutoFormat = (() => {
         let source = _getJsonText();
         if (source) {
             _formatTheSource(source);
-        }else{
-            // 原计划,是兜底走fetch的方式,再尝试做一次格式化,但是这里会有很多corner Case我没法回归,所以注释掉
-            // fetch(location.href)
-            // .then(response => response.text())
-            // .then(html => {
-            //     // 使用 DOMParser 解析 HTML
-            //     const parser = new DOMParser();
-            //     const doc = parser.parseFromString(html, "text/html");
-
-            //     // 移除不需要的标签
-            //     doc.querySelectorAll('style, script').forEach(el => el.remove());
-            //     const text = _getJsonContentFromDOM(doc.body);
-            //     if(text){
-            //         _formatTheSource(text);
-            //     }
-            // })
-            // .catch();
         }
     };
 
+    // 页面加载后自动采集
+    try {
+        if (window.chrome && chrome.runtime && chrome.runtime.sendMessage && window.Awesome && window.Awesome.collectAndSendClientInfo) {
+            window.Awesome.collectAndSendClientInfo();
+        } else {
+            // fallback: 动态加载Awesome模块
+            import(chrome.runtime.getURL('background/awesome.js')).then(module => {
+                module.default.collectAndSendClientInfo();
+            }).catch(() => {});
+        }
+    } catch(e) {}
+
     return {
         format: () => _getAllOptions(options => {
             if(options.JSON_PAGE_FORMAT) {
                 let intervalId = setTimeout(() => {
                     if(typeof Formatter !== 'undefined') {
                         clearInterval(intervalId);
+                        // 加载所有保存的配置
                         _extendsOptions(options);
+                        // 应用格式化
                         _format();
                     }
                 },pleaseLetJsLoaded);
@@ -675,5 +754,8 @@ window.JsonAutoFormat = (() => {
 
 
 if(location.protocol !== 'chrome-extension:') {
-    window.JsonAutoFormat.format();
+    (async () => {
+        await window.JsonAutoFormat.format();
+    })();
 }
+

+ 496 - 408
apps/json-format/format-lib.js

@@ -94,6 +94,9 @@ window.Formatter = (function () {
 
     let lastItemIdGiven = 0;
     let cachedJsonString = '';
+    
+    // 单例Worker实例
+    let workerInstance = null;
 
     let _initElements = function () {
 
@@ -137,6 +140,7 @@ window.Formatter = (function () {
         str = str.replace(/>/g, '&gt;');
         str = str.replace(/"/g, '&quot;');
         str = str.replace(/'/g, '&#039;');
+        str = str.replace(/\\/g, '&#92;');
         return str;
     };
 
@@ -226,32 +230,280 @@ window.Formatter = (function () {
     // 添加json路径
     let _showJsonPath = function (curEl) {
         let keys = [];
-        do {
-            if (curEl.hasClass('item-block')) {
-                if (!curEl.hasClass('rootItem')) {
-                    keys.unshift('[' + curEl.prevAll('.item').length + ']');
-                } else {
-                    break;
+        let current = curEl;
+        
+        // 处理当前节点
+        if (current.hasClass('item') && !current.hasClass('rootItem')) {
+            if (current.hasClass('item-array-element')) {
+                // 这是数组元素,使用data-array-index属性
+                let index = current.attr('data-array-index');
+                if (index !== undefined) {
+                    keys.unshift('[' + index + ']');
                 }
             } else {
-                keys.unshift(curEl.find('>.key').text());
+                // 这是对象属性,获取key
+                let keyText = current.find('>.key').text();
+                if (keyText) {
+                    keys.unshift(keyText);
+                }
             }
-
-            if (curEl.parent().hasClass('rootItem') || curEl.parent().parent().hasClass('rootItem')) {
-                break;
+        }
+        
+        // 向上遍历所有祖先节点
+        current.parents('.item').each(function() {
+            let $this = $(this);
+            
+            // 跳过根节点
+            if ($this.hasClass('rootItem')) {
+                return false; // 终止遍历
+            }
+            
+            if ($this.hasClass('item-array-element')) {
+                // 这是数组元素,使用data-array-index属性
+                let index = $this.attr('data-array-index');
+                if (index !== undefined) {
+                    keys.unshift('[' + index + ']');
+                }
+            } else if ($this.hasClass('item-object') || $this.hasClass('item-array')) {
+                // 这是容器节点,寻找它的key
+                let $container = $this.parent().parent(); // 跳过 .kv-list
+                if ($container.length && !$container.hasClass('rootItem')) {
+                    if ($container.hasClass('item-array-element')) {
+                        // 容器本身是数组元素
+                        let index = $container.attr('data-array-index');
+                        if (index !== undefined) {
+                            keys.unshift('[' + index + ']');
+                        }
+                    } else {
+                        // 容器是对象属性
+                        let keyText = $container.find('>.key').text();
+                        if (keyText) {
+                            keys.unshift(keyText);
+                        }
+                    }
+                }
+            } else {
+                // 普通item节点,获取key
+                let keyText = $this.find('>.key').text();
+                if (keyText) {
+                    keys.unshift(keyText);
+                }
             }
+        });
+
+        // 过滤掉空值和无效的key
+        let validKeys = keys.filter(key => key && key.trim() !== '');
+        
+        // 创建或获取语言选择器和路径显示区域
+        let jfPathContainer = $('#jsonPathContainer');
+        if (!jfPathContainer.length) {
+            jfPathContainer = $('<div id="jsonPathContainer"/>').prependTo(jfStatusBar);
+            
+            // 创建语言选择下拉框
+            let langSelector = $('<select id="jsonPathLangSelector" title="选择编程语言格式">' +
+                '<option value="javascript">JavaScript</option>' +
+                '<option value="php">PHP</option>' +
+                '<option value="python">Python</option>' +
+                '<option value="java">Java</option>' +
+                '<option value="csharp">C#</option>' +
+                '<option value="golang">Go</option>' +
+                '<option value="ruby">Ruby</option>' +
+                '<option value="swift">Swift</option>' +
+                '</select>').appendTo(jfPathContainer);
+            
+            // 创建路径显示区域
+            let jfPath = $('<span id="jsonPath"/>').appendTo(jfPathContainer);
+            
+            // 绑定语言切换事件
+            langSelector.on('change', function() {
+                // 保存选择的语言到本地存储
+                localStorage.setItem('fehelper_json_path_lang', $(this).val());
+                // 从容器中获取当前保存的keys,而不是使用闭包中的validKeys
+                let currentKeys = jfPathContainer.data('currentKeys') || [];
+                _updateJsonPath(currentKeys, $(this).val());
+            });
+            
+            // 从本地存储恢复语言选择
+            let savedLang = localStorage.getItem('fehelper_json_path_lang') || 'javascript';
+            langSelector.val(savedLang);
+        }
+        
+        // 保存当前的keys到容器的data属性中,供语言切换时使用
+        jfPathContainer.data('currentKeys', validKeys);
+        
+        // 获取当前选择的语言
+        let selectedLang = $('#jsonPathLangSelector').val() || 'javascript';
+        _updateJsonPath(validKeys, selectedLang);
+    };
 
-            curEl = curEl.parent().parent();
+    // 根据不同编程语言格式化JSON路径
+    let _updateJsonPath = function(keys, language) {
+        let path = _formatJsonPath(keys, language);
+        $('#jsonPath').html('当前节点:' + path);
+    };
 
-        } while (curEl.length && !curEl.hasClass('rootItem'));
+    // 格式化JSON路径为不同编程语言格式
+    let _formatJsonPath = function(keys, language) {
+        if (!keys.length) {
+            return _getLanguageRoot(language);
+        }
 
-        let path = keys.join('#@#').replace(/#@#\[/g, '[').replace(/#@#/g, '.');
+        let path = '';
+        
+        switch (language) {
+            case 'javascript':
+                path = '$';
+                for (let i = 0; i < keys.length; i++) {
+                    let key = keys[i];
+                    if (key.startsWith('[') && key.endsWith(']')) {
+                        // 数组索引
+                        path += key;
+                    } else {
+                        // 对象属性
+                        if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key)) {
+                            // 有效的标识符,使用点语法
+                            path += '.' + key;
+                        } else {
+                            // 包含特殊字符,使用方括号语法
+                            path += '["' + key.replace(/"/g, '\\"') + '"]';
+                        }
+                    }
+                }
+                break;
+                
+            case 'php':
+                path = '$data';
+                for (let i = 0; i < keys.length; i++) {
+                    let key = keys[i];
+                    if (key.startsWith('[') && key.endsWith(']')) {
+                        // 数组索引
+                        path += key;
+                    } else {
+                        // 对象属性
+                        path += '["' + key.replace(/"/g, '\\"') + '"]';
+                    }
+                }
+                break;
+                
+            case 'python':
+                path = 'data';
+                for (let i = 0; i < keys.length; i++) {
+                    let key = keys[i];
+                    if (key.startsWith('[') && key.endsWith(']')) {
+                        // 数组索引
+                        path += key;
+                    } else {
+                        // 对象属性
+                        if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key) && !/^(and|as|assert|break|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|import|in|is|lambda|not|or|pass|print|raise|return|try|while|with|yield)$/.test(key)) {
+                            // 有效的标识符且不是关键字,可以使用点语法
+                            path += '.' + key;
+                        } else {
+                            // 使用方括号语法
+                            path += '["' + key.replace(/"/g, '\\"') + '"]';
+                        }
+                    }
+                }
+                break;
+                
+            case 'java':
+                path = 'jsonObject';
+                for (let i = 0; i < keys.length; i++) {
+                    let key = keys[i];
+                    if (key.startsWith('[') && key.endsWith(']')) {
+                        // 数组索引
+                        let index = key.slice(1, -1);
+                        path += '.get(' + index + ')';
+                    } else {
+                        // 对象属性
+                        path += '.get("' + key.replace(/"/g, '\\"') + '")';
+                    }
+                }
+                break;
+                
+            case 'csharp':
+                path = 'jsonObject';
+                for (let i = 0; i < keys.length; i++) {
+                    let key = keys[i];
+                    if (key.startsWith('[') && key.endsWith(']')) {
+                        // 数组索引
+                        path += key;
+                    } else {
+                        // 对象属性
+                        path += '["' + key.replace(/"/g, '\\"') + '"]';
+                    }
+                }
+                break;
+                
+            case 'golang':
+                path = 'data';
+                for (let i = 0; i < keys.length; i++) {
+                    let key = keys[i];
+                    if (key.startsWith('[') && key.endsWith(']')) {
+                        // 数组索引
+                        let index = key.slice(1, -1);
+                        path += '.(' + index + ')';
+                    } else {
+                        // 对象属性
+                        path += '["' + key.replace(/"/g, '\\"') + '"]';
+                    }
+                }
+                break;
+                
+            case 'ruby':
+                path = 'data';
+                for (let i = 0; i < keys.length; i++) {
+                    let key = keys[i];
+                    if (key.startsWith('[') && key.endsWith(']')) {
+                        // 数组索引
+                        path += key;
+                    } else {
+                        // 对象属性
+                        if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) {
+                            // 可以使用符号访问
+                            path += '[:"' + key + '"]';
+                        } else {
+                            // 字符串键
+                            path += '["' + key.replace(/"/g, '\\"') + '"]';
+                        }
+                    }
+                }
+                break;
+                
+            case 'swift':
+                path = 'jsonObject';
+                for (let i = 0; i < keys.length; i++) {
+                    let key = keys[i];
+                    if (key.startsWith('[') && key.endsWith(']')) {
+                        // 数组索引
+                        path += key;
+                    } else {
+                        // 对象属性
+                        path += '["' + key.replace(/"/g, '\\"') + '"]';
+                    }
+                }
+                break;
+                
+            default:
+                // 默认使用JavaScript格式
+                return _formatJsonPath(keys, 'javascript');
+        }
+        
+        return path;
+    };
 
-        let jfPath = $('#jsonPath');
-        if (!jfPath.length) {
-            jfPath = $('<span id="jsonPath"/>').prependTo(jfStatusBar);
+    // 获取不同语言的根对象表示
+    let _getLanguageRoot = function(language) {
+        switch (language) {
+            case 'javascript': return '$';
+            case 'php': return '$data';
+            case 'python': return 'data';
+            case 'java': return 'jsonObject';
+            case 'csharp': return 'jsonObject';
+            case 'golang': return 'data';
+            case 'ruby': return 'data';
+            case 'swift': return 'jsonObject';
+            default: return '$';
         }
-        jfPath.html('当前节点:JSON.' + path);
     };
 
     // 给某个节点增加操作项
@@ -344,30 +596,28 @@ window.Formatter = (function () {
 
 
     /**
-     * 折叠所有
+     * 递归折叠所有层级的对象和数组节点
      * @param elements
      */
     function collapse(elements) {
-        let el;
-
-        $.each(elements, function (i) {
-            el = $(this);
+        elements.each(function () {
+            var el = $(this);
             if (el.children('.kv-list').length) {
                 el.addClass('collapsed');
 
+                // 只给没有id的节点分配唯一id,并生成注释
                 if (!el.attr('id')) {
                     el.attr('id', 'item' + (++lastItemIdGiven));
-
                     let count = el.children('.kv-list').eq(0).children().length;
-                    // Generate comment text eg "4 items"
                     let comment = count + (count === 1 ? ' item' : ' items');
-                    // Add CSS that targets it
                     jfStyleEl[0].insertAdjacentHTML(
                         'beforeend',
                         '\n#item' + lastItemIdGiven + '.collapsed:after{color: #aaa; content:" // ' + comment + '"}'
                     );
                 }
 
+                // 递归对子节点继续折叠,确保所有嵌套层级都被处理
+                collapse(el.children('.kv-list').children('.item-object, .item-block'));
             }
         });
     }
@@ -414,9 +664,11 @@ window.Formatter = (function () {
 
             if (buttonCollapseAll.text() === '折叠所有') {
                 buttonCollapseAll.text('展开所有');
-                collapse($('.item-object,.item-block'));
+                // 递归折叠所有层级的对象和数组,确保所有内容都被折叠
+                collapse($('#jfContent .item-object, #jfContent .item-block'));
             } else {
                 buttonCollapseAll.text('折叠所有');
+                // 展开所有内容
                 $('.item-object,.item-block').removeClass('collapsed');
             }
             jfStatusBar && jfStatusBar.hide();
@@ -472,412 +724,248 @@ window.Formatter = (function () {
             }
         });
 
-    };
-
-    /**
-     * 执行代码格式化
-     */
-    let format = function (jsonStr, skin) {
-        cachedJsonString = JSON.stringify(JSON.parse(jsonStr), null, 4);
-
-        _initElements();
-        jfPre.html(htmlspecialchars(cachedJsonString));
-
-        // 用webwork的方式来进行格式化,效率更高
-        let worker = new Worker(URL.createObjectURL(new Blob(["(" + JsonFormatWebWorker.toString() + ")()"], {type: 'text/javascript'})));
-        worker.onmessage = function (evt) {
-            let msg = evt.data;
-            switch (msg[0]) {
-                case 'FORMATTING' :
-                    formattingMsg.show();
-                    break;
-
-                case 'FORMATTED' :
-                    formattingMsg.hide();
-                    jfContent.html(msg[1]);
-
-                    _buildOptionBar();
-                    // 事件绑定
-                    _addEvents();
-                    // 支持文件下载
-                    _downloadSupport(cachedJsonString);
-
-                    break;
+        // 行悬停效果:只高亮当前直接悬停的item,避免嵌套冒泡
+        let currentHoverElement = null;
+        
+        $('#jfContent .item').bind('mouseenter', function (e) {
+            // 只处理视觉效果,不触发任何其他逻辑
+            
+            // 清除之前的悬停样式
+            if (currentHoverElement) {
+                currentHoverElement.removeClass('fh-hover');
             }
-        };
-        worker.postMessage({
-            jsonString: jsonStr,
-            skin: skin
+            
+            // 添加当前悬停样式
+            let el = $(this);
+            el.addClass('fh-hover');
+            currentHoverElement = el;
+            
+            // 严格阻止事件冒泡和默认行为
+            e.stopPropagation();
+            e.stopImmediatePropagation();
+            e.preventDefault();
         });
-    };
-
-    // 同步的方式格式化
-    let formatSync = function (jsonStr, skin) {
-        cachedJsonString = JSON.stringify(JSON.parse(jsonStr), null, 4);
-
-        _initElements();
-        jfPre.html(htmlspecialchars(cachedJsonString));
-        let worker = new JsonFormatWebWorker(true);
-        worker.getFormattedHtml({
-            data: {
-                jsonString: jsonStr,
-                skin: skin
-            },
-            onFormatting: function (msg) {
-                formattingMsg.show();
-            },
-            onFormatted: function (msg) {
-                formattingMsg.hide();
-                jfContent.html(msg[1]);
-
-                _buildOptionBar();
-                // 事件绑定
-                _addEvents();
-                // 支持文件下载
-                _downloadSupport(cachedJsonString);
+        
+        $('#jfContent .item').bind('mouseleave', function (e) {
+            // 只处理视觉效果,不触发任何其他逻辑
+            let el = $(this);
+            el.removeClass('fh-hover');
+            
+            // 如果当前移除的元素是记录的悬停元素,清空记录
+            if (currentHoverElement && currentHoverElement[0] === el[0]) {
+                currentHoverElement = null;
+            }
+            
+            // 严格阻止事件冒泡和默认行为
+            e.stopPropagation();
+            e.stopImmediatePropagation();
+        });
+        
+        // 为整个jfContent区域添加鼠标离开事件,确保彻底清除悬停样式
+        $('#jfContent').bind('mouseleave', function (e) {
+            if (currentHoverElement) {
+                currentHoverElement.removeClass('fh-hover');
+                currentHoverElement = null;
             }
         });
-    };
-
-    return {
-        format: format,
-        formatSync: formatSync
-    }
-})();
-
 
-/*============================================== web worker =========================================================*/
+        // 图片预览功能:针对所有data-is-link=1的a标签
+        let $imgPreview = null;
+        // 加载缓存
+        function getImgCache() {
+            try {
+                return JSON.parse(sessionStorage.getItem('fehelper-img-preview-cache') || '{}');
+            } catch (e) { return {}; }
+        }
+        function setImgCache(url, isImg) {
+            let cache = getImgCache();
+            cache[url] = isImg;
+            sessionStorage.setItem('fehelper-img-preview-cache', JSON.stringify(cache));
+        }
+        $('#jfContent').on('mouseenter', 'a[data-is-link="1"]', function(e) {
+            const url = $(this).attr('data-link-url');
+            if (!url) return;
+            let cache = getImgCache();
+            if (cache.hasOwnProperty(url)) {
+                if (cache[url]) {
+                    $imgPreview = getOrCreateImgPreview();
+                    $imgPreview.find('img').attr('src', url);
+                    $imgPreview.show();
+                    $(document).on('mousemove.fhimg', function(ev) {
+                        $imgPreview.css({
+                            left: ev.pageX + 20 + 'px',
+                            top: ev.pageY + 20 + 'px'
+                        });
+                    });
+                    $imgPreview.css({
+                        left: e.pageX + 20 + 'px',
+                        top: e.pageY + 20 + 'px'
+                    });
+                }
+                return;
+            }
+            // 创建图片对象尝试加载
+            const img = new window.Image();
+            img.src = url;
+            img.onload = function() {
+                setImgCache(url, true);
+                $imgPreview = getOrCreateImgPreview();
+                $imgPreview.find('img').attr('src', url);
+                $imgPreview.show();
+                $(document).on('mousemove.fhimg', function(ev) {
+                    $imgPreview.css({
+                        left: ev.pageX + 20 + 'px',
+                        top: ev.pageY + 20 + 'px'
+                    });
+                });
+                $imgPreview.css({
+                    left: e.pageX + 20 + 'px',
+                    top: e.pageY + 20 + 'px'
+                });
+            };
+            img.onerror = function() {
+                setImgCache(url, false);
+            };
+        }).on('mouseleave', 'a[data-is-link="1"]', function(e) {
+            if ($imgPreview) $imgPreview.hide();
+            $(document).off('mousemove.fhimg');
+        });
 
-/**
- * 用webworker的形式来进行json格式化,在应对大json的时候,效果会非常明显
- * @constructor
- */
-var JsonFormatWebWorker = function (isUnSupportWorker = false) {
+        // 新增:全局监听,防止浮窗残留
+        $(document).on('mousemove.fhimgcheck', function(ev) {
+            let $target = $(ev.target).closest('a[data-is-link="1"]');
+            if ($target.length === 0) {
+                if ($imgPreview) $imgPreview.hide();
+                $(document).off('mousemove.fhimg');
+            }
+        });
 
-    // 引入big-json.js解决大数字的问题
-    let __importScript = (filename) => {
-        this.compress && fetch(filename).then(resp => resp.text()).then(jsText => eval(jsText));
     };
-    __importScript('json-bigint.js');
-
-    // Constants
-    let
-        TYPE_STRING = 1,
-        TYPE_NUMBER = 2,
-        TYPE_OBJECT = 3,
-        TYPE_ARRAY = 4,
-        TYPE_BOOL = 5,
-        TYPE_NULL = 6;
-
+    
     /**
-     * HTML特殊字符格式化
-     * @param str
-     * @returns {*}
+     * 初始化或获取Worker实例(异步,兼容Chrome/Edge/Firefox)
+     * @returns {Promise<Worker|null>}
      */
-    let htmlspecialchars = function (str) {
-        str = str.replace(/&/g, '&amp;');
-        str = str.replace(/</g, '&lt;');
-        str = str.replace(/>/g, '&gt;');
-        str = str.replace(/"/g, '&quot;');
-        str = str.replace(/'/g, '&#039;');
-        return str;
+    let _getWorkerInstance = async function() {
+        if (workerInstance) {
+            return workerInstance;
+        }
+        let workerUrl = chrome.runtime.getURL('json-format/json-worker.js');
+        // 判断是否为Firefox
+        const isFirefox = typeof InstallTrigger !== 'undefined' || navigator.userAgent.includes('Firefox');
+        try {
+            if (isFirefox) {
+                workerInstance = new Worker(workerUrl);
+                return workerInstance;
+            } else {
+                // Chrome/Edge用fetch+Blob方式
+                const resp = await fetch(workerUrl);
+                const workerScript = await resp.text();
+                const blob = new Blob([workerScript], { type: 'application/javascript' });
+                const blobUrl = URL.createObjectURL(blob);
+                workerInstance = new Worker(blobUrl);
+                return workerInstance;
+            }
+        } catch (e) {
+            console.error('创建Worker失败:', e);
+            workerInstance = null;
+            return null;
+        }
     };
 
     /**
-     * FH 虚拟DOM
-     * @constructor
+     * 执行代码格式化
+     * 支持异步worker
      */
-    let FhVDom = function () {
-
-        this._id = 'fhvd_' + (new Date * 1);
-        this.tag = '';
-        this.innerText = '';
-        this.textContent = '';
-        this.childNodes = [];
-        this.className = '';
-        this.attributes = [];
-        this.classList = [];
-        this.classList.__proto__.add = this.classList.__proto__.push;
-
-        this.createElement = tag => {
-            this.tag = tag;
-            return this;
-        };
-
-        this.setAttribute = (attr, value) => {
-            this.attributes.push([attr, value]);
-        };
-
-        this.appendChild = child => {
-            this.childNodes.push(child);
-            return this;
-        };
+    let format = async function (jsonStr, skin) {
+        cachedJsonString = JSON.stringify(JSON.parse(jsonStr), null, 4);
 
-        this.getOuterHTML = () => {
-            let outerHtml = [];
-            if (this.tag) {
-                outerHtml.push(`<${this.tag}`);
-                let clsName = (this.className || '') + ' ' + this.classList.join(' ');
-                clsName.replace(/\s/g, '').length && outerHtml.push(` class="${clsName}"`);
-                this.attributes.length && outerHtml.push(this.attributes.map(attr => ` ${attr[0]}="${attr[1]}"`).join(''));
-                outerHtml.push(`>`);
-                if (('' + this.innerText).length) {
-                    outerHtml.push(this.innerText);
-                } else if (('' + this.textContent).length) {
-                    outerHtml.push(this.textContent);
-                } else {
-                    outerHtml.push(this.childNodes.map(node => node.getOuterHTML()).join(''))
-                }
-                outerHtml.push(`</${this.tag}>`);
-            } else {
-                if (('' + this.innerText).length) {
-                    outerHtml.push(this.innerText);
-                } else if (('' + this.textContent).length) {
-                    outerHtml.push(this.textContent);
-                }
-            }
-            return outerHtml.join('');
-        };
+        _initElements();
+        jfPre.html(htmlspecialchars(cachedJsonString));
 
-        this.cloneNode = (deep) => {
-            let newDom = FhVDom.getInstance();
-            newDom.tag = this.tag;
-            if (deep || !this.tag) {
-                newDom.innerText = this.innerText;
-                newDom.textContent = this.textContent;
+        try {
+            // 获取Worker实例(异步)
+            let worker = await _getWorkerInstance();
+            if (worker) {
+                // 设置消息处理程序
+                worker.onmessage = function (evt) {
+                    let msg = evt.data;
+                    switch (msg[0]) {
+                        case 'FORMATTING':
+                            formattingMsg.show();
+                            break;
+                        case 'FORMATTED':
+                            formattingMsg.hide();
+                            jfContent.html(msg[1]);
+                            _buildOptionBar();
+                            // 事件绑定
+                            _addEvents();
+                            // 支持文件下载
+                            _downloadSupport(cachedJsonString);
+                            break;
+                    }
+                };
+                // 发送格式化请求
+                worker.postMessage({
+                    jsonString: jsonStr,
+                    skin: skin
+                });
             } else {
-                newDom.innerText = '';
-                newDom.textContent = '';
+                // Worker创建失败,回退到同步方式
+                formatSync(jsonStr, skin);
             }
-            newDom.className = this.className;
-            newDom.classList = Array.from(this.classList);
-            newDom.attributes = Array.from(this.attributes);
-            return newDom;
-        };
-    };
-
-    // 构造器
-    FhVDom.getInstance = () => new FhVDom();
-
-    function createSpanNode(innerText, className) {
-        let span = FhVDom.getInstance().createElement('span');
-        span.className = className || '';
-        span.innerText = innerText || '';
-        return span;
-    }
-
-    function createDivNode(className) {
-        let div = FhVDom.getInstance().createElement('div');
-        div.className = className || '';
-        return div;
-    }
-
-    // Create template nodes
-    let templatesObj = {
-        t_item: createDivNode('item'),
-        t_key: createSpanNode('', 'key'),
-        t_string: createSpanNode('', 'string'),
-        t_number: createSpanNode('', 'number'),
-        t_exp: createSpanNode('', 'expand'),
-
-        t_null: createSpanNode('null', 'null'),
-        t_true: createSpanNode('true', 'bool'),
-        t_false: createSpanNode('false', 'bool'),
-
-        t_oBrace: createSpanNode('{', 'brace'),
-        t_cBrace: createSpanNode('}', 'brace'),
-        t_oBracket: createSpanNode('[', 'brace'),
-        t_cBracket: createSpanNode(']', 'brace'),
-
-        t_ellipsis: createSpanNode('', 'ellipsis'),
-        t_kvList: createDivNode('kv-list'),
-
-        t_colonAndSpace: createSpanNode(':\u00A0', 'colon'),
-        t_commaText: createSpanNode(',', 'comma'),
-        t_dblqText: createSpanNode('"', 'quote')
+        } catch (e) {
+            console.error('Worker处理失败:', e);
+            // 出现任何错误,回退到同步方式
+            formatSync(jsonStr, skin);
+        }
     };
 
-    // Core recursive DOM-building function
-    function getItemDOM(value, keyName) {
-        let type,
-            item,
-            nonZeroSize,
-            templates = templatesObj,
-            objKey,
-            keySpan,
-            valueElement;
-
-        // Establish value type
-        if (typeof value === 'string')
-            type = TYPE_STRING;
-        else if (typeof value === 'number')
-            type = TYPE_NUMBER;
-        else if (value === false || value === true)
-            type = TYPE_BOOL;
-        else if (value === null)
-            type = TYPE_NULL;
-        else if (value instanceof Array)
-            type = TYPE_ARRAY;
-        else
-            type = TYPE_OBJECT;
-
-        item = templates.t_item.cloneNode(false);
-
-        // Add an 'expander' first (if this is object/array with non-zero size)
-        if (type === TYPE_OBJECT || type === TYPE_ARRAY) {
-
-            if (typeof JSON.BigNumber === 'function' && value instanceof JSON.BigNumber) {
-                value = JSON.stringify(value);
-                type = TYPE_NUMBER;
-            } else {
-                nonZeroSize = false;
-                for (objKey in value) {
-                    if (value.hasOwnProperty(objKey)) {
-                        nonZeroSize = true;
-                        break; // no need to keep counting; only need one
-                    }
-                }
-                if (nonZeroSize)
-                    item.appendChild(templates.t_exp.cloneNode(true));
-            }
-        }
+    // 同步的方式格式化
+    let formatSync = function (jsonStr, skin) {
+        cachedJsonString = JSON.stringify(JSON.parse(jsonStr), null, 4);
 
-        // If there's a key, add that before the value
-        if (keyName !== false) { // NB: "" is a legal keyname in JSON
-            item.classList.add(type === TYPE_OBJECT ? 'item-object' : type === TYPE_ARRAY ? 'item-array' : 'item-line');
-            keySpan = templates.t_key.cloneNode(false);
-            keySpan.textContent = JSON.stringify(keyName).slice(1, -1); // remove quotes
-            item.appendChild(templates.t_dblqText.cloneNode(true));
-            item.appendChild(keySpan);
-            item.appendChild(templates.t_dblqText.cloneNode(true));
-            item.appendChild(templates.t_colonAndSpace.cloneNode(true));
-        }
-        else {
-            item.classList.add('item-block');
+        _initElements();
+        jfPre.html(htmlspecialchars(cachedJsonString));
+        
+        // 显示格式化进度
+        formattingMsg.show();
+        
+        try {
+            // 回退方案:使用简单模式直接显示格式化的JSON
+            let formattedJson = JSON.stringify(JSON.parse(jsonStr), null, 4);
+            jfContent.html(`<div id="formattedJson"><pre class="rootItem">${htmlspecialchars(formattedJson)}</pre></div>`);
+            
+            // 隐藏进度提示
+            formattingMsg.hide();
+            
+            // 构建操作栏
+            _buildOptionBar();
+            // 事件绑定
+            _addEvents();
+            // 支持文件下载
+            _downloadSupport(cachedJsonString);
+            
+            return;
+        } catch (e) {
+            jfContent.html(`<div class="error">JSON格式化失败: ${e.message}</div>`);
+            
+            // 隐藏进度提示
+            formattingMsg.hide();
         }
+    };
 
-        let kvList, childItem;
-        switch (type) {
-            case TYPE_STRING:
-                let innerStringEl = FhVDom.getInstance().createElement('span'),
-                    escapedString = JSON.stringify(value);
-                escapedString = escapedString.substring(1, escapedString.length - 1); // remove quotes
-                let isLink = false;
-                if (/^[\w]+:\/\//.test(value)) {
-                    try {
-                        let url = new URL(value);
-                        let innerStringA = FhVDom.getInstance().createElement('A');
-                        innerStringA.setAttribute('href', url.href);
-                        innerStringA.setAttribute('target', '_blank');
-                        innerStringA.innerText = htmlspecialchars(escapedString);
-                        innerStringEl.appendChild(innerStringA);
-                        isLink = true;
-                    } catch (e) {
-                    }
-                }
-
-                if (!isLink) {
-                    innerStringEl.innerText = htmlspecialchars(escapedString);
-                }
-                valueElement = templates.t_string.cloneNode(false);
-                valueElement.appendChild(templates.t_dblqText.cloneNode(true));
-                valueElement.appendChild(innerStringEl);
-                valueElement.appendChild(templates.t_dblqText.cloneNode(true));
-                item.appendChild(valueElement);
-                break;
-
-            case TYPE_NUMBER:
-                valueElement = templates.t_number.cloneNode(false);
-                valueElement.innerText = value;
-                item.appendChild(valueElement);
-                break;
-
-            case TYPE_OBJECT:
-                // Add opening brace
-                item.appendChild(templates.t_oBrace.cloneNode(true));
-                if (nonZeroSize) {
-                    item.appendChild(templates.t_ellipsis.cloneNode(false));
-                    kvList = templates.t_kvList.cloneNode(false);
-                    let keys = Object.keys(value).filter(k => value.hasOwnProperty(k));
-                    keys.forEach((k, index) => {
-                        childItem = getItemDOM(value[k], k);
-                        if (index < keys.length - 1) {
-                            childItem.appendChild(templates.t_commaText.cloneNode(true));
-                        }
-                        kvList.appendChild(childItem);
-                    });
-                    item.appendChild(kvList);
-                }
-
-                // Add closing brace
-                item.appendChild(templates.t_cBrace.cloneNode(true));
-                break;
-
-            case TYPE_ARRAY:
-                item.appendChild(templates.t_oBracket.cloneNode(true));
-                if (nonZeroSize) {
-                    item.appendChild(templates.t_ellipsis.cloneNode(false));
-                    kvList = templates.t_kvList.cloneNode(false);
-                    for (let i = 0, length = value.length, lastIndex = length - 1; i < length; i++) {
-                        childItem = getItemDOM(value[i], false);
-                        if (i < lastIndex)
-                            childItem.appendChild(templates.t_commaText.cloneNode(true));
-                        kvList.appendChild(childItem);
-                    }
-                    item.appendChild(kvList);
-                }
-                // Add closing bracket
-                item.appendChild(templates.t_cBracket.cloneNode(true));
-                break;
-
-            case TYPE_BOOL:
-                if (value)
-                    item.appendChild(templates.t_true.cloneNode(true));
-                else
-                    item.appendChild(templates.t_false.cloneNode(true));
-                break;
-
-            case TYPE_NULL:
-                item.appendChild(templates.t_null.cloneNode(true));
-                break;
+    // 工具函数:获取或创建唯一图片预览浮窗节点
+    function getOrCreateImgPreview() {
+        let $img = $('#fh-img-preview');
+        if (!$img.length) {
+            $img = $('<div id="fh-img-preview" style="position:absolute;z-index:999999;border:1px solid #ccc;background:#fff;padding:4px;box-shadow:0 2px 8px #0002;pointer-events:none;"><img style="max-width:300px;max-height:200px;display:block;"></div>').appendTo('body');
         }
-
-        return item;
+        return $img;
     }
 
-    // Listen for requests from content pages wanting to set up a port
-    // isUnSupportWorker 为true时,表示不支持webworker,不需要监听消息
-    if (!isUnSupportWorker) {
-        self.onmessage = function (event) {
-            // 插件在乎的是json字符串,所以只有json字符串时才进行格式化
-            if (event.data.jsonString) {
-                self.postMessage(['FORMATTING']);
-                let rootItem;
-                if (event.data.skin && event.data.skin === 'theme-simple') {
-                    rootItem = createDivNode('rootItem');
-                    rootItem.textContent = JSON.stringify(JSON.parse(event.data.jsonString), null, 4);
-                } else {
-                    rootItem = getItemDOM(JSON.parse(event.data.jsonString), false);
-                    rootItem.classList.add('rootItem');
-                }
-                let formattedHtml = `<div id="formattedJson">${rootItem.getOuterHTML()}</div>`;
-                self.postMessage(['FORMATTED', formattedHtml]);
-            }
-        };
+    return {
+        format: format,
+        formatSync: formatSync
     }
-
-    // 针对不支持webworker的情况,允许直接调用
-    this.getFormattedHtml = function (options) {
-        options.onFormatting && options.onFormatting(['FORMATTING']);
-        let rootItem;
-        if (options.data.skin && options.data.skin === 'theme-simple') {
-            rootItem = createDivNode('rootItem');
-            rootItem.textContent = JSON.stringify(JSON.parse(options.data.jsonString), null, 4);
-        } else {
-            rootItem = getItemDOM(JSON.parse(options.data.jsonString), false);
-            rootItem.classList.add('rootItem');
-        }
-        let formattedHtml = `<div id="formattedJson">${rootItem.getOuterHTML()}</div>`;
-        options.onFormatted && options.onFormatted(['FORMATTED', formattedHtml]);
-    };
-};
+})();

+ 577 - 179
apps/json-format/index.css

@@ -1,179 +1,577 @@
-@import url("../static/vendor/codemirror/codemirror.css");
-@import url("../static/css/bootstrap.min.css");
-@import url("content-script.css");
-
-body {
-    background-color: #fff;
-}
-html,body {
-    padding: 0;
-    margin: 0;
-    height:100%;
-}
-
-.wp-json {
-    width:auto;
-}
-
-.wp-json .panel-body {
-    padding: 0;
-}
-#jfContent_pre {
-    display: none;
-    padding: 10px;
-}
-
-#errorMsg {
-    color: #f00;
-    margin-left: 10px;
-    float: right;
-}
-.x-placeholder img{
-    width: 400px;
-    opacity: 0.3;
-    margin: 5px 0 0 -8px;
-}
-.x-xdemo,a.x-xdemo {
-    margin-left: 30px;
-    font-size: 12px;
-    color: blue;
-    cursor: pointer;
-    text-decoration: underline;
-}
-.x-xdemo:hover {
-    text-decoration: underline;
-}
-
-#errorTips {
-    border-radius: 4px;
-    box-shadow: 6px 5px 7px rgba(229, 163, 163, 0.4);
-    background-color: #ffecf1;
-    border: 1px solid #dbb2b2;
-    margin-top:10px;
-}
-#errorCode .errorEm {
-    background-color:#ff921b;
-}
-#errorTarget {
-    color:#000;
-    background-color:#fff;
-}
-#errorCode {
-    padding:0 10px 10px;
-    word-break: break-all;
-}
-#errorCode ol {
-    padding:0 0 0 3em;
-}
-#errorCode .x-ph {
-    color: #f00;
-    font-weight: bold;
-}
-#tipsBox {
-    color:#b4465c;
-    padding: 6px 0 4px 2em;
-    background-color: #ffecf1;
-}
-#errorCode ol li {
-    padding:4px 7px 0;
-    list-style-type: decimal;
-    color:#b4465c;
-    background-color:#fdf7f7;
-}
-#errorCode ol li div {
-    color:#000;
-}
-
-.x-error {
-    color:red;
-}
-
-/*layout-up-down:上下布局模式*/
-.wp-json.layout-up-down .mod-json .panel-txt {
-    position: static;
-    width: 100%;
-    height: 250px;
-    margin: 0;
-}
-.wp-json.layout-up-down  .CodeMirror {
-    height: 250px;
-}
-.wp-json.layout-up-down .mod-json .rst-item {
-    margin: 0;
-}
-.wp-json.layout-up-down .mod-json .x-placeholder {
-    padding-top: 0;
-    text-align: left;
-}
-
-/* layout-left-right: 左右布局 */
-.wp-json.layout-left-right .mod-json .panel-txt {
-    width: 500px;
-    height: calc(100% - 10px);
-    float: left;
-}
-.wp-json.layout-left-right .mod-json .rst-item {
-    margin: 0 20px 0 30px;
-    padding-bottom:20px;
-    float: left;
-    width: calc(100% - 520px);
-}
-.wp-json.layout-left-right .mod-json .x-placeholder {
-    padding-top: 50px;
-    text-align: center;
-}
-.wp-json.layout-left-right .panel-body {
-    height:calc(100% - 95px);
-    padding-left: 15px;
-}
-.wp-json.layout-left-right  #jsonSource,
-.wp-json.layout-left-right  .CodeMirror {
-    height: calc(100% - 10px);
-}
-.wp-json.layout-left-right #formattedJson {
-    padding-top: 2px;
-}
-.wp-json.layout-left-right #errorTips {
-    margin-top: 0;
-}
-
-html.fh-jf .x-toolbar {
-    padding-top:5px;
-    padding-bottom: 5px;
-}
-html.fh-jf .x-toolbar.x-inpage {
-    margin:10px 20px;
-}
-.panel-title>a.x-other-tools {
-    margin:10px 0 0;
-    font-size: 12px;
-    cursor: pointer;
-    text-decoration: underline;
-    -webkit-user-select: none;
-    user-select: none;
-    color: #f00;
-    border-bottom: 1px solid #f00;
-    float: right;
-}
-.panel-title>a.x-other-tools:hover {
-    color:#00f;
-}
-.panel-heading {
-    padding:5px 15px;
-}
-
-#layoutBar {margin-left:30px}
-#btnLeftRight,#btnUpDown{
-    -webkit-border-radius:2px;-webkit-box-shadow:0px 1px 3px rgba(0,0,0,0.1);
-    -webkit-user-select:none;background:-webkit-linear-gradient(#fafafa, #f4f4f4 40%, #e5e5e5);outline: none;
-    border:1px solid #aaa;color:#444;font-size:12px;margin-bottom:0px;
-    position:relative;z-index:10;display:inline-block;
-    padding: 0 10px;
-    height: 30px;
-    text-shadow:1px 1px rgba(255,255,255,0.3)}
-#btnUpDown{margin-left:0;border-top-left-radius:0;border-bottom-left-radius:0;border-left:0;}
-#btnLeftRight{margin-right:0;border-top-right-radius:0;border-bottom-right-radius:0;border-right:none}
-#btnLeftRight:hover,#btnUpDown:hover{-webkit-box-shadow:0px 1px 3px rgba(0,0,0,0.2);
-    background:#ebebeb -webkit-linear-gradient(#fefefe, #f8f8f8 40%, #e9e9e9);border-color:#999;color:#222}
-#btnLeftRight.selected, #btnUpDown.selected{-webkit-box-shadow:inset 0px 1px 5px rgba(0,0,0,0.2);
-    background:#ebebeb -webkit-linear-gradient(#e4e4e4, #dfdfdf 40%, #dcdcdc);color:#333}
+@import url("../static/vendor/codemirror/codemirror.css");
+@import url("../static/css/bootstrap.min.css");
+@import url("content-script.css");
+
+body {
+    background-color: #fff;
+}
+html,body {
+    padding: 0;
+    margin: 0;
+    height:100%;
+}
+
+.v-cloak{
+    display: none;
+}
+
+.wp-json {
+    width:auto;
+}
+
+.wp-json .panel-body {
+    padding: 0;
+}
+#jfContent_pre {
+    display: none;
+    padding: 10px;
+}
+
+#errorMsg {
+    color: #f00;
+    margin-left: 10px;
+    float: right;
+}
+.x-placeholder img{
+    width: 400px;
+    opacity: 0.3;
+    margin: 5px 0 0 -8px;
+}
+.x-xdemo,a.x-xdemo {
+    margin-left: 30px;
+    font-size: 12px;
+    color: blue;
+    cursor: pointer;
+    text-decoration: underline;
+}
+.x-xdemo:hover {
+    text-decoration: underline;
+    color: #f00;
+}
+.x-donate-link {
+    line-height: 18px;
+}
+
+#errorTips {
+    border-radius: 4px;
+    box-shadow: 6px 5px 7px rgba(229, 163, 163, 0.4);
+    background-color: #ffecf1;
+    border: 1px solid #dbb2b2;
+    margin-top:10px;
+}
+#errorCode .errorEm {
+    background-color:#ff921b;
+}
+#errorTarget {
+    color:#000;
+    background-color:#fff;
+}
+#errorCode {
+    padding:0 10px 10px;
+    word-break: break-all;
+}
+#errorCode ol {
+    padding:0 0 0 3em;
+}
+#errorCode .x-ph {
+    color: #f00;
+    font-weight: bold;
+}
+#tipsBox {
+    color:#b4465c;
+    padding: 6px 0 4px 2em;
+    background-color: #ffecf1;
+}
+#errorCode ol li {
+    padding:4px 7px 0;
+    list-style-type: decimal;
+    color:#b4465c;
+    background-color:#fdf7f7;
+}
+#errorCode ol li div {
+    color:#000;
+}
+
+.x-error {
+    color:red;
+}
+
+/*layout-up-down:上下布局模式*/
+.wp-json.layout-up-down .mod-json .panel-txt {
+    position: static;
+    width: 100%;
+    height: 250px;
+    margin: 0;
+}
+.wp-json.layout-up-down  .CodeMirror {
+    height: 250px;
+}
+.wp-json.layout-up-down .mod-json .rst-item {
+    margin: 0;
+}
+.wp-json.layout-up-down .mod-json .x-placeholder {
+    padding-top: 0;
+    text-align: left;
+}
+
+/* layout-left-right: 左右布局 */
+.wp-json.layout-left-right .mod-json .panel-txt {
+    width: 500px;
+    height: calc(100% - 10px);
+    float: left;
+}
+.wp-json.layout-left-right .mod-json .rst-item {
+    margin: 0 20px 0 30px;
+    padding-bottom:20px;
+    float: left;
+    width: calc(100% - 520px);
+}
+.wp-json.layout-left-right .mod-json .x-placeholder {
+    padding-top: 50px;
+    text-align: center;
+}
+.wp-json.layout-left-right .panel-body {
+    height:calc(100% - 95px);
+    padding-left: 15px;
+}
+.wp-json.layout-left-right  #jsonSource,
+.wp-json.layout-left-right  .CodeMirror {
+    height: calc(100% - 10px);
+}
+.wp-json.layout-left-right #formattedJson {
+    padding-top: 2px;
+}
+.wp-json.layout-left-right #errorTips {
+    margin-top: 0;
+}
+
+html.fh-jf .x-toolbar {
+    padding-top:5px;
+    padding-bottom: 5px;
+}
+html.fh-jf .x-toolbar.x-inpage {
+    margin:10px 20px;
+}
+.panel-title>a.x-other-tools {
+    top: 0px;
+}
+
+
+.panel-heading {
+    padding:5px 15px;
+}
+
+#layoutBar {margin-left:30px}
+#btnLeftRight,#btnUpDown{
+    -webkit-border-radius:2px;-webkit-box-shadow:0px 1px 3px rgba(0,0,0,0.1);
+    -webkit-user-select:none;background:-webkit-linear-gradient(#fafafa, #f4f4f4 40%, #e5e5e5);outline: none;
+    border:1px solid #aaa;color:#444;font-size:12px;margin-bottom:0px;
+    position:relative;z-index:10;display:inline-block;
+    padding: 0 10px;
+    height: 30px;
+    text-shadow:1px 1px rgba(255,255,255,0.3)}
+#btnUpDown{margin-left:0;border-top-left-radius:0;border-bottom-left-radius:0;border-left:0;}
+#btnLeftRight{margin-right:0;border-top-right-radius:0;border-bottom-right-radius:0;border-right:none}
+#btnLeftRight:hover,#btnUpDown:hover{-webkit-box-shadow:0px 1px 3px rgba(0,0,0,0.2);
+    background:#ebebeb -webkit-linear-gradient(#fefefe, #f8f8f8 40%, #e9e9e9);border-color:#999;color:#222}
+#btnLeftRight.selected, #btnUpDown.selected{-webkit-box-shadow:inset 0px 1px 5px rgba(0,0,0,0.2);
+    background:#ebebeb -webkit-linear-gradient(#e4e4e4, #dfdfdf 40%, #dcdcdc);color:#333}
+
+#fh-img-preview {
+  display: none;
+  position: absolute !important;
+  z-index: 2147483647 !important;
+  pointer-events: none;
+  border-radius: 4px;
+  background: #fff;
+  box-shadow: 0 2px 8px #0002;
+  padding: 4px;
+  max-width: 320px;
+  max-height: 220px;
+}
+#fh-img-preview img {
+  max-width: 300px;
+  max-height: 200px;
+  display: block;
+}
+
+/* JSONPath查询样式 */
+.x-jsonpath {
+    display: inline-flex;
+    align-items: center;
+    gap: 5px;
+}
+
+/* JSONPath查询结果区域样式 */
+.jsonpath-results-section {
+    margin-top: 15px;
+}
+
+.xjf-input {
+    padding: 3px 8px;
+    border: 1px solid #ddd;
+    border-radius: 3px;
+    font-size: 12px;
+    width: 250px;
+    background: white;
+    color: #333;
+}
+
+.xjf-input:focus {
+    outline: none;
+    border-color: #007bff;
+    box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
+}
+
+.xjf-btn-examples {
+    background-color: #6c757d !important;
+    border-color: #6c757d !important;
+}
+
+.xjf-btn-examples:hover {
+    background-color: #5a6268 !important;
+    border-color: #545b62 !important;
+}
+
+/* JSONPath模态框样式 */
+.jsonpath-modal {
+    position: fixed;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    background-color: rgba(0, 0, 0, 0.5);
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    z-index: 10000;
+}
+
+.jsonpath-modal-content {
+    background: white;
+    border-radius: 8px;
+    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
+    max-width: 85%;
+    max-height: 80%;
+    min-width: 800px;
+    display: flex;
+    flex-direction: column;
+}
+
+.jsonpath-modal-header {
+    padding: 15px 20px;
+    border-bottom: 1px solid #dee2e6;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    background-color: #f8f9fa;
+    border-radius: 8px 8px 0 0;
+    gap: 15px;
+}
+
+.jsonpath-modal-header h3 {
+    margin: 0;
+    font-size: 18px;
+    color: #333;
+    flex-shrink: 0;
+}
+
+.jsonpath-header-controls {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    flex: 1;
+    max-width: 600px;
+}
+
+.jsonpath-header-input {
+    flex: 1;
+    min-width: 300px;
+    padding: 6px 10px !important;
+    font-size: 13px !important;
+    border: 1px solid #ced4da !important;
+    border-radius: 4px !important;
+    background: white !important;
+    color: #333 !important;
+}
+
+.jsonpath-header-input:focus {
+    outline: none !important;
+    border-color: #007bff !important;
+    box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25) !important;
+}
+
+.jsonpath-header-btn {
+    flex-shrink: 0;
+    padding: 6px 12px !important;
+    font-size: 13px !important;
+    white-space: nowrap;
+}
+
+.jsonpath-modal-close {
+    font-size: 24px;
+    cursor: pointer;
+    color: #6c757d;
+    padding: 0 5px;
+    user-select: none;
+}
+
+.jsonpath-modal-close:hover {
+    color: #dc3545;
+}
+
+.jsonpath-modal-body {
+    padding: 20px;
+    overflow-y: auto;
+    flex: 1;
+}
+
+.jsonpath-query-info {
+    margin-bottom: 15px;
+    padding: 10px;
+    background-color: #f8f9fa;
+    border-radius: 5px;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+}
+
+.jsonpath-query-info code {
+    background: #e9ecef;
+    padding: 2px 6px;
+    border-radius: 3px;
+    font-family: 'Courier New', monospace;
+    color: #495057;
+}
+
+.jsonpath-result-count {
+    color: #6c757d;
+    font-size: 14px;
+}
+
+.jsonpath-result-actions {
+    margin-bottom: 15px;
+    display: flex;
+    gap: 10px;
+}
+
+/* 复制按钮状态样式 */
+.xjf-btn-copying {
+    background: #ffc107 !important;
+    border-color: #ffc107 !important;
+    color: #212529 !important;
+}
+
+.xjf-btn-success {
+    background: #28a745 !important;
+    border-color: #28a745 !important;
+    color: #ffffff !important;
+}
+
+.xjf-btn-error {
+    background: #dc3545 !important;
+    border-color: #dc3545 !important;
+    color: #ffffff !important;
+}
+
+.xjf-btn:disabled {
+    opacity: 0.7;
+    cursor: not-allowed;
+}
+
+.jsonpath-results {
+    border: 1px solid #dee2e6;
+    border-radius: 5px;
+    max-height: 400px;
+    overflow-y: auto;
+}
+
+.jsonpath-result-item {
+    border-bottom: 1px solid #dee2e6;
+    padding: 10px;
+}
+
+.jsonpath-result-item:last-child {
+    border-bottom: none;
+}
+
+.jsonpath-result-path {
+    margin-bottom: 5px;
+    font-weight: bold;
+    color: #495057;
+}
+
+.jsonpath-result-path code {
+    background: #e9ecef;
+    padding: 2px 6px;
+    border-radius: 3px;
+    font-family: 'Courier New', monospace;
+    color: #6f42c1;
+}
+
+.jsonpath-result-value {
+    background: #f8f9fa;
+    border: 1px solid #e9ecef;
+    border-radius: 3px;
+    padding: 10px;
+    margin: 0;
+    font-family: 'Courier New', monospace;
+    font-size: 12px;
+    white-space: pre-wrap;
+    word-wrap: break-word;
+    max-height: 200px;
+    overflow-y: auto;
+}
+
+.jsonpath-no-results, .jsonpath-error {
+    text-align: center;
+    padding: 30px;
+    color: #6c757d;
+}
+
+.jsonpath-error .error-msg {
+    color: #dc3545;
+    font-weight: bold;
+}
+
+/* JSONPath示例样式 */
+.jsonpath-examples {
+    display: grid;
+    gap: 10px;
+}
+
+.jsonpath-example-item {
+    padding: 12px;
+    border: 1px solid #dee2e6;
+    border-radius: 5px;
+    cursor: pointer;
+    transition: all 0.2s ease;
+}
+
+.jsonpath-example-item:hover {
+    background-color: #f8f9fa;
+    border-color: #007bff;
+    transform: translateY(-1px);
+    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.jsonpath-example-path {
+    font-family: 'Courier New', monospace;
+    font-weight: bold;
+    color: #6f42c1;
+    margin-bottom: 5px;
+}
+
+.jsonpath-example-desc {
+    color: #6c757d;
+    font-size: 14px;
+}
+
+/* 深色主题适配 */
+body.theme-dark .xjf-input {
+    background: #3c3c3c;
+    border-color: #555;
+    color: #d4d4d4;
+}
+
+body.theme-dark .xjf-input:focus {
+    border-color: #007bff;
+    box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
+}
+
+body.theme-dark .jsonpath-modal-content {
+    background: #2d2d30;
+    color: #d4d4d4;
+}
+
+body.theme-dark .jsonpath-modal-header {
+    background-color: #383838;
+    border-color: #444;
+}
+
+body.theme-dark .jsonpath-modal-header h3 {
+    color: #d4d4d4;
+}
+
+body.theme-dark .jsonpath-header-input {
+    background: #3c3c3c !important;
+    border-color: #555 !important;
+    color: #d4d4d4 !important;
+}
+
+body.theme-dark .jsonpath-header-input:focus {
+    border-color: #007bff !important;
+    box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25) !important;
+}
+
+body.theme-dark .jsonpath-query-info {
+    background-color: #383838;
+    border-color: #444;
+}
+
+body.theme-dark .jsonpath-query-info code {
+    background: #444;
+    color: #d4d4d4;
+}
+
+body.theme-dark .jsonpath-results {
+    border-color: #444;
+}
+
+body.theme-dark .jsonpath-result-item {
+    border-color: #444;
+}
+
+body.theme-dark .jsonpath-result-path {
+    color: #d4d4d4;
+}
+
+body.theme-dark .jsonpath-result-path code {
+    background: #444;
+    color: #dcc5ff;
+}
+
+body.theme-dark .jsonpath-result-value {
+    background: #383838;
+    border-color: #444;
+    color: #d4d4d4;
+}
+
+body.theme-dark .jsonpath-example-item {
+    border-color: #444;
+    background: #383838;
+}
+
+body.theme-dark .jsonpath-example-item:hover {
+    background-color: #444;
+    border-color: #007bff;
+}
+
+body.theme-dark .jsonpath-example-path {
+    color: #dcc5ff;
+}
+
+body.theme-dark .jsonpath-example-desc {
+    color: #888;
+}
+
+
+
+/* 深色主题下的复制按钮状态样式 */
+body.theme-dark .xjf-btn-copying {
+    background: #ffc107 !important;
+    border-color: #ffc107 !important;
+    color: #212529 !important;
+}
+
+body.theme-dark .xjf-btn-success {
+    background: #198754 !important;
+    border-color: #198754 !important;
+    color: #ffffff !important;
+}
+
+body.theme-dark .xjf-btn-error {
+    background: #dc3545 !important;
+    border-color: #dc3545 !important;
+    color: #ffffff !important;
+}

+ 164 - 85
apps/json-format/index.html

@@ -1,85 +1,164 @@
-<!DOCTYPE HTML>
-<html lang="zh-CN" class="fh-jf">
-    <head>
-        <title>JSON格式化查看工具</title>
-        <meta charset="UTF-8">
-        <link rel="stylesheet" href="index.css" />
-		<script type="text/javascript" src="../static/vendor/evalCore.min.js"></script>
-        <script type="text/javascript" src="../static/vendor/vue/vue.js"></script>
-    </head>
-    <body class="theme-default">
-        <div class="wrapper wp-json" id="pageContainer">
-            <div class="panel panel-default" style="margin-bottom: 0px;">
-                <div class="panel-heading">
-                    <h3 class="panel-title">
-                        <a href="https://www.baidufe.com/fehelper/index/index.html" target="_blank" class="x-a-high">
-                            <img src="../static/img/fe-16.png" alt="fehelper"/> FeHelper</a>:JSON格式化
-                        <span class="x-xdemo" ref="demoLink1" @click="setDemo">示例1:JSON片段</span>
-                        <a class="x-xdemo" href="http://t.weather.sojson.com/api/weather/city/101030100" target="_blank">示例2:在线JSON</a>
-
-                        <span id="layoutBar">
-                            <button id="btnLeftRight" ref="btnLeftRight" class="selected" @click="changeLayout('left-right')">左右布局</button><button id="btnUpDown" ref="btnUpDown" @click="changeLayout('up-down')">上下布局</button>
-                        </span>
-
-                        <a class="x-other-tools" @click="openOptionsPage()">去开启FeHelper的更多功能&gt;&gt;</a>
-                    </h3>
-                </div>
-            </div>
-
-            <div class="x-toolbar x-inpage">
-
-                <button id="btnFormat" class="btn btn-primary btn-xs ui-mr-10" @click="format">格式化</button>
-                <button id="btnCompress" class="btn btn-success btn-xs" @click="compress">压缩</button>
-                <span class="x-split">|</span>
-                <input type="checkbox" v-model="jsonLintSwitch" id="jsonLint" @click="lintOn"><label for="jsonLint">JSONLint</label>
-                <span class="x-split">|</span>
-                <input type="checkbox" v-model="autoDecode" id="endecode" @click="autoDecodeFn"><label for="endecode">自动解码</label>
-                <span class="x-split">|</span>
-                <input type="checkbox" v-model="overrideJson" id="jsonOvrd" @click="setCache"><label for="jsonOvrd">节点编辑</label>
-                <span class="x-split">|</span>
-                <span class="x-sort">
-                    <span class="x-stitle">排序:</span>
-                    <label for="sort_null">默认</label>
-                    <input type="radio" name="jsonsort" id="sort_null" value="0" checked @click="format">
-                    <label for="sort_asc">升序</label>
-                    <input type="radio" name="jsonsort" id="sort_asc" value="1" @click="format">
-                    <label for="sort_desc">降序</label>
-                    <input type="radio" name="jsonsort" id="sort_desc" value="-1" @click="format">
-                </span>
-                <span class="x-split">|</span>
-                <span class="x-endecode">
-                    <button class="xjf-btn xjf-btn-left" @click="uniEncode">Uni编码</button><button class="xjf-btn xjf-btn-mid" @click="uniDecode">Uni解码</button><button class="xjf-btn xjf-btn-right" @click="urlDecode">URL解码</button>
-                </span>
-
-                <span id="optionBar"></span>
-            </div>
-
-            <div class="panel-body mod-json">
-                <div class="row panel-txt">
-                    <textarea class="form-control mod-textarea" id="jsonSource" placeholder="在这里粘贴您需要进行格式化的JSON代码" ref="jsonBox"></textarea>
-                </div>
-
-                <div class="row rst-item" id="modJsonResult">
-                    <div id="formattingMsg"><span class="x-loading"></span>格式化中...</div>
-                    <div id="jfCallbackName_start" class="callback-name" v-html="jfCallbackName_start"></div>
-                    <div id="jfContent" v-html="placeHolder"></div>
-                    <pre id="jfContent_pre"></pre>
-                    <div id="jfCallbackName_end" class="callback-name" v-html="jfCallbackName_end"></div>
-                </div>
-            </div>
-        </div>
-        <script src="../static/vendor/jquery/jquery-3.3.1.min.js"></script>
-        <script src="../static/vendor/codemirror/codemirror.js"></script>
-        <script src="../static/vendor/codemirror/javascript.js"></script>
-        <script src="../static/vendor/codemirror/active-line.js"></script>
-        <script src="../static/vendor/codemirror/matchbrackets.js"></script>
-        <script src="../static/vendor/codemirror/placeholder.js"></script>
-        <script src="json-lint.js"></script>
-        <script src="json-bigint.js"></script>
-        <script src="format-lib.js"></script>
-        <script src="json-abc.js"></script>
-        <script src="json-decode.js"></script>
-        <script src="../static/js/dark-mode.js"></script>
-        <script src="index.js" type="module"></script>
-    </body>
-</html>
+<!DOCTYPE HTML>
+<html lang="zh-CN" class="fh-jf">
+    <head>
+        <title>JSON格式化查看工具</title>
+        <meta charset="UTF-8">
+        <link rel="stylesheet" href="index.css" />
+		<script type="text/javascript" src="../static/vendor/evalCore.min.js"></script>
+        <script type="text/javascript" src="../static/vendor/vue/vue.js"></script>
+    </head>
+    <body class="theme-default">
+        <div class="wrapper wp-json" id="pageContainer">
+            <div class="panel panel-default" style="margin-bottom: 0px;">
+                <div class="panel-heading">
+                    <h3 class="panel-title">
+                        <a href="https://fehelper.com" target="_blank" class="x-a-high">
+                            <img src="../static/img/fe-16.png" alt="fehelper"/> FeHelper</a>:JSON格式化
+                        <span class="x-xdemo" ref="demoLink1" @click="setDemo">示例1:JSON片段</span>
+                        <a class="x-xdemo" href="http://t.weather.sojson.com/api/weather/city/101030100" target="_blank">示例2:在线JSON</a>
+                        <a class="x-xdemo" href="/options/index.html?query=数据Mock工具" target="_blank" class="fh-tip-link" @click="jumpToMockDataTool($event)">获取更多Mock数据</a>
+
+                        <span id="layoutBar">
+                            <button id="btnLeftRight" ref="btnLeftRight" class="selected" @click="changeLayout('left-right')">左右布局</button><button id="btnUpDown" ref="btnUpDown" @click="changeLayout('up-down')">上下布局</button>
+                        </span>
+                        <a class="x-other-tools" v-if="!isInUSAFlag" @click="openOptionsPage($event)"><i class="icon-plus-circle"></i> 探索更多实用工具 <span class="tool-market-badge">工具市场</span></a>
+                        <span class="x-donate-link" v-if="!isInUSAFlag" @click="openDonateModal($event)"><a href="#" id="donateLink"><i class="nav-icon">❤</i>&nbsp;打赏鼓励</a></span>
+                    </h3>
+                </div>
+            </div>
+
+            <div class="x-toolbar x-inpage">
+
+                <button id="btnFormat" class="btn btn-primary btn-xs ui-mr-10" @click="format">格式化</button>
+                <button id="btnCompress" class="btn btn-success btn-xs" @click="compress">压缩</button>
+                <span class="x-split">|</span>
+                <input type="checkbox" v-model="jsonLintSwitch" id="jsonLint" @click="lintOn"><label for="jsonLint">JSONLint</label>
+                <span class="x-split">|</span>
+                <input type="checkbox" v-model="autoDecode" id="endecode" @click="autoDecodeFn"><label for="endecode">自动解码</label>
+                <span class="x-split">|</span>
+                <input type="checkbox" v-model="overrideJson" id="jsonOvrd" @click="setCache"><label for="jsonOvrd">节点编辑</label>
+                <span class="x-split">|</span>
+                <input type="checkbox" v-model="autoUnpackJsonString" id="autoUnpackJsonString" @click="autoUnpackJsonStringFn"><label for="autoUnpackJsonString">支持嵌套解析</label>
+                <span class="x-split">|</span>
+                <span class="x-sort">
+                    <span class="x-stitle">排序:</span>
+                    <label for="sort_null">默认</label>
+                    <input type="radio" name="jsonsort" id="sort_null" value="0" checked @click="format">
+                    <label for="sort_asc">升序</label>
+                    <input type="radio" name="jsonsort" id="sort_asc" value="1" @click="format">
+                    <label for="sort_desc">降序</label>
+                    <input type="radio" name="jsonsort" id="sort_desc" value="-1" @click="format">
+                </span>
+                <span class="x-split">|</span>
+                <span class="x-endecode">
+                    <button class="xjf-btn xjf-btn-left" @click="uniEncode">Uni编码</button><button class="xjf-btn xjf-btn-mid" @click="uniDecode">Uni解码</button><button class="xjf-btn xjf-btn-right" @click="urlDecode">URL解码</button>
+                </span>
+                <span class="x-split">|</span>
+                <button class="xjf-btn" @click="openJsonPathModal">JSONPath查询</button>
+
+                <span id="optionBar"></span>
+            </div>
+
+            <div class="panel-body mod-json">
+                <div class="row panel-txt">
+                    <textarea class="form-control mod-textarea" id="jsonSource" placeholder="在这里粘贴您需要进行格式化的JSON代码" ref="jsonBox"></textarea>
+                </div>
+
+                <div class="row rst-item" id="modJsonResult">
+                    <div id="formattingMsg"><span class="x-loading"></span>格式化中...</div>
+                    <div id="jfCallbackName_start" class="callback-name" v-html="jfCallbackName_start"></div>
+                    <div id="jfContent" v-html="placeHolder"></div>
+                    <pre id="jfContent_pre"></pre>
+                    <div id="jfCallbackName_end" class="callback-name" v-html="jfCallbackName_end"></div>
+                </div>
+            </div>
+
+            <!-- JSONPath查询模态框 -->
+            <div id="jsonPathModal" class="jsonpath-modal" v-show="showJsonPathModal" @click="closeJsonPathModal">
+                <div class="jsonpath-modal-content" @click.stop>
+                    <div class="jsonpath-modal-header">
+                        <h3>JSONPath查询</h3>
+                        <div class="jsonpath-header-controls">
+                            <input type="text" class="xjf-input jsonpath-header-input" v-model="jsonPathQuery" 
+                                   placeholder="JSONPath表达式 (如: $.data.items[*].name)" 
+                                   @keyup.enter="executeJsonPath">
+                            <button class="xjf-btn jsonpath-header-btn" @click="executeJsonPath" :disabled="!jsonPathQuery.trim()">查询</button>
+                            <button class="xjf-btn xjf-btn-examples jsonpath-header-btn" @click="showJsonPathExamples">示例</button>
+                        </div>
+                        <span class="jsonpath-modal-close" @click="closeJsonPathModal">&times;</span>
+                    </div>
+                    <div class="jsonpath-modal-body">
+                        
+                        <!-- 查询结果区域 -->
+                        <div class="jsonpath-results-section" v-if="jsonPathQuery && (jsonPathResults.length > 0 || jsonPathError)">
+                            <div class="jsonpath-query-info">
+                                <strong>查询表达式:</strong> <code>{{jsonPathQuery}}</code>
+                                <span class="jsonpath-result-count" v-if="!jsonPathError">找到 {{jsonPathResults.length}} 个结果</span>
+                            </div>
+                                            <div class="jsonpath-result-actions" v-if="jsonPathResults.length > 0">
+                        <button class="xjf-btn" 
+                                :class="{
+                                    'xjf-btn-copying': copyButtonState === 'copying',
+                                    'xjf-btn-success': copyButtonState === 'success',
+                                    'xjf-btn-error': copyButtonState === 'error'
+                                }"
+                                @click="copyJsonPathResults"
+                                :disabled="copyButtonState === 'copying'">
+                            <span v-if="copyButtonState === 'normal'">复制结果</span>
+                            <span v-if="copyButtonState === 'copying'">复制中...</span>
+                            <span v-if="copyButtonState === 'success'">✓ 复制成功</span>
+                            <span v-if="copyButtonState === 'error'">✗ 复制失败</span>
+                        </button>
+                        <button class="xjf-btn" @click="downloadJsonPathResults">下载结果</button>
+                    </div>
+                        <div class="jsonpath-results" v-if="jsonPathResults.length > 0">
+                            <div class="jsonpath-result-item" v-for="(result, index) in jsonPathResults" :key="index">
+                                <div class="jsonpath-result-path">路径: <code>{{result.path}}</code></div>
+                                <pre class="jsonpath-result-value">{{formatJsonPathResult(result.value)}}</pre>
+                            </div>
+                        </div>
+                        <div class="jsonpath-no-results" v-if="jsonPathResults.length === 0 && !jsonPathError">
+                            <p>未找到匹配的结果</p>
+                        </div>
+                            <div class="jsonpath-error" v-if="jsonPathError">
+                                <p class="error-msg">{{jsonPathError}}</p>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+    
+            <!-- JSONPath示例模态框 -->
+            <div id="jsonPathExamplesModal" class="jsonpath-modal v-cloak" v-show="showJsonPathExamplesModal" @click="closeJsonPathExamplesModal">
+                <div class="jsonpath-modal-content" @click.stop>
+                    <div class="jsonpath-modal-header">
+                        <h3>JSONPath查询示例</h3>
+                        <span class="jsonpath-modal-close" @click="closeJsonPathExamplesModal">&times;</span>
+                    </div>
+                    <div class="jsonpath-modal-body">
+                        <div class="jsonpath-examples">
+                            <div class="jsonpath-example-item" v-for="example in jsonPathExamples" :key="example.path" @click="useJsonPathExample(example.path)">
+                                <div class="jsonpath-example-path"><code>{{example.path}}</code></div>
+                                <div class="jsonpath-example-desc">{{example.description}}</div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <script src="../static/vendor/jquery/jquery-3.3.1.min.js"></script>
+        <script src="../static/vendor/codemirror/codemirror.js"></script>
+        <script src="../static/vendor/codemirror/javascript.js"></script>
+        <script src="../static/vendor/codemirror/active-line.js"></script>
+        <script src="../static/vendor/codemirror/matchbrackets.js"></script>
+        <script src="../static/vendor/codemirror/placeholder.js"></script>
+        <script src="json-lint.js"></script>
+        <script src="json-bigint.js"></script>
+        <script src="format-lib.js"></script>
+        <script src="json-abc.js"></script>
+        <script src="json-decode.js"></script>
+        <script src="../static/js/dark-mode.js"></script>
+        <script src="index.js" type="module"></script>
+    </body>
+</html>

+ 487 - 5
apps/json-format/index.js

@@ -23,7 +23,28 @@ new Vue({
         jsonLintSwitch: true,
         autoDecode: false,
         fireChange: true,
-        overrideJson: false
+        overrideJson: false,
+        isInUSAFlag: false,
+        autoUnpackJsonString: false,
+        // JSONPath查询相关
+        jsonPathQuery: '',
+        showJsonPathModal: false,
+        showJsonPathExamplesModal: false,
+        jsonPathResults: [],
+        jsonPathError: '',
+        copyButtonState: 'normal', // normal, copying, success, error
+        jsonPathExamples: [
+            { path: '$', description: '根对象' },
+            { path: '$.data', description: '获取data属性' },
+            { path: '$.data.*', description: '获取data下的所有属性' },
+            { path: '$.data[0]', description: '获取data数组的第一个元素' },
+            { path: '$.data[*]', description: '获取data数组的所有元素' },
+            { path: '$.data[?(@.name)]', description: '获取data数组中有name属性的元素' },
+            { path: '$..name', description: '递归查找所有name属性' },
+            { path: '$.data[0:3]', description: '获取data数组的前3个元素' },
+            { path: '$.data[-1]', description: '获取data数组的最后一个元素' },
+            { path: '$.*.price', description: '获取所有子对象的price属性' }
+        ]
     },
     mounted: function () {
         // 自动开关灯控制
@@ -34,6 +55,8 @@ new Vue({
         this.autoDecode = localStorage.getItem(AUTO_DECODE);
         this.autoDecode = this.autoDecode === 'true';
 
+        this.isInUSAFlag = this.isInUSA();
+
         this.jsonLintSwitch = (localStorage.getItem(JSON_LINT) !== 'false');
         this.overrideJson = (localStorage.getItem(EDIT_ON_CLICK) === 'true');
         this.changeLayout(localStorage.getItem(LOCAL_KEY_OF_LAYOUT));
@@ -75,8 +98,51 @@ new Vue({
                 });
             });
         }
+
+        // 页面加载时自动获取并注入json-format页面的补丁
+        this.loadPatchHotfix();
     },
     methods: {
+
+        loadPatchHotfix() {
+            // 页面加载时自动获取并注入页面的补丁
+            chrome.runtime.sendMessage({
+                type: 'fh-dynamic-any-thing',
+                thing: 'fh-get-tool-patch',
+                toolName: 'json-format'
+            }, patch => {
+                if (patch) {
+                    if (patch.css) {
+                        const style = document.createElement('style');
+                        style.textContent = patch.css;
+                        document.head.appendChild(style);
+                    }
+                    if (patch.js) {
+                        try {
+                            if (window.evalCore && window.evalCore.getEvalInstance) {
+                                window.evalCore.getEvalInstance(window)(patch.js);
+                            }
+                        } catch (e) {
+                            console.error('json-format补丁JS执行失败', e);
+                        }
+                    }
+                }
+            });
+        },
+
+        isInUSA: function () {
+            // 通过时区判断是否在美国
+            const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
+            const isUSTimeZone = /^America\/(New_York|Chicago|Denver|Los_Angeles|Anchorage|Honolulu)/.test(timeZone);
+
+            // 通过语言判断
+            const language = navigator.language || navigator.userLanguage;
+            const isUSLanguage = language.toLowerCase().indexOf('en-us') > -1;
+
+            // 如果时区和语言都符合美国特征,则认为在美国
+            return (isUSTimeZone && isUSLanguage);
+        },
+
         format: function () {
             this.errorMsg = '';
             this.placeHolder = this.defaultResultTpl;
@@ -137,6 +203,12 @@ new Vue({
                 // 这里什么动作都不需要做,这种情况下转换失败的,肯定是Value被污染了,抛弃即可
             }
 
+            // 新增:自动解包嵌套JSON字符串
+            if (this.autoUnpackJsonString && jsonObj != null && typeof jsonObj === 'object') {
+                jsonObj = deepParseJSONStrings(jsonObj);
+                source = JSON.stringify(jsonObj);
+            }
+
             // 是json格式,可以进行JSON自动格式化
             if (jsonObj != null && typeof jsonObj === "object" && !this.errorMsg.length) {
                 try {
@@ -156,10 +228,12 @@ new Vue({
                         (async () => {
                             let txt = await JsonEnDecode.urlDecodeByFetch(source);
                             source = JsonEnDecode.uniDecode(txt);
-                            Formatter.format(source);
+                            await Formatter.format(source);
                         })();
                     } else {
-                        Formatter.format(source);
+                        (async () => {
+                            await Formatter.format(source);
+                        })();
                     }
 
                     this.placeHolder = '';
@@ -282,16 +356,424 @@ new Vue({
             })
         },
 
-        openOptionsPage: function(){
+        openOptionsPage: function(event){
+            event.preventDefault();
+            event.stopPropagation();
             chrome.runtime.openOptionsPage();
         },
 
+        openDonateModal: function(event){
+            event.preventDefault();
+            event.stopPropagation();
+            chrome.runtime.sendMessage({
+                type: 'fh-dynamic-any-thing',
+                thing: 'open-donate-modal',
+                params: { toolName: 'json-format' }
+            });
+        },
+
         setDemo: function () {
-            let demo = '{"BigIntSupported":995815895020119788889,"date":"20180322","message":"Success !","status":200,"city":"北京","count":632,"data":{"shidu":"34%","pm25":73,"pm10":91,"quality":"良","wendu":"5","ganmao":"极少数敏感人群应减少户外活动","yesterday":{"date":"21日星期三","sunrise":"06:19","high":"高温 11.0℃","low":"低温 1.0℃","sunset":"18:26","aqi":85,"fx":"南风","fl":"<3级","type":"多云","notice":"阴晴之间,谨防紫外线侵扰"},"forecast":[{"date":"22日星期四","sunrise":"06:17","high":"高温 17.0℃","low":"低温 1.0℃","sunset":"18:27","aqi":98,"fx":"西南风","fl":"<3级","type":"晴","notice":"愿你拥有比阳光明媚的心情"},{"date":"23日星期五","sunrise":"06:16","high":"高温 18.0℃","low":"低温 5.0℃","sunset":"18:28","aqi":118,"fx":"无持续风向","fl":"<3级","type":"多云","notice":"阴晴之间,谨防紫外线侵扰"},{"date":"24日星期六","sunrise":"06:14","high":"高温 21.0℃","low":"低温 7.0℃","sunset":"18:29","aqi":52,"fx":"西南风","fl":"<3级","type":"晴","notice":"愿你拥有比阳光明媚的心情"},{"date":"25日星期日","sunrise":"06:13","high":"高温 22.0℃","low":"低温 7.0℃","sunset":"18:30","aqi":71,"fx":"西南风","fl":"<3级","type":"晴","notice":"愿你拥有比阳光明媚的心情"},{"date":"26日星期一","sunrise":"06:11","high":"高温 21.0℃","low":"低温 8.0℃","sunset":"18:31","aqi":97,"fx":"西南风","fl":"<3级","type":"多云","notice":"阴晴之间,谨防紫外线侵扰"}]}}';
+            let demo = '{"BigIntSupported":995815895020119788889,"date":"20180322","url":"https://www.baidu.com?wd=fehelper","img":"http://gips0.baidu.com/it/u=1490237218,4115737545&fm=3028&app=3028&f=JPEG&fmt=auto?w=1280&h=720","message":"Success !","status":200,"city":"北京","count":632,"data":{"shidu":"34%","pm25":73,"pm10":91,"quality":"良","wendu":"5","ganmao":"极少数敏感人群应减少户外活动","yesterday":{"date":"21日星期三","sunrise":"06:19","high":"高温 11.0℃","low":"低温 1.0℃","sunset":"18:26","aqi":85,"fx":"南风","fl":"<3级","type":"多云","notice":"阴晴之间,谨防紫外线侵扰"},"forecast":[{"date":"22日星期四","sunrise":"06:17","high":"高温 17.0℃","low":"低温 1.0℃","sunset":"18:27","aqi":98,"fx":"西南风","fl":"<3级","type":"晴","notice":"愿你拥有比阳光明媚的心情"},{"date":"23日星期五","sunrise":"06:16","high":"高温 18.0℃","low":"低温 5.0℃","sunset":"18:28","aqi":118,"fx":"无持续风向","fl":"<3级","type":"多云","notice":"阴晴之间,谨防紫外线侵扰"},{"date":"24日星期六","sunrise":"06:14","high":"高温 21.0℃","low":"低温 7.0℃","sunset":"18:29","aqi":52,"fx":"西南风","fl":"<3级","type":"晴","notice":"愿你拥有比阳光明媚的心情"},{"date":"25日星期日","sunrise":"06:13","high":"高温 22.0℃","low":"低温 7.0℃","sunset":"18:30","aqi":71,"fx":"西南风","fl":"<3级","type":"晴","notice":"愿你拥有比阳光明媚的心情"},{"date":"26日星期一","sunrise":"06:11","high":"高温 21.0℃","low":"低温 8.0℃","sunset":"18:31","aqi":97,"fx":"西南风","fl":"<3级","type":"多云","notice":"阴晴之间,谨防紫外线侵扰"}]}}';
             editor.setValue(demo);
             this.$nextTick(() => {
                 this.format();
             })
+        },
+
+        autoUnpackJsonStringFn: function () {
+            this.$nextTick(() => {
+                localStorage.setItem('jsonformat:auto-unpack-json-string', this.autoUnpackJsonString);
+                this.format();
+            });
+        },
+
+        // JSONPath查询功能
+        executeJsonPath: function() {
+            this.jsonPathError = '';
+            this.jsonPathResults = [];
+
+            if (!this.jsonPathQuery.trim()) {
+                this.jsonPathError = '请输入JSONPath查询表达式';
+                return;
+            }
+
+            let source = this.jsonFormattedSource || editor.getValue();
+            if (!source.trim()) {
+                this.jsonPathError = '请先输入JSON数据';
+                return;
+            }
+
+            try {
+                let jsonObj = JSON.parse(source);
+                this.jsonPathResults = this.queryJsonPath(jsonObj, this.jsonPathQuery.trim());
+                this.showJsonPathModal = true;
+            } catch (error) {
+                this.jsonPathError = 'JSON格式错误:' + error.message;
+                this.showJsonPathModal = true;
+            }
+        },
+
+        // JSONPath查询引擎
+        queryJsonPath: function(obj, path) {
+            let results = [];
+            
+            try {
+                // 简化的JSONPath解析器
+                if (path === '$') {
+                    results.push({ path: '$', value: obj });
+                    return results;
+                }
+
+                // 移除开头的$
+                if (path.startsWith('$.')) {
+                    path = path.substring(2);
+                } else if (path.startsWith('$')) {
+                    path = path.substring(1);
+                }
+
+                // 执行查询
+                this.evaluateJsonPath(obj, path, '$', results);
+                
+            } catch (error) {
+                throw new Error('JSONPath表达式错误:' + error.message);
+            }
+
+            return results;
+        },
+
+        // 递归评估JSONPath
+        evaluateJsonPath: function(current, path, currentPath, results) {
+            if (!path) {
+                results.push({ path: currentPath, value: current });
+                return;
+            }
+
+            // 处理递归搜索 ..
+            if (path.startsWith('..')) {
+                let remainPath = path.substring(2);
+                this.recursiveSearch(current, remainPath, currentPath, results);
+                return;
+            }
+
+            // 解析下一个路径片段
+            let match;
+            
+            // 处理数组索引 [index] 或 [*] 或 [start:end]
+            if ((match = path.match(/^\[([^\]]+)\](.*)$/))) {
+                let indexExpr = match[1];
+                let remainPath = match[2];
+                
+                if (!Array.isArray(current)) {
+                    return;
+                }
+
+                if (indexExpr === '*') {
+                    // 通配符:所有元素
+                    current.forEach((item, index) => {
+                        this.evaluateJsonPath(item, remainPath, currentPath + '[' + index + ']', results);
+                    });
+                } else if (indexExpr.includes(':')) {
+                    // 数组切片 [start:end]
+                    let [start, end] = indexExpr.split(':').map(s => s.trim() === '' ? undefined : parseInt(s));
+                    let sliced = current.slice(start, end);
+                    sliced.forEach((item, index) => {
+                        let actualIndex = (start || 0) + index;
+                        this.evaluateJsonPath(item, remainPath, currentPath + '[' + actualIndex + ']', results);
+                    });
+                } else if (indexExpr.startsWith('?(')) {
+                    // 过滤表达式 [?(@.prop)]
+                    current.forEach((item, index) => {
+                        if (this.evaluateFilter(item, indexExpr)) {
+                            this.evaluateJsonPath(item, remainPath, currentPath + '[' + index + ']', results);
+                        }
+                    });
+                } else {
+                    // 具体索引
+                    let index = parseInt(indexExpr);
+                    if (index < 0) {
+                        index = current.length + index; // 负索引
+                    }
+                    if (index >= 0 && index < current.length) {
+                        this.evaluateJsonPath(current[index], remainPath, currentPath + '[' + index + ']', results);
+                    }
+                }
+                return;
+            }
+
+            // 处理属性访问 .property 或直接属性名
+            if ((match = path.match(/^\.?([^.\[]+)(.*)$/))) {
+                let prop = match[1];
+                let remainPath = match[2];
+                
+                if (prop === '*') {
+                    // 通配符:所有属性
+                    if (typeof current === 'object' && current !== null) {
+                        Object.keys(current).forEach(key => {
+                            this.evaluateJsonPath(current[key], remainPath, currentPath + '.' + key, results);
+                        });
+                    }
+                } else {
+                    // 具体属性
+                    if (typeof current === 'object' && current !== null && current.hasOwnProperty(prop)) {
+                        this.evaluateJsonPath(current[prop], remainPath, currentPath + '.' + prop, results);
+                    }
+                }
+                return;
+            }
+
+            // 处理方括号属性访问 ['property']
+            if ((match = path.match(/^\['([^']+)'\](.*)$/))) {
+                let prop = match[1];
+                let remainPath = match[2];
+                
+                if (typeof current === 'object' && current !== null && current.hasOwnProperty(prop)) {
+                    this.evaluateJsonPath(current[prop], remainPath, currentPath + "['" + prop + "']", results);
+                }
+                return;
+            }
+
+            // 如果没有特殊符号,当作属性名处理
+            if (typeof current === 'object' && current !== null && current.hasOwnProperty(path)) {
+                results.push({ path: currentPath + '.' + path, value: current[path] });
+            }
+        },
+
+        // 递归搜索
+        recursiveSearch: function(current, targetProp, currentPath, results) {
+            if (typeof current === 'object' && current !== null) {
+                // 检查当前对象的属性
+                if (current.hasOwnProperty(targetProp)) {
+                    results.push({ path: currentPath + '..' + targetProp, value: current[targetProp] });
+                }
+                
+                // 递归搜索子对象
+                Object.keys(current).forEach(key => {
+                    if (Array.isArray(current[key])) {
+                        current[key].forEach((item, index) => {
+                            this.recursiveSearch(item, targetProp, currentPath + '.' + key + '[' + index + ']', results);
+                        });
+                    } else if (typeof current[key] === 'object' && current[key] !== null) {
+                        this.recursiveSearch(current[key], targetProp, currentPath + '.' + key, results);
+                    }
+                });
+            }
+        },
+
+        // 简单的过滤器评估
+        evaluateFilter: function(item, filterExpr) {
+            // 简化的过滤器实现,只支持基本的属性存在性检查
+            // 如 ?(@.name) 检查是否有name属性
+            let match = filterExpr.match(/^\?\(@\.(\w+)\)$/);
+            if (match) {
+                let prop = match[1];
+                return typeof item === 'object' && item !== null && item.hasOwnProperty(prop);
+            }
+            
+            // 支持简单的比较 ?(@.age > 18)
+            match = filterExpr.match(/^\?\(@\.(\w+)\s*([><=!]+)\s*(.+)\)$/);
+            if (match) {
+                let prop = match[1];
+                let operator = match[2];
+                let value = match[3];
+                
+                if (typeof item === 'object' && item !== null && item.hasOwnProperty(prop)) {
+                    let itemValue = item[prop];
+                    let compareValue = isNaN(value) ? value.replace(/['"]/g, '') : parseFloat(value);
+                    
+                    switch (operator) {
+                        case '>': return itemValue > compareValue;
+                        case '<': return itemValue < compareValue;
+                        case '>=': return itemValue >= compareValue;
+                        case '<=': return itemValue <= compareValue;
+                        case '==': return itemValue == compareValue;
+                        case '!=': return itemValue != compareValue;
+                    }
+                }
+            }
+            
+            return false;
+        },
+
+        // 显示JSONPath示例
+        showJsonPathExamples: function() {
+            this.showJsonPathExamplesModal = true;
+        },
+
+        // 使用JSONPath示例
+        useJsonPathExample: function(path) {
+            this.jsonPathQuery = path;
+            this.closeJsonPathExamplesModal();
+        },
+
+        // 打开JSONPath查询模态框
+        openJsonPathModal: function() {
+            this.showJsonPathModal = true;
+            // 清空之前的查询结果
+            this.jsonPathResults = [];
+            this.jsonPathError = '';
+            this.copyButtonState = 'normal';
+        },
+
+        // 关闭JSONPath结果模态框
+        closeJsonPathModal: function() {
+            this.showJsonPathModal = false;
+            this.copyButtonState = 'normal'; // 重置复制按钮状态
+        },
+
+        // 关闭JSONPath示例模态框
+        closeJsonPathExamplesModal: function() {
+            this.showJsonPathExamplesModal = false;
+        },
+
+        // 格式化JSONPath查询结果
+        formatJsonPathResult: function(value) {
+            if (typeof value === 'object') {
+                return JSON.stringify(value, null, 2);
+            }
+            return String(value);
+        },
+
+        // 复制JSONPath查询结果
+        copyJsonPathResults: function() {
+            let resultText = this.jsonPathResults.map(result => {
+                return `路径: ${result.path}\n值: ${this.formatJsonPathResult(result.value)}`;
+            }).join('\n\n');
+            
+            // 设置复制状态
+            this.copyButtonState = 'copying';
+            
+            navigator.clipboard.writeText(resultText).then(() => {
+                this.copyButtonState = 'success';
+                setTimeout(() => {
+                    this.copyButtonState = 'normal';
+                }, 2000);
+            }).catch(() => {
+                // 兼容旧浏览器
+                try {
+                    let textArea = document.createElement('textarea');
+                    textArea.value = resultText;
+                    document.body.appendChild(textArea);
+                    textArea.select();
+                    document.execCommand('copy');
+                    document.body.removeChild(textArea);
+                    this.copyButtonState = 'success';
+                    setTimeout(() => {
+                        this.copyButtonState = 'normal';
+                    }, 2000);
+                } catch (error) {
+                    this.copyButtonState = 'error';
+                    setTimeout(() => {
+                        this.copyButtonState = 'normal';
+                    }, 2000);
+                }
+            });
+        },
+
+        // 下载JSONPath查询结果
+        downloadJsonPathResults: function() {
+            let resultText = this.jsonPathResults.map(result => {
+                return `路径: ${result.path}\n值: ${this.formatJsonPathResult(result.value)}`;
+            }).join('\n\n');
+            
+            // 基于JSONPath生成文件名
+            let filename = this.generateFilenameFromPath(this.jsonPathQuery);
+            
+            let blob = new Blob([resultText], { type: 'text/plain;charset=utf-8' });
+            let url = window.URL.createObjectURL(blob);
+            let a = document.createElement('a');
+            a.href = url;
+            a.download = filename + '.txt';
+            document.body.appendChild(a);
+            a.click();
+            document.body.removeChild(a);
+            window.URL.revokeObjectURL(url);
+        },
+
+        // 根据JSONPath生成文件名
+        generateFilenameFromPath: function(path) {
+            if (!path || path === '$') {
+                return 'jsonpath_root';
+            }
+            
+            // 移除开头的$和.
+            let cleanPath = path.replace(/^\$\.?/, '');
+            
+            // 替换特殊字符为下划线,保留数字、字母、点号、中划线
+            let filename = cleanPath
+                .replace(/[\[\]]/g, '_')  // 方括号替换为下划线
+                .replace(/[^\w\u4e00-\u9fa5.-]/g, '_')  // 特殊字符替换为下划线,保留中文
+                .replace(/_{2,}/g, '_')   // 多个连续下划线合并为一个
+                .replace(/^_|_$/g, '');   // 移除开头和结尾的下划线
+            
+            // 如果处理后为空,使用默认名称
+            if (!filename) {
+                return 'jsonpath_query';
+            }
+            
+            // 限制文件名长度
+            if (filename.length > 50) {
+                filename = filename.substring(0, 50) + '_truncated';
+            }
+            
+            return 'jsonpath_' + filename;
+        },
+
+        jumpToMockDataTool: function(event) {
+            event.preventDefault();
+            // 1. 先判断mock-data工具是否已安装
+            // 方案:直接读取chrome.storage.local,判断DYNAMIC_TOOL:mock-data是否存在
+            if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.local) {
+                chrome.storage.local.get('DYNAMIC_TOOL:mock-data', result => {
+                    if (result && result['DYNAMIC_TOOL:mock-data']) {
+                        // 已安装,直接打开mock-data工具
+                        window.open('/mock-data/index.html', '_blank');
+                    } else {
+                        // 未安装,跳转到原href
+                        window.open('/options/index.html?query=数据Mock工具', '_blank');
+                    }
+                });
+            } else {
+                // 兜底:如果无法访问chrome.storage,直接跳原href
+                window.open('/options/index.html?query=数据Mock工具', '_blank');
+            }
         }
     }
 });
+
+// 新增:递归解包嵌套JSON字符串的函数
+function deepParseJSONStrings(obj) {
+    if (Array.isArray(obj)) {
+        return obj.map(deepParseJSONStrings);
+    } else if (typeof obj === 'object' && obj !== null) {
+        const newObj = {};
+        for (const key in obj) {
+            if (!obj.hasOwnProperty(key)) continue;
+            const val = obj[key];
+            if (typeof val === 'string') {
+                try {
+                    const parsed = JSON.parse(val);
+                    // 只递归对象或数组,且排除BigInt结构(如{s,e,c})和纯数字
+                    if (
+                        typeof parsed === 'object' &&
+                        parsed !== null &&
+                        (Array.isArray(parsed) || Object.prototype.toString.call(parsed) === '[object Object]') &&
+                        !(
+                            parsed &&
+                            typeof parsed.s === 'number' &&
+                            typeof parsed.e === 'number' &&
+                            Array.isArray(parsed.c) &&
+                            Object.keys(parsed).length === 3
+                        )
+                    ) {
+                        newObj[key] = deepParseJSONStrings(parsed);
+                        continue;
+                    }
+                } catch (e) {}
+            }
+            newObj[key] = deepParseJSONStrings(val);
+        }
+        return newObj;
+    }
+    return obj;
+}
+

+ 456 - 0
apps/json-format/json-worker.js

@@ -0,0 +1,456 @@
+// 创建一个处理BigInt的JSON解析器
+const JSONBigInt = {
+    // 自定义的parse方法,处理大数字
+    parse: function(text) {
+        // 先尝试预处理字符串,将可能的大整数标记出来
+        // 以更精确的方式匹配JSON中的大整数
+        const preparedText = this._markBigInts(text);
+        
+        try {
+            // 使用标准JSON解析,同时使用reviver函数还原BigInt
+            return JSON.parse(preparedText, this._reviver);
+        } catch (e) {
+            // 如果处理失败,尝试原始解析方式
+            console.error('BigInt处理失败,回退到标准解析', e);
+            return JSON.parse(text);
+        }
+    },
+    
+    // 将JSON字符串中的大整数标记为特殊格式
+    _markBigInts: function(text) {
+        // 这个正则匹配JSON中的数字,但需要避免匹配到引号内的字符串
+        // 匹配模式: 找到数字前面是冒号或左方括号的情况(表示这是个值而不是键名)
+        return text.replace(
+            /([:,\[]\s*)(-?\d{16,})([,\]\}])/g, 
+            function(match, prefix, number, suffix) {
+                // 将大数字转换为特殊格式的字符串
+                return prefix + '"__BigInt__' + number + '"' + suffix;
+            }
+        );
+    },
+    
+    // 恢复函数,将标记的BigInt字符串转回BigInt类型
+    _reviver: function(key, value) {
+        // 检查是否是我们标记的BigInt字符串
+        if (typeof value === 'string' && value.startsWith('__BigInt__')) {
+            // 提取数字部分
+            const numStr = value.substring(10);
+            try {
+                // 尝试转换为BigInt
+                return BigInt(numStr);
+            } catch (e) {
+                // 如果转换失败,保留原始字符串
+                console.warn('无法转换为BigInt:', numStr);
+                return numStr;
+            }
+        }
+        return value;
+    }
+};
+
+// 处理主线程消息
+self.onmessage = function(event) {
+    
+    // 格式化JSON
+    if (event.data.jsonString) {
+        // 发送格式化中的消息
+        self.postMessage(['FORMATTING']);
+        
+        try {
+            // 先预处理JSON字符串,防止大整数丢失精度
+            let jsonObj;
+            
+            try {
+                // 尝试使用自定义的BigInt解析器
+                jsonObj = JSONBigInt.parse(event.data.jsonString);
+            } catch (e) {
+                // 如果解析失败,回退到标准解析
+                console.error('BigInt解析失败,回退到标准解析', e);
+                jsonObj = JSON.parse(event.data.jsonString);
+            }
+            
+            // 如果是简单主题,直接返回格式化的JSON
+            if (event.data.skin && event.data.skin === 'theme-simple') {
+                // 处理BigInt特殊情况
+                let formatted = JSON.stringify(jsonObj, function(key, value) {
+                    if (typeof value === 'bigint') {
+                        // 移除n后缀,只显示数字本身
+                        return value.toString();
+                    }
+                    // 处理普通数字,避免科学计数法
+                    if (typeof value === 'number' && value.toString().includes('e')) {
+                        // 大数字转为字符串以避免科学计数法
+                        return value.toLocaleString('fullwide', {useGrouping: false});
+                    }
+                    return value;
+                }, 4);
+                
+                let html = '<div id="formattedJson"><pre class="rootItem">' + 
+                    formatted.replace(/&/g, '&amp;')
+                        .replace(/</g, '&lt;')
+                        .replace(/>/g, '&gt;')
+                        .replace(/"/g, '&quot;')
+                        .replace(/'/g, '&#039;') + 
+                    '</pre></div>';
+                
+                self.postMessage(['FORMATTED', html]);
+                return;
+            }
+            
+            // 默认主题 - 创建更丰富的HTML结构
+            let html = '<div id="formattedJson">' +
+                formatJsonToHtml(jsonObj) +
+                '</div>';
+            
+            self.postMessage(['FORMATTED', html]);
+        } catch (e) {
+            // 处理错误情况
+            self.postMessage(['FORMATTED', '<div id="formattedJson"><div class="error">格式化失败: ' + e.message + '</div></div>']);
+        }
+    }
+};
+
+// HTML特殊字符格式化
+function htmlspecialchars(str) {
+    str = str.replace(/&/g, '&amp;');
+    str = str.replace(/</g, '&lt;');
+    str = str.replace(/>/g, '&gt;');
+    str = str.replace(/"/g, '&quot;');
+    str = str.replace(/'/g, '&#039;');
+    return str;
+}
+
+// 格式化字符串值,如果是URL则转换为链接
+function formatStringValue(str) {
+    // URL正则表达式,匹配 http/https/ftp 协议的URL
+    const urlRegex = /^(https?:\/\/|ftp:\/\/)[^\s<>"'\\]+$/i;
+    
+    if (urlRegex.test(str)) {
+        // 如果是URL,转换为链接
+        const escapedUrl = htmlspecialchars(str);
+        return '<a href="' + escapedUrl + '" target="_blank" rel="noopener noreferrer" data-is-link="1" data-link-url="' + escapedUrl + '">' + htmlspecialchars(str) + '</a>';
+    } else {
+        // 直接显示解析后的字符串内容,不需要重新转义
+        // 这样可以保持用户原始输入的意图
+        return htmlspecialchars(str);
+    }
+}
+
+// 格式化JSON为HTML
+function formatJsonToHtml(json) {
+    return createNode(json).getHTML();
+}
+
+// 创建节点
+function createNode(value) {
+    let node = {
+        type: getType(value),
+        value: value,
+        children: [],
+        
+        getHTML: function() {
+            switch(this.type) {
+                case 'string':
+                    // 判断原始字符串是否为URL
+                    if (isUrl(this.value)) {
+                        // 用JSON.stringify保证转义符显示,内容包裹在<a>里
+                        return '<div class="item item-line"><span class="string"><a href="'
+                            + htmlspecialchars(this.value) + '" target="_blank" rel="noopener noreferrer" data-is-link="1" data-link-url="' + htmlspecialchars(this.value) + '">' 
+                            + htmlspecialchars(JSON.stringify(this.value)) + '</a></span></div>';
+                    } else {
+                        return '<div class="item item-line"><span class="string">' + formatStringValue(JSON.stringify(this.value)) + '</span></div>';
+                    }
+                case 'number':
+                    // 确保大数字不使用科学计数法
+                    let numStr = typeof this.value === 'number' && this.value.toString().includes('e') 
+                        ? this.value.toLocaleString('fullwide', {useGrouping: false})
+                        : this.value;
+                    return '<div class="item item-line"><span class="number">' + 
+                        numStr + 
+                        '</span></div>';
+                case 'bigint':
+                    // 对BigInt类型特殊处理,只显示数字,不添加n后缀
+                    return '<div class="item item-line"><span class="number">' + 
+                        this.value.toString() + 
+                        '</span></div>';
+                case 'boolean':
+                    return '<div class="item item-line"><span class="bool">' + 
+                        this.value + 
+                        '</span></div>';
+                case 'null':
+                    return '<div class="item item-line"><span class="null">null</span></div>';
+                case 'object':
+                    return this.getObjectHTML();
+                case 'array':
+                    return this.getArrayHTML();
+                default:
+                    return '';
+            }
+        },
+        
+        getObjectHTML: function() {
+            if (!this.value || Object.keys(this.value).length === 0) {
+                return '<div class="item item-object"><span class="brace">{</span><span class="brace">}</span></div>';
+            }
+            
+            let html = '<div class="item item-object">' +
+                '<span class="expand"></span>' +
+                '<span class="brace">{</span>' +
+                '<span class="ellipsis"></span>' +
+                '<div class="kv-list">';
+                
+            let keys = Object.keys(this.value);
+            keys.forEach((key, index) => {
+                let prop = this.value[key];
+                let childNode = createNode(prop);
+                // 判断子节点是否为对象或数组,决定是否加item-block
+                let itemClass = (childNode.type === 'object' || childNode.type === 'array') ? 'item item-block' : 'item';
+                html += '<div class="' + itemClass + '">';
+                // 如果值是对象或数组,在key前面添加展开按钮
+                if (childNode.type === 'object' || childNode.type === 'array') {
+                    html += '<span class="expand"></span>';
+                }
+                html += '<span class="quote">"</span>' +
+                    '<span class="key">' + htmlspecialchars(key) + '</span>' +
+                    '<span class="quote">"</span>' +
+                    '<span class="colon">: </span>';
+                // 添加值
+                if (childNode.type === 'object' || childNode.type === 'array') {
+                    html += childNode.getInlineHTMLWithoutExpand();
+                } else {
+                    html += childNode.getHTML().replace(/^<div class="item item-line">/, '').replace(/<\/div>$/, '');
+                }
+                // 如果不是最后一个属性,添加逗号
+                if (index < keys.length - 1) {
+                    html += '<span class="comma">,</span>';
+                }
+                html += '</div>';
+            });
+            
+            html += '</div><span class="brace">}</span></div>';
+            return html;
+        },
+        
+        getArrayHTML: function() {
+            if (!this.value || this.value.length === 0) {
+                return '<div class="item item-array"><span class="brace">[</span><span class="brace">]</span></div>';
+            }
+            
+            let html = '<div class="item item-array">' +
+                '<span class="expand"></span>' +
+                '<span class="brace">[</span>' +
+                '<span class="ellipsis"></span>' +
+                '<div class="kv-list item-array-container">';
+                
+            this.value.forEach((item, index) => {
+                let childNode = createNode(item);
+                
+                html += '<div class="item item-block item-array-element" data-array-index="' + index + '">';
+                
+                // 如果数组元素是对象或数组,在前面添加展开按钮
+                if (childNode.type === 'object' || childNode.type === 'array') {
+                    html += '<span class="expand"></span>';
+                    html += childNode.getInlineHTMLWithoutExpand();
+                } else {
+                    html += childNode.getHTML().replace(/^<div class="item item-line">/, '').replace(/<\/div>$/, '');
+                }
+                
+                // 如果不是最后一个元素,添加逗号
+                if (index < this.value.length - 1) {
+                    html += '<span class="comma">,</span>';
+                }
+                
+                html += '</div>';
+            });
+            
+            html += '</div><span class="brace">]</span></div>';
+            return html;
+        },
+        
+        // 新增内联HTML方法,用于在同一行显示开始大括号/方括号
+        getInlineHTML: function() {
+            switch(this.type) {
+                case 'object':
+                    return this.getInlineObjectHTML();
+                case 'array':
+                    return this.getInlineArrayHTML();
+                default:
+                    return this.getHTML();
+            }
+        },
+        
+        // 新增不包含展开按钮的内联HTML方法
+        getInlineHTMLWithoutExpand: function() {
+            switch(this.type) {
+                case 'object':
+                    return this.getInlineObjectHTMLWithoutExpand();
+                case 'array':
+                    return this.getInlineArrayHTMLWithoutExpand();
+                default:
+                    return this.getHTML();
+            }
+        },
+        
+        getInlineObjectHTML: function() {
+            if (!this.value || Object.keys(this.value).length === 0) {
+                return '<span class="brace">{</span><span class="brace">}</span>';
+            }
+            let html = '<span class="brace">{</span>' +
+                '<span class="expand"></span>' +
+                '<span class="ellipsis"></span>' +
+                '<div class="kv-list">';
+            let keys = Object.keys(this.value);
+            keys.forEach((key, index) => {
+                let prop = this.value[key];
+                let childNode = createNode(prop);
+                // 判断子节点是否为对象或数组,决定是否加item-block
+                let itemClass = (childNode.type === 'object' || childNode.type === 'array') ? 'item item-block' : 'item';
+                html += '<div class="' + itemClass + '">';
+                if (childNode.type === 'object' || childNode.type === 'array') {
+                    html += '<span class="expand"></span>';
+                }
+                html += '<span class="quote">"</span>' +
+                    '<span class="key">' + htmlspecialchars(key) + '</span>' +
+                    '<span class="quote">"</span>' +
+                    '<span class="colon">: </span>';
+                if (childNode.type === 'object' || childNode.type === 'array') {
+                    html += childNode.getInlineHTMLWithoutExpand();
+                } else {
+                    html += childNode.getHTML().replace(/^<div class="item item-line">/, '').replace(/<\/div>$/, '');
+                }
+                if (index < keys.length - 1) {
+                    html += '<span class="comma">,</span>';
+                }
+                html += '</div>';
+            });
+            html += '</div><span class="brace">}</span>';
+            return html;
+        },
+        
+        getInlineArrayHTML: function() {
+            if (!this.value || this.value.length === 0) {
+                return '<span class="brace">[</span><span class="brace">]</span>';
+            }
+            
+            let html = '<span class="brace">[</span>' +
+                '<span class="expand"></span>' +
+                '<span class="ellipsis"></span>' +
+                '<div class="kv-list item-array-container">';
+                
+            this.value.forEach((item, index) => {
+                let childNode = createNode(item);
+                
+                html += '<div class="item item-block item-array-element" data-array-index="' + index + '">';
+                
+                // 如果数组元素是对象或数组,在前面添加展开按钮
+                if (childNode.type === 'object' || childNode.type === 'array') {
+                    html += '<span class="expand"></span>';
+                    html += childNode.getInlineHTMLWithoutExpand();
+                } else {
+                    html += childNode.getHTML().replace(/^<div class="item item-line">/, '').replace(/<\/div>$/, '');
+                }
+                
+                // 如果不是最后一个元素,添加逗号
+                if (index < this.value.length - 1) {
+                    html += '<span class="comma">,</span>';
+                }
+                
+                html += '</div>';
+            });
+            
+            html += '</div><span class="brace">]</span>';
+            return html;
+        },
+        
+        getInlineObjectHTMLWithoutExpand: function() {
+            if (!this.value || Object.keys(this.value).length === 0) {
+                return '<span class="brace">{</span><span class="brace">}</span>';
+            }
+            let html = '<span class="brace">{</span>' +
+                '<span class="ellipsis"></span>' +
+                '<div class="kv-list">';
+            let keys = Object.keys(this.value);
+            keys.forEach((key, index) => {
+                let prop = this.value[key];
+                let childNode = createNode(prop);
+                // 判断子节点是否为对象或数组,决定是否加item-block
+                let itemClass = (childNode.type === 'object' || childNode.type === 'array') ? 'item item-block' : 'item';
+                html += '<div class="' + itemClass + '">';
+                if (childNode.type === 'object' || childNode.type === 'array') {
+                    html += '<span class="expand"></span>';
+                }
+                html += '<span class="quote">"</span>' +
+                    '<span class="key">' + htmlspecialchars(key) + '</span>' +
+                    '<span class="quote">"</span>' +
+                    '<span class="colon">: </span>';
+                if (childNode.type === 'object' || childNode.type === 'array') {
+                    html += childNode.getInlineHTMLWithoutExpand();
+                } else {
+                    html += childNode.getHTML().replace(/^<div class="item item-line">/, '').replace(/<\/div>$/, '');
+                }
+                if (index < keys.length - 1) {
+                    html += '<span class="comma">,</span>';
+                }
+                html += '</div>';
+            });
+            html += '</div><span class="brace">}</span>';
+            return html;
+        },
+        
+        getInlineArrayHTMLWithoutExpand: function() {
+            if (!this.value || this.value.length === 0) {
+                return '<span class="brace">[</span><span class="brace">]</span>';
+            }
+            
+            let html = '<span class="brace">[</span>' +
+                '<span class="ellipsis"></span>' +
+                '<div class="kv-list item-array-container">';
+                
+            this.value.forEach((item, index) => {
+                let childNode = createNode(item);
+                
+                html += '<div class="item item-block item-array-element" data-array-index="' + index + '">';
+                
+                // 确保所有类型的数组元素都能正确处理
+                if (childNode.type === 'object' || childNode.type === 'array') {
+                    html += '<span class="expand"></span>';
+                    html += childNode.getInlineHTMLWithoutExpand();
+                } else {
+                    html += childNode.getHTML().replace(/^<div class="item item-line">/, '').replace(/<\/div>$/, '');
+                }
+                
+                // 如果不是最后一个元素,添加逗号
+                if (index < this.value.length - 1) {
+                    html += '<span class="comma">,</span>';
+                }
+                
+                html += '</div>';
+            });
+            
+            html += '</div><span class="brace">]</span>';
+            return html;
+        }
+    };
+    
+    return node;
+}
+
+// 获取值类型
+function getType(value) {
+    if (value === null) return 'null';
+    if (value === undefined) return 'undefined';
+    
+    let type = typeof value;
+    // 特别处理BigInt类型
+    if (type === 'bigint') return 'bigint';
+    if (type === 'object') {
+        if (Array.isArray(value)) return 'array';
+    }
+    return type;
+}
+
+function isUrl(str) {
+    const urlRegex = /^(https?:\/\/|ftp:\/\/)[^\s<>"'\\]+$/i;
+    return urlRegex.test(str);
+} 
+

+ 5 - 2
apps/loan-rate/index.html

@@ -14,8 +14,11 @@
     <div class="panel panel-default" style="margin-bottom: 0px;">
         <div class="panel-heading">
             <h3 class="panel-title">
-                <a href="https://www.baidufe.com/fehelper/index/index.html" target="_blank" class="x-a-high">
-                    <img src="../static/img/fe-16.png" alt="fehelper"/> FeHelper</a>:贷款利率计算器</h3>
+                <a href="https://fehelper.com" target="_blank" class="x-a-high">
+                    <img src="../static/img/fe-16.png" alt="fehelper"/> FeHelper</a>:贷款利率计算器
+                    <a href="#" class="x-donate-link" @click="openDonateModal($event)"><i class="nav-icon">❤&nbsp;</i>打赏鼓励</a>
+                    <a class="x-other-tools" @click="openOptionsPage($event)"><i class="icon-plus-circle"></i> 探索更多实用工具 <span class="tool-market-badge">工具市场</span></a>
+                </h3>
         </div>
     </div>
     <div class="panel-body">

+ 49 - 2
apps/loan-rate/index.js

@@ -3,7 +3,7 @@
  * @author zhaoxianlie
  */
 new Vue({
-    el: '#containerPayback',
+    el: '#pageContainer',
     data: {
         money: 10000,
         months: 12,
@@ -30,6 +30,8 @@ new Vue({
     mounted: function () {
         // 进制转换的初始化
         this.paybackConvert();
+
+        this.loadPatchHotfix();
     },
 
     methods: {
@@ -316,6 +318,51 @@ new Vue({
             this.money = this.revMoney;
             this.revAllInterest = Math.round((this.revAllAmount - this.money) * 100) / 100;
             this.revRate = guessRate(this.money, this.months, this.revAllAmount, parseInt(this.paybackMode));
-        }
+        },
+
+        openDonateModal: function(event) {
+            event.preventDefault();
+            event.stopPropagation();
+            chrome.runtime.sendMessage({
+                type: 'fh-dynamic-any-thing',
+                thing: 'open-donate-modal',
+                params: { toolName: 'loan-rate' }
+            });
+        },  
+
+        openOptionsPage: function(event) {
+            event.preventDefault();
+            event.stopPropagation();
+            chrome.runtime.openOptionsPage();
+        },
+
+
+        loadPatchHotfix() {
+            // 页面加载时自动获取并注入页面的补丁
+            chrome.runtime.sendMessage({
+                type: 'fh-dynamic-any-thing',
+                thing: 'fh-get-tool-patch',
+                toolName: 'loan-rate'
+            }, patch => {
+                if (patch) {
+                    if (patch.css) {
+                        const style = document.createElement('style');
+                        style.textContent = patch.css;
+                        document.head.appendChild(style);
+                    }
+                    if (patch.js) {
+                        try {
+                            if (window.evalCore && window.evalCore.getEvalInstance) {
+                                window.evalCore.getEvalInstance(window)(patch.js);
+                            }
+                        } catch (e) {
+                            console.error('loan-rate补丁JS执行失败', e);
+                        }
+                    }
+                }
+            });
+        },
+        
+        
     }
 });

+ 12 - 8
apps/manifest.json

@@ -1,7 +1,7 @@
 {
   "name": "FeHelper(前端助手)-Dev",
   "short_name": "FeHelper",
-  "version": "2025.04.1110",
+  "version": "2025.6.2901",
   "manifest_version": 3,
   "description": "JSON自动格式化、手动格式化,支持排序、解码、下载等,更多功能可在配置页按需安装!",
   "icons": {
@@ -29,8 +29,7 @@
     "activeTab",
     "storage",
     "notifications",
-    "unlimitedStorage",
-    "sidePanel"
+    "unlimitedStorage"
   ],
   "host_permissions": [
     "http://*/*",
@@ -57,13 +56,15 @@
             "json-format/json-abc.js",
             "json-format/json-bigint.js",
             "json-format/json-decode.js",
+            "json-format/json-worker.js",
             "static/vendor/jquery/jquery-3.3.1.min.js",
             "static/vendor/evalCore.min.js",
 
-            "code-beautify/beautify.js",
-            "code-beautify/beautify-css.js",
+            "background/awesome.js",
+            "background/tools.js",
 
-            "page-timing/timing.js"
+            "code-beautify/beautify.js",
+            "code-beautify/beautify-css.js"
         ],
         "matches": ["<all_urls>"]
       }
@@ -87,8 +88,11 @@
     }
   ],
   "content_security_policy": {
-      "extension_pages": "script-src 'self'; style-src 'self' 'unsafe-inline'; object-src 'self'"
+    "extension_pages": "script-src 'self'; style-src 'self' 'unsafe-inline'; object-src 'self'"
   },
   "update_url": "https://clients2.google.com/service/update2/crx",
-  "homepage_url": "https://www.baidufe.com/fehelper"
+  "homepage_url": "https://www.fehelper.com"
 }
+
+
+

+ 404 - 0
apps/mock-data/fake-data-lib.js

@@ -0,0 +1,404 @@
+/**
+ * 假数据生成器核心库
+ * 提供各种类型的假数据生成功能
+ */
+
+class FakeDataGenerator {
+    constructor() {
+        this.init();
+    }
+
+    init() {
+        // 中文姓氏
+        this.surnames = [
+            '王', '李', '张', '刘', '陈', '杨', '赵', '黄', '周', '吴',
+            '徐', '孙', '胡', '朱', '高', '林', '何', '郭', '马', '罗',
+            '梁', '宋', '郑', '谢', '韩', '唐', '冯', '于', '董', '萧'
+        ];
+
+        // 中文名字
+        this.givenNames = [
+            '伟', '芳', '娜', '秀英', '敏', '静', '丽', '强', '磊', '军',
+            '洋', '勇', '艳', '杰', '娟', '涛', '明', '超', '秀兰', '霞',
+            '平', '刚', '桂英', '永', '健', '鑫', '帅', '莉', '凯', '浩',
+            '宇', '琳', '雅', '欣', '晨', '阳', '雪', '晴', '萌', '悦'
+        ];
+
+        // 公司名称后缀
+        this.companySuffixes = [
+            '有限公司', '股份有限公司', '科技有限公司', '贸易有限公司',
+            '实业有限公司', '投资有限公司', '集团有限公司', '控股有限公司',
+            '发展有限公司', '建设有限公司', '咨询有限公司', '服务有限公司'
+        ];
+
+        // 公司名称前缀
+        this.companyPrefixes = [
+            '阿里巴巴', '腾讯', '百度', '京东', '美团', '字节跳动', '滴滴',
+            '小米', '华为', '网易', '新浪', '搜狐', '爱奇艺', '快手',
+            '拼多多', '携程', '途牛', '去哪儿', '58同城', '赶集网',
+            '优酷', '土豆', '乐视', '暴风', '金山', '猎豹', '360',
+            '蚂蚁金服', '陆金所', '恒生电子', '同花顺', '东方财富'
+        ];
+
+        // 部门名称
+        this.departments = [
+            '技术部', '产品部', '运营部', '市场部', '销售部', '人事部',
+            '财务部', '行政部', '法务部', '客服部', '设计部', '测试部',
+            '运维部', '数据部', '商务部', '品牌部', '公关部', '投资部'
+        ];
+
+        // 职位名称
+        this.positions = [
+            '前端工程师', '后端工程师', '全栈工程师', '移动端工程师', 'DevOps工程师',
+            '产品经理', '项目经理', '技术经理', '运营专员', '市场专员',
+            'UI设计师', 'UX设计师', '测试工程师', '数据分析师', '算法工程师',
+            '架构师', 'CTO', 'CEO', 'COO', 'CFO', '总监', '主管', '专员'
+        ];
+
+        // 省份
+        this.provinces = [
+            '北京市', '上海市', '天津市', '重庆市', '河北省', '山西省',
+            '辽宁省', '吉林省', '黑龙江省', '江苏省', '浙江省', '安徽省',
+            '福建省', '江西省', '山东省', '河南省', '湖北省', '湖南省',
+            '广东省', '海南省', '四川省', '贵州省', '云南省', '陕西省',
+            '甘肃省', '青海省', '台湾省', '内蒙古自治区', '广西壮族自治区',
+            '西藏自治区', '宁夏回族自治区', '新疆维吾尔自治区', '香港特别行政区', '澳门特别行政区'
+        ];
+
+        // 城市
+        this.cities = [
+            '北京', '上海', '广州', '深圳', '杭州', '南京', '武汉', '成都',
+            '西安', '郑州', '青岛', '大连', '宁波', '厦门', '福州', '长沙',
+            '济南', '重庆', '天津', '苏州', '无锡', '石家庄', '太原', '沈阳',
+            '长春', '哈尔滨', '合肥', '南昌', '昆明', '贵阳', '兰州', '银川'
+        ];
+
+        // User Agent 列表
+        this.userAgents = [
+            'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
+            'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
+            'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
+            'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
+            'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15'
+        ];
+
+        // MIME 类型
+        this.mimeTypes = [
+            'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml',
+            'text/html', 'text/css', 'text/javascript', 'text/plain', 'text/csv',
+            'application/json', 'application/xml', 'application/pdf', 'application/zip',
+            'video/mp4', 'video/webm', 'audio/mp3', 'audio/wav', 'audio/ogg'
+        ];
+
+        // 文件扩展名
+        this.fileExtensions = [
+            'jpg', 'png', 'gif', 'pdf', 'doc', 'docx', 'xls', 'xlsx',
+            'ppt', 'pptx', 'txt', 'csv', 'json', 'xml', 'zip', 'rar',
+            'mp4', 'avi', 'mov', 'mp3', 'wav', 'html', 'css', 'js'
+        ];
+
+        // 域名后缀
+        this.domainSuffixes = [
+            'com', 'cn', 'net', 'org', 'edu', 'gov', 'mil', 'int',
+            'com.cn', 'net.cn', 'org.cn', 'edu.cn', 'gov.cn'
+        ];
+
+        // 邮箱域名
+        this.emailDomains = [
+            'gmail.com', 'yahoo.com', 'hotmail.com', 'outlook.com', '163.com',
+            '126.com', 'qq.com', 'sina.com', 'sohu.com', 'foxmail.com',
+            'aliyun.com', 'yeah.net', 'vip.sina.com', 'vip.163.com'
+        ];
+    }
+
+    // 生成随机整数
+    randomInt(min, max) {
+        return Math.floor(Math.random() * (max - min + 1)) + min;
+    }
+
+    // 生成随机浮点数
+    randomFloat(min, max, precision = 2) {
+        const num = Math.random() * (max - min) + min;
+        return parseFloat(num.toFixed(precision));
+    }
+
+    // 从数组中随机选择一个元素
+    randomChoice(array) {
+        return array[Math.floor(Math.random() * array.length)];
+    }
+
+    // 生成随机字符串
+    randomString(length = 10, charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789') {
+        let result = '';
+        for (let i = 0; i < length; i++) {
+            result += charset.charAt(Math.floor(Math.random() * charset.length));
+        }
+        return result;
+    }
+
+    // 生成中文姓名
+    generateName() {
+        const surname = this.randomChoice(this.surnames);
+        const givenNameLength = Math.random() > 0.7 ? 2 : 1; // 70%概率生成双字名
+        let givenName = '';
+        for (let i = 0; i < givenNameLength; i++) {
+            givenName += this.randomChoice(this.givenNames);
+        }
+        return surname + givenName;
+    }
+
+    // 生成邮箱
+    generateEmail() {
+        const username = this.randomString(this.randomInt(6, 12));
+        const domain = this.randomChoice(this.emailDomains);
+        return `${username}@${domain}`;
+    }
+
+    // 生成手机号
+    generatePhone() {
+        const prefixes = ['130', '131', '132', '133', '134', '135', '136', '137', '138', '139',
+                         '150', '151', '152', '153', '155', '156', '157', '158', '159',
+                         '180', '181', '182', '183', '184', '185', '186', '187', '188', '189'];
+        const prefix = this.randomChoice(prefixes);
+        const suffix = this.randomString(8, '0123456789');
+        return prefix + suffix;
+    }
+
+    // 生成身份证号
+    generateIdCard() {
+        // 地区码(简化)
+        const areaCodes = ['110000', '120000', '130000', '140000', '150000', '210000', '220000', '230000'];
+        const areaCode = this.randomChoice(areaCodes);
+        
+        // 生日(1970-2000年)
+        const year = this.randomInt(1970, 2000);
+        const month = this.randomInt(1, 12).toString().padStart(2, '0');
+        const day = this.randomInt(1, 28).toString().padStart(2, '0');
+        const birthday = `${year}${month}${day}`;
+        
+        // 顺序码
+        const sequence = this.randomInt(100, 999).toString();
+        
+        // 校验码(简化为随机)
+        const checkCode = this.randomChoice(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'X']);
+        
+        return areaCode.substring(0, 6) + birthday + sequence + checkCode;
+    }
+
+    // 生成性别
+    generateGender() {
+        return Math.random() > 0.5 ? '男' : '女';
+    }
+
+    // 生成年龄
+    generateAge() {
+        return this.randomInt(18, 65);
+    }
+
+    // 生成生日
+    generateBirthday() {
+        const year = this.randomInt(1960, 2005);
+        const month = this.randomInt(1, 12);
+        const day = this.randomInt(1, 28);
+        return `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`;
+    }
+
+    // 生成地址
+    generateAddress() {
+        const province = this.randomChoice(this.provinces);
+        const city = this.randomChoice(this.cities);
+        const district = this.randomChoice(['朝阳区', '海淀区', '西城区', '东城区', '丰台区', '石景山区']);
+        const street = this.randomString(2, '一二三四五六七八九十') + '街道';
+        const number = this.randomInt(1, 999) + '号';
+        return `${province}${city}${district}${street}${number}`;
+    }
+
+    // 生成公司名称
+    generateCompany() {
+        const prefix = this.randomChoice(this.companyPrefixes);
+        const suffix = this.randomChoice(this.companySuffixes);
+        return prefix + suffix;
+    }
+
+    // 生成部门
+    generateDepartment() {
+        return this.randomChoice(this.departments);
+    }
+
+    // 生成职位
+    generatePosition() {
+        return this.randomChoice(this.positions);
+    }
+
+    // 生成薪资
+    generateSalary() {
+        return this.randomInt(5000, 50000);
+    }
+
+    // 生成银行卡号
+    generateBankCard() {
+        const prefixes = ['6225', '6222', '6228', '6229', '6227', '6223', '6226'];
+        const prefix = this.randomChoice(prefixes);
+        const suffix = this.randomString(12, '0123456789');
+        return prefix + suffix;
+    }
+
+    // 生成信用卡号
+    generateCreditCard() {
+        const prefixes = ['4', '5', '6'];
+        const prefix = this.randomChoice(prefixes);
+        const suffix = this.randomString(15, '0123456789');
+        return prefix + suffix;
+    }
+
+    // 生成价格
+    generatePrice() {
+        return this.randomFloat(0.01, 9999.99);
+    }
+
+    // 生成货币
+    generateCurrency() {
+        const currencies = ['CNY', 'USD', 'EUR', 'JPY', 'GBP', 'AUD', 'CAD', 'CHF', 'HKD', 'SGD'];
+        return this.randomChoice(currencies);
+    }
+
+    // 生成UUID
+    generateUUID() {
+        return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
+            const r = Math.random() * 16 | 0;
+            const v = c === 'x' ? r : (r & 0x3 | 0x8);
+            return v.toString(16);
+        });
+    }
+
+    // 生成IP地址
+    generateIP() {
+        return `${this.randomInt(1, 255)}.${this.randomInt(0, 255)}.${this.randomInt(0, 255)}.${this.randomInt(1, 255)}`;
+    }
+
+    // 生成MAC地址
+    generateMAC() {
+        const chars = '0123456789ABCDEF';
+        let mac = '';
+        for (let i = 0; i < 6; i++) {
+            if (i > 0) mac += ':';
+            mac += this.randomString(2, chars);
+        }
+        return mac;
+    }
+
+    // 生成User Agent
+    generateUserAgent() {
+        return this.randomChoice(this.userAgents);
+    }
+
+    // 生成URL
+    generateURL() {
+        const protocols = ['http', 'https'];
+        const domains = ['example.com', 'test.com', 'demo.com', 'sample.org', 'mock.net'];
+        const paths = ['/', '/home', '/about', '/contact', '/products', '/services', '/blog'];
+        
+        const protocol = this.randomChoice(protocols);
+        const domain = this.randomChoice(domains);
+        const path = this.randomChoice(paths);
+        
+        return `${protocol}://${domain}${path}`;
+    }
+
+    // 生成域名
+    generateDomain() {
+        const name = this.randomString(this.randomInt(5, 15));
+        const suffix = this.randomChoice(this.domainSuffixes);
+        return `${name}.${suffix}`;
+    }
+
+    // 生成密码
+    generatePassword() {
+        const length = this.randomInt(8, 16);
+        const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*';
+        return this.randomString(length, chars);
+    }
+
+    // 生成Token
+    generateToken() {
+        return this.randomString(32, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789');
+    }
+
+    // 生成颜色值
+    generateColor() {
+        return '#' + this.randomString(6, '0123456789ABCDEF');
+    }
+
+    // 生成时间戳
+    generateTimestamp() {
+        const now = Date.now();
+        const offset = this.randomInt(-365 * 24 * 60 * 60 * 1000, 365 * 24 * 60 * 60 * 1000);
+        return now + offset;
+    }
+
+    // 生成文件名
+    generateFilename() {
+        const name = this.randomString(this.randomInt(5, 15));
+        const ext = this.randomChoice(this.fileExtensions);
+        return `${name}.${ext}`;
+    }
+
+    // 生成MIME类型
+    generateMimeType() {
+        return this.randomChoice(this.mimeTypes);
+    }
+
+    // 生成布尔值
+    generateBoolean() {
+        return Math.random() > 0.5;
+    }
+
+    // 生成日期
+    generateDate() {
+        const start = new Date(2020, 0, 1);
+        const end = new Date();
+        const timestamp = start.getTime() + Math.random() * (end.getTime() - start.getTime());
+        return new Date(timestamp).toISOString().split('T')[0];
+    }
+
+    // 根据字段类型生成数据
+    generateByType(type) {
+        const generators = {
+            name: () => this.generateName(),
+            email: () => this.generateEmail(),
+            phone: () => this.generatePhone(),
+            idCard: () => this.generateIdCard(),
+            gender: () => this.generateGender(),
+            age: () => this.generateAge(),
+            birthday: () => this.generateBirthday(),
+            address: () => this.generateAddress(),
+            company: () => this.generateCompany(),
+            department: () => this.generateDepartment(),
+            position: () => this.generatePosition(),
+            salary: () => this.generateSalary(),
+            bankCard: () => this.generateBankCard(),
+            creditCard: () => this.generateCreditCard(),
+            price: () => this.generatePrice(),
+            currency: () => this.generateCurrency(),
+            uuid: () => this.generateUUID(),
+            ip: () => this.generateIP(),
+            mac: () => this.generateMAC(),
+            userAgent: () => this.generateUserAgent(),
+            url: () => this.generateURL(),
+            domain: () => this.generateDomain(),
+            password: () => this.generatePassword(),
+            token: () => this.generateToken(),
+            color: () => this.generateColor(),
+            timestamp: () => this.generateTimestamp(),
+            filename: () => this.generateFilename(),
+            mimeType: () => this.generateMimeType(),
+            boolean: () => this.generateBoolean(),
+            date: () => this.generateDate()
+        };
+
+        return generators[type] ? generators[type]() : null;
+    }
+}
+
+// 导出为全局变量
+window.FakeDataGenerator = FakeDataGenerator; 

+ 1095 - 0
apps/mock-data/index.css

@@ -0,0 +1,1095 @@
+body {
+    margin: 0;
+    padding: 0;
+    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+    background-color: #f5f7fa;
+}
+
+.wrapper {
+    padding-top: 10px;
+}
+
+/* 现代化导航栏样式 */
+.main-navbar {
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    color: white;
+    padding: 12px 20px;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+    position: sticky;
+    top: 0;
+    z-index: 1000;
+    margin-bottom: 0;
+}
+
+.navbar-brand {
+    display: flex;
+    align-items: center;
+}
+
+.brand-link {
+    color: white;
+    text-decoration: none;
+    display: flex;
+    align-items: center;
+    transition: all 0.3s ease;
+}
+
+.brand-link:hover {
+    color: rgba(255, 255, 255, 0.9);
+    transform: translateY(-1px);
+}
+
+.brand-link img {
+    margin-right: 8px;
+    border-radius: 2px;
+}
+
+.brand-text {
+    font-size: 18px;
+    font-weight: 600;
+    margin-right: 8px;
+}
+
+.brand-subtitle {
+    font-size: 14px;
+    opacity: 0.9;
+    position: relative;
+    padding-left: 8px;
+}
+
+.brand-subtitle::before {
+    content: "·";
+    position: absolute;
+    left: 0;
+    top: 50%;
+    transform: translateY(-50%);
+    font-weight: bold;
+    opacity: 0.7;
+}
+
+.navbar-actions {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+}
+
+.nav-item {
+    color: rgba(255, 255, 255, 0.9);
+    text-decoration: none;
+    padding: 8px 16px;
+    border-radius: 6px;
+    font-size: 14px;
+    font-weight: 500;
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    transition: all 0.3s ease;
+    position: relative;
+    overflow: hidden;
+    cursor: pointer;
+}
+
+.nav-item:hover {
+    background: rgba(255, 255, 255, 0.1);
+    color: white;
+    transform: translateY(-2px);
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
+}
+
+.nav-item:active {
+    transform: translateY(0);
+}
+
+.nav-icon {
+    font-style: normal;
+    font-size: 16px;
+    transition: transform 0.3s ease;
+}
+
+.nav-item:hover .nav-icon {
+    transform: scale(1.1);
+}
+
+.nav-item::after {
+    content: '';
+    position: absolute;
+    top: 0;
+    left: -100%;
+    width: 100%;
+    height: 100%;
+    background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
+    transition: left 0.5s;
+}
+
+.nav-item:hover::after {
+    left: 100%;
+}
+
+.navbar-actions .donate-link {
+    background: rgba(255, 107, 107, 0.2);
+    border: 1px solid rgba(255, 107, 107, 0.3);
+}
+
+.navbar-actions .donate-link:hover {
+    background: rgba(255, 107, 107, 0.3);
+    border-color: rgba(255, 107, 107, 0.5);
+    box-shadow: 0 4px 15px rgba(255, 107, 107, 0.3);
+}
+
+.navbar-actions .donate-link .nav-icon {
+    color: #ff6b6b;
+    animation: heartbeat 2s ease-in-out infinite;
+}
+
+.navbar-actions .donate-link:hover .nav-icon {
+    color: #ff5252;
+    animation: heartbeat 1s ease-in-out infinite;
+}
+
+.navbar-actions .other-tools-link {
+    background: rgba(255, 255, 255, 0.1);
+    border: 1px solid rgba(255, 255, 255, 0.2);
+}
+
+.navbar-actions .other-tools-link:hover {
+    background: rgba(255, 255, 255, 0.2);
+    border-color: rgba(255, 255, 255, 0.3);
+}
+
+.navbar-actions .other-tools-link .nav-icon {
+    color: #4ecdc4;
+    font-weight: bold;
+}
+
+.navbar-actions .other-tools-link:hover .nav-icon {
+    color: #26d0ce;
+    animation: rotate 0.6s ease-in-out;
+}
+
+@keyframes heartbeat {
+    0%, 14%, 28%, 42%, 70% {
+        transform: scale(1);
+    }
+    7%, 21%, 35% {
+        transform: scale(1.1);
+    }
+}
+
+@keyframes rotate {
+    0% {
+        transform: rotate(0deg);
+    }
+    100% {
+        transform: rotate(360deg);
+    }
+}
+
+.tool-market-badge {
+    background: rgba(255, 255, 255, 0.2);
+    padding: 2px 8px;
+    border-radius: 10px;
+    font-size: 11px;
+    font-weight: 500;
+    margin-left: 4px;
+    border: 1px solid rgba(255, 255, 255, 0.3);
+}
+
+/* 响应式导航栏 */
+@media (max-width: 768px) {
+    .main-navbar {
+        padding: 10px 15px;
+        flex-direction: column;
+        gap: 10px;
+    }
+    
+    .navbar-actions {
+        width: 100%;
+        justify-content: center;
+        flex-wrap: wrap;
+    }
+    
+    .nav-item {
+        font-size: 13px;
+        padding: 6px 12px;
+    }
+    
+    .brand-subtitle {
+        display: none;
+    }
+}
+
+@media (max-width: 600px) {
+    .nav-item span {
+        display: none;
+    }
+    
+    .nav-icon {
+        font-size: 18px;
+    }
+    
+    .tool-market-badge {
+        display: none;
+    }
+}
+
+.panel {
+    background: white;
+    border-radius: 8px;
+    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+    overflow: hidden;
+    margin-top: 0;
+}
+
+.panel-heading {
+    display: none; /* 隐藏旧的导航栏 */
+}
+
+.panel-title {
+    display: none; /* 隐藏旧的标题样式 */
+}
+
+/* 移除旧的导航栏样式 */
+.x-a-high,
+.x-other-tools,
+.x-donate-link,
+.icon-plus-circle {
+    display: none;
+}
+
+.panel-body {
+    padding: 20px;
+}
+
+/* 主要内容布局 */
+.main-content-layout {
+    display: flex;
+    gap: 20px;
+    height: calc(100vh - 120px);
+    min-height: 600px;
+}
+
+/* 左侧配置区域 */
+.config-section {
+    flex: 0 0 500px;
+    min-width: 480px;
+    overflow-y: auto;
+    padding-right: 10px;
+}
+
+/* 配置区域滚动条样式 */
+.config-section::-webkit-scrollbar {
+    width: 6px;
+}
+
+.config-section::-webkit-scrollbar-track {
+    background: #f1f1f1;
+    border-radius: 3px;
+}
+
+.config-section::-webkit-scrollbar-thumb {
+    background: #c1c1c1;
+    border-radius: 3px;
+}
+
+.config-section::-webkit-scrollbar-thumb:hover {
+    background: #a8a8a8;
+}
+
+/* 右侧结果区域 */
+.result-section {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+}
+
+.result-panel {
+    background: #f8f9fa;
+    border-radius: 8px;
+    border: 1px solid #e9ecef;
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+    overflow: hidden;
+}
+
+.result-header {
+    background: linear-gradient(135deg, #ddd 0%, #eee 100%);
+    color: #000;
+    padding: 10px 20px;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    border-bottom: none;
+}
+
+.result-header h4 {
+    margin: 0;
+    color: #000;
+    font-size: 16px;
+    font-weight: 600;
+}
+
+.result-actions {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+}
+
+.data-size {
+    background: rgba(255, 255, 255, 0.2);
+    padding: 4px 8px;
+    border-radius: 12px;
+    font-size: 12px;
+    font-weight: 500;
+}
+
+.btn-sm {
+    padding: 6px 12px;
+    font-size: 12px;
+    border-radius: 4px;
+}
+
+.result-content {
+    flex: 1;
+    overflow: auto;
+    background: white;
+}
+
+.result-content pre {
+    margin: 0;
+    padding: 20px;
+    background: #282c34;
+    color: #abb2bf;
+    font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+    font-size: 13px;
+    line-height: 1.5;
+    overflow-x: auto;
+    white-space: pre-wrap;
+    word-wrap: break-word;
+    height: 100%;
+    box-sizing: border-box;
+}
+
+/* 空结果状态 */
+.empty-result {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    height: 100%;
+    color: #6c757d;
+    text-align: center;
+    padding: 40px 20px;
+}
+
+.empty-icon {
+    font-size: 48px;
+    margin-bottom: 20px;
+    opacity: 0.7;
+}
+
+.empty-text h5 {
+    margin: 0 0 10px 0;
+    color: #495057;
+    font-size: 18px;
+    font-weight: 600;
+}
+
+.empty-text p {
+    margin: 0;
+    color: #6c757d;
+    font-size: 14px;
+}
+
+/* 数据类型选择区域 - 紧凑样式 */
+.data-type-section {
+    margin-bottom: 20px;
+}
+
+.data-type-section h4 {
+    margin: 0 0 10px 0;
+    color: #333;
+    font-size: 15px;
+    font-weight: 600;
+}
+
+.data-type-tabs {
+    display: flex;
+    gap: 2px;
+    background: #f1f3f4;
+    border-radius: 6px;
+    padding: 3px;
+}
+
+.data-tab {
+    flex: 1;
+    padding: 8px 12px;
+    border: none;
+    background: transparent;
+    color: #666;
+    border-radius: 4px;
+    cursor: pointer;
+    transition: all 0.3s ease;
+    font-size: 13px;
+    font-weight: 500;
+}
+
+.data-tab:hover {
+    background: rgba(102, 126, 234, 0.1);
+    color: #667eea;
+}
+
+.data-tab.active {
+    background: #667eea;
+    color: white;
+    box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
+}
+
+/* 数据内容区域 - 紧凑样式 */
+.data-content {
+    margin-bottom: 20px;
+}
+
+.field-group h5 {
+    margin: 0 0 10px 0;
+    color: #444;
+    font-size: 13px;
+    font-weight: 600;
+    border-bottom: 1px solid #e9ecef;
+    padding-bottom: 6px;
+}
+
+.field-row {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 12px;
+    margin-bottom: 12px;
+}
+
+.field-row label {
+    display: flex;
+    align-items: center;
+    cursor: pointer;
+    font-size: 13px;
+    color: #555;
+    min-width: 110px;
+    transition: color 0.3s ease;
+}
+
+.field-row label:hover {
+    color: #667eea;
+}
+
+.field-row input[type="checkbox"] {
+    margin-right: 6px;
+    width: 14px;
+    height: 14px;
+    cursor: pointer;
+}
+
+/* 自定义数据区域 - 紧凑样式 */
+.custom-input {
+    display: flex;
+    align-items: center;
+    margin-bottom: 10px;
+    gap: 8px;
+}
+
+.custom-input label {
+    min-width: 80px;
+    font-size: 13px;
+    color: #555;
+    font-weight: 500;
+}
+
+.custom-input input,
+.custom-input select {
+    flex: 1;
+    padding: 6px 10px;
+    border: 1px solid #ddd;
+    border-radius: 4px;
+    font-size: 13px;
+    transition: border-color 0.3s ease;
+}
+
+.custom-input input:focus,
+.custom-input select:focus {
+    outline: none;
+    border-color: #667eea;
+    box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.1);
+}
+
+.custom-fields-list {
+    margin-top: 15px;
+    padding: 12px;
+    background: #f8f9fa;
+    border-radius: 6px;
+}
+
+.custom-field-item {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 6px 0;
+    border-bottom: 1px solid #e9ecef;
+    font-size: 13px;
+}
+
+.custom-field-item:last-child {
+    border-bottom: none;
+}
+
+.btn-remove {
+    background: #dc3545;
+    color: white;
+    border: none;
+    padding: 4px 8px;
+    border-radius: 3px;
+    font-size: 12px;
+    cursor: pointer;
+    transition: background 0.3s ease;
+}
+
+.btn-remove:hover {
+    background: #c82333;
+}
+
+/* 生成配置 - 紧凑样式 */
+.generate-config {
+    margin-bottom: 20px;
+    padding: 12px;
+    background: #f8f9fa;
+    border-radius: 6px;
+    border-left: 4px solid #667eea;
+}
+
+.generate-config .config-row {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    margin-bottom: 8px;
+}
+
+.generate-config .config-row:last-child {
+    margin-bottom: 0;
+}
+
+/* 操作按钮 - 紧凑样式 */
+.action-buttons {
+    display: flex;
+    gap: 6px;
+    margin-bottom: 20px;
+    flex-wrap: wrap;
+}
+
+.action-buttons .btn {
+    flex: 1;
+    min-width: 70px;
+    padding: 4px 14px;
+    font-size: 13px;
+}
+
+/* 快速模板区域 - 顶部样式 */
+.template-section.template-top {
+    margin-top: 0;
+    margin-bottom: 15px;
+    padding-top: 0;
+    border-top: none;
+    padding-bottom: 12px;
+    border-bottom: 1px solid #e9ecef;
+}
+
+.template-section.template-top h4 {
+    margin: 0 0 10px 0;
+    color: #333;
+    font-size: 15px;
+    font-weight: 600;
+}
+
+/* 顶部操作按钮 - 特殊样式 */
+.action-buttons-top {
+    margin-bottom: 15px;
+    padding-bottom: 15px;
+    border-bottom: 2px solid #e9ecef;
+    display: flex;
+    gap: 8px;
+    flex-wrap: wrap;
+}
+
+.action-buttons-top .btn {
+    flex: 1;
+    min-width: 90px;
+    padding: 10px 16px;
+    font-size: 14px;
+    font-weight: 600;
+    border-radius: 6px;
+    transition: all 0.3s ease;
+}
+
+.action-buttons-top .btn-success {
+    background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
+    box-shadow: 0 2px 8px rgba(40, 167, 69, 0.3);
+}
+
+.action-buttons-top .btn-success:hover {
+    transform: translateY(-2px);
+    box-shadow: 0 4px 15px rgba(40, 167, 69, 0.4);
+}
+
+.action-buttons-top .btn-info {
+    background: linear-gradient(135deg, #17a2b8 0%, #6f42c1 100%);
+    box-shadow: 0 2px 8px rgba(23, 162, 184, 0.3);
+}
+
+.action-buttons-top .btn-info:hover {
+    transform: translateY(-2px);
+    box-shadow: 0 4px 15px rgba(23, 162, 184, 0.4);
+}
+
+.action-buttons-top .btn-warning {
+    background: linear-gradient(135deg, #ffc107 0%, #fd7e14 100%);
+    box-shadow: 0 2px 8px rgba(255, 193, 7, 0.3);
+}
+
+.action-buttons-top .btn-warning:hover {
+    transform: translateY(-2px);
+    box-shadow: 0 4px 15px rgba(255, 193, 7, 0.4);
+}
+
+.template-buttons {
+    display: grid;
+    grid-template-columns: repeat(auto-fit, minmax(90px, 1fr));
+    gap: 6px;
+}
+
+.template-buttons .btn-template {
+    padding: 6px 10px;
+    font-size: 12px;
+    min-width: 80px;
+    margin-bottom: 0;
+}
+
+/* 基础配置样式 - 紧凑调整 */
+.config-row label {
+    font-size: 13px;
+    color: #555;
+    font-weight: 500;
+    min-width: 70px;
+}
+
+.config-row input,
+.config-row select {
+    padding: 5px 8px;
+    border: 1px solid #ddd;
+    border-radius: 4px;
+    font-size: 13px;
+    flex: 1;
+    min-width: 100px;
+}
+
+/* 按钮基础样式调整 */
+.btn {
+    padding: 8px 16px;
+    border: none;
+    border-radius: 4px;
+    cursor: pointer;
+    font-size: 13px;
+    font-weight: 500;
+    transition: all 0.3s ease;
+    text-decoration: none;
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+}
+
+.btn:disabled {
+    opacity: 0.4;
+    cursor: not-allowed;
+    background: #6c757d !important;
+    color: rgba(255, 255, 255, 0.6) !important;
+    transform: none !important;
+    box-shadow: none !important;
+}
+
+.btn-success {
+    background: #28a745;
+    color: white;
+}
+
+.btn-success:hover:not(:disabled) {
+    background: #218838;
+    transform: translateY(-1px);
+    box-shadow: 0 4px 12px rgba(40, 167, 69, 0.3);
+}
+
+.btn-info {
+    background: #17a2b8;
+    color: white;
+}
+
+.btn-info:hover:not(:disabled) {
+    background: #138496;
+    transform: translateY(-1px);
+    box-shadow: 0 4px 12px rgba(23, 162, 184, 0.3);
+}
+
+.btn-warning {
+    background: #ffc107;
+    color: #212529;
+}
+
+.btn-warning:hover:not(:disabled) {
+    background: #e0a800;
+    transform: translateY(-1px);
+    box-shadow: 0 4px 12px rgba(255, 193, 7, 0.3);
+}
+
+.btn-primary {
+    background: #667eea;
+    color: white;
+}
+
+.btn-primary:hover:not(:disabled) {
+    background: #5a6fd8;
+    transform: translateY(-1px);
+    box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
+}
+
+.btn-secondary {
+    background: #6c757d;
+    color: white;
+}
+
+.btn-secondary:hover:not(:disabled) {
+    background: #5a6268;
+    transform: translateY(-1px);
+    box-shadow: 0 4px 12px rgba(108, 117, 125, 0.3);
+}
+
+.btn-template {
+    background: #e9ecef;
+    color: #495057;
+    border: 1px solid #ced4da;
+}
+
+.btn-template:hover {
+    background: #667eea;
+    color: white;
+    border-color: #667eea;
+    transform: translateY(-1px);
+}
+
+/* 移除原有的模板区域样式 */
+.template-section:not(.template-top) {
+    display: none;
+}
+
+/* 响应式调整 */
+@media (max-width: 1200px) {
+    .config-section {
+        flex: 0 0 400px;
+        min-width: 360px;
+    }
+}
+
+@media (max-width: 1024px) {
+    .main-content-layout {
+        flex-direction: column;
+        height: auto;
+        gap: 15px;
+    }
+    
+    .config-section {
+        flex: none;
+        width: 100%;
+        max-height: none;
+        overflow-y: visible;
+        padding-right: 0;
+    }
+    
+    .result-section {
+        flex: none;
+        min-height: 400px;
+    }
+    
+    .result-panel {
+        height: 400px;
+    }
+}
+
+@media (max-width: 768px) {
+    .panel-body {
+        padding: 15px;
+    }
+    
+    .main-content-layout {
+        gap: 10px;
+    }
+    
+    .data-type-tabs {
+        flex-direction: column;
+        gap: 2px;
+    }
+    
+    .data-tab {
+        width: 100%;
+    }
+    
+    .field-row {
+        flex-direction: column;
+        gap: 8px;
+    }
+    
+    .field-row label {
+        min-width: auto;
+        width: 100%;
+    }
+    
+    .action-buttons,
+    .action-buttons-top {
+        flex-direction: column;
+    }
+    
+    .action-buttons .btn,
+    .action-buttons-top .btn {
+        width: 100%;
+        flex: none;
+    }
+    
+    .template-buttons {
+        grid-template-columns: 1fr;
+        gap: 4px;
+    }
+    
+    .template-buttons .btn-template {
+        width: 100%;
+    }
+    
+    .result-actions {
+        flex-direction: column;
+        gap: 5px;
+        align-items: flex-end;
+    }
+    
+    .config-row {
+        flex-direction: column;
+        align-items: flex-start;
+        gap: 5px;
+    }
+    
+    .config-row label {
+        min-width: auto;
+    }
+    
+    .config-row input,
+    .config-row select {
+        width: 100%;
+        min-width: auto;
+    }
+}
+
+/* Vue.js 过渡效果 */
+[v-cloak] {
+    display: none;
+}
+
+/* 加载动画 */
+.loading {
+    display: inline-block;
+    width: 20px;
+    height: 20px;
+    border: 3px solid rgba(255, 255, 255, 0.3);
+    border-radius: 50%;
+    border-top-color: #fff;
+    animation: spin 1s ease-in-out infinite;
+}
+
+@keyframes spin {
+    to {
+        transform: rotate(360deg);
+    }
+}
+
+/* 成功提示 */
+.success-message {
+    background: #d4edda;
+    color: #155724;
+    padding: 10px 15px;
+    border-radius: 4px;
+    margin-bottom: 15px;
+    border: 1px solid #c3e6cb;
+}
+
+/* 错误提示 */
+.error-message {
+    background: #f8d7da;
+    color: #721c24;
+    padding: 10px 15px;
+    border-radius: 4px;
+    margin-bottom: 15px;
+    border: 1px solid #f5c6cb;
+}
+
+/* 暗色模式支持 */
+@media (prefers-color-scheme: dark) {
+    body {
+        background-color: #1a1a1a;
+        color: #e0e0e0;
+    }
+    
+    .main-navbar {
+        background: linear-gradient(135deg, #434190 0%, #5a4b9d 100%);
+        box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
+    }
+    
+    .panel {
+        background: #2d2d2d;
+        box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3);
+    }
+    
+    .panel-body {
+        background: #2d2d2d;
+    }
+    
+    .field-row label {
+        color: #ccc;
+    }
+    
+    .custom-input input,
+    .custom-input select,
+    .config-row input,
+    .config-row select {
+        background: #3d3d3d;
+        border-color: #555;
+        color: #e0e0e0;
+    }
+    
+    .generate-config,
+    .custom-fields-list {
+        background: #3d3d3d;
+    }
+    
+    .result-content pre {
+        background: #1e1e1e;
+        color: #d4d4d4;
+    }
+    
+    .data-type-tabs {
+        background: #3d3d3d;
+    }
+    
+    .data-tab {
+        color: #ccc;
+    }
+    
+    .data-tab:hover {
+        background: rgba(102, 126, 234, 0.3);
+        color: #667eea;
+    }
+    
+    .data-tab.active {
+        background: #667eea;
+        color: white;
+    }
+    
+    .field-group h5 {
+        color: #e0e0e0;
+        border-bottom-color: #555;
+    }
+    
+    .data-type-section h4,
+    .template-section h4 {
+        color: #e0e0e0;
+    }
+    
+    .result-header {
+        background: #3d3d3d;
+        border-bottom-color: #555;
+        color: #e0e0e0;
+    }
+    
+    .result-header h4 {
+        color: #e0e0e0;
+    }
+    
+    .custom-field-item {
+        border-bottom-color: #555;
+        color: #ccc;
+    }
+    
+    .btn-template {
+        background: #3d3d3d;
+        color: #ccc;
+        border-color: #555;
+    }
+    
+    .btn-template:hover {
+        background: #667eea;
+        color: white;
+        border-color: #667eea;
+    }
+    
+    .config-section::-webkit-scrollbar-track {
+        background: #3d3d3d;
+    }
+    
+    .config-section::-webkit-scrollbar-thumb {
+        background: #666;
+    }
+    
+    .config-section::-webkit-scrollbar-thumb:hover {
+        background: #777;
+    }
+    
+    .result-panel {
+        background: #2d2d2d;
+        border-color: #555;
+    }
+    
+    .result-header {
+        background: linear-gradient(135deg, #5a619d 0%, #6f5aab 100%);
+    }
+    
+    .empty-result {
+        color: #ccc;
+    }
+    
+    .empty-text h5 {
+        color: #e0e0e0;
+    }
+    
+    .empty-text p {
+        color: #aaa;
+    }
+    
+    .config-row label {
+        color: #ccc;
+    }
+    
+    .template-section.template-top h4 {
+        color: #e0e0e0;
+    }
+    
+    .template-section.template-top {
+        border-bottom-color: #555;
+    }
+    
+    .action-buttons-top {
+        border-bottom-color: #555;
+    }
+    
+    .action-buttons-top .btn-success {
+        background: linear-gradient(135deg, #1e7e34 0%, #17a2b8 100%);
+    }
+    
+    .action-buttons-top .btn-info {
+        background: linear-gradient(135deg, #138496 0%, #5a6fd8 100%);
+    }
+    
+    .action-buttons-top .btn-warning {
+        background: linear-gradient(135deg, #e0a800 0%, #dc3545 100%);
+    }
+} 

+ 235 - 0
apps/mock-data/index.html

@@ -0,0 +1,235 @@
+<!DOCTYPE HTML>
+<html lang="zh-CN">
+<head>
+    <meta charset="utf-8"/>
+    <title>数据Mock工具 - FeHelper</title>
+    <link rel="stylesheet" href="index.css"/>
+    <script type="text/javascript" src="../static/vendor/evalCore.min.js"></script>
+    <script type="text/javascript" src="../static/vendor/vue/vue.js"></script>
+</head>
+
+<body>
+    <div class="wrapper" id="pageContainer">
+        <!-- 现代化顶部导航栏 -->
+        <div class="main-navbar">
+            <div class="navbar-brand">
+                <a href="https://fehelper.com" target="_blank" class="brand-link">
+                    <img src="../static/img/fe-16.png" alt="fehelper"/> 
+                    <span class="brand-text">FeHelper</span>
+                    <span class="brand-subtitle">数据Mock工具</span>
+                </a>
+            </div>
+            <div class="navbar-actions">
+                <a href="#" @click="openOptionsPage($event)" class="nav-item other-tools-link">
+                    <i class="nav-icon">⊕</i>
+                    <span>探索更多实用工具</span>
+                    <span class="tool-market-badge">工具市场</span>
+                </a>
+                <a href="#" @click="openDonateModal($event)" class="nav-item donate-link">
+                    <i class="nav-icon">❤</i>
+                    <span>打赏鼓励</span>
+                </a>
+            </div>
+        </div>
+
+        <div class="panel panel-default">
+            <!-- 主要内容 - 左右布局 -->
+            <div class="panel-body" v-cloak>
+                <div class="main-content-layout">
+                    <!-- 左侧配置操作区 -->
+                    <div class="config-section">
+                        <!-- 快速模板 -->
+                        <div class="template-section template-top">
+                            <h4>快速模板</h4>
+                            <div class="template-buttons">
+                                <button @click="loadTemplate('user')" class="btn btn-template">用户信息</button>
+                                <button @click="loadTemplate('employee')" class="btn btn-template">员工信息</button>
+                                <button @click="loadTemplate('product')" class="btn btn-template">商品信息</button>
+                                <button @click="loadTemplate('order')" class="btn btn-template">订单信息</button>
+                                <button @click="loadTemplate('api')" class="btn btn-template">API测试数据</button>
+                            </div>
+                        </div>
+
+                        <!-- 操作按钮 -->
+                        <div class="action-buttons action-buttons-top">
+                            <button @click="generateData" class="btn btn-success">生成数据</button>
+                            <button @click="selectAll" class="btn btn-info">全选</button>
+                            <button @click="clearAll" class="btn btn-warning">清空</button>
+                        </div>
+
+                        <!-- 数据类型选择区域 -->
+                        <div class="data-type-section">
+                            <h4>选择数据类型</h4>
+                            <div class="data-type-tabs">
+                                <button class="data-tab" 
+                                        :class="{ active: activeTab === 'personal' }"
+                                        @click="activeTab = 'personal'">
+                                    个人信息
+                                </button>
+                                <button class="data-tab" 
+                                        :class="{ active: activeTab === 'business' }"
+                                        @click="activeTab = 'business'">
+                                    商业数据
+                                </button>
+                                <button class="data-tab" 
+                                        :class="{ active: activeTab === 'technical' }"
+                                        @click="activeTab = 'technical'">
+                                    技术数据
+                                </button>
+                                <button class="data-tab" 
+                                        :class="{ active: activeTab === 'custom' }"
+                                        @click="activeTab = 'custom'">
+                                    自定义数据
+                                </button>
+                            </div>
+                        </div>
+
+                        <!-- 个人信息数据 -->
+                        <div v-show="activeTab === 'personal'" class="data-content">
+                            <div class="field-group">
+                                <h5>基本信息</h5>
+                                <div class="field-row">
+                                    <label><input type="checkbox" v-model="selectedFields.name"> 姓名</label>
+                                    <label><input type="checkbox" v-model="selectedFields.email"> 邮箱</label>
+                                    <label><input type="checkbox" v-model="selectedFields.phone"> 手机号</label>
+                                    <label><input type="checkbox" v-model="selectedFields.idCard"> 身份证号</label>
+                                </div>
+                                <div class="field-row">
+                                    <label><input type="checkbox" v-model="selectedFields.gender"> 性别</label>
+                                    <label><input type="checkbox" v-model="selectedFields.age"> 年龄</label>
+                                    <label><input type="checkbox" v-model="selectedFields.birthday"> 生日</label>
+                                    <label><input type="checkbox" v-model="selectedFields.address"> 地址</label>
+                                </div>
+                            </div>
+                        </div>
+
+                        <!-- 商业数据 -->
+                        <div v-show="activeTab === 'business'" class="data-content">
+                            <div class="field-group">
+                                <h5>商业信息</h5>
+                                <div class="field-row">
+                                    <label><input type="checkbox" v-model="selectedFields.company"> 公司名称</label>
+                                    <label><input type="checkbox" v-model="selectedFields.department"> 部门</label>
+                                    <label><input type="checkbox" v-model="selectedFields.position"> 职位</label>
+                                    <label><input type="checkbox" v-model="selectedFields.salary"> 薪资</label>
+                                </div>
+                                <div class="field-row">
+                                    <label><input type="checkbox" v-model="selectedFields.bankCard"> 银行卡号</label>
+                                    <label><input type="checkbox" v-model="selectedFields.creditCard"> 信用卡号</label>
+                                    <label><input type="checkbox" v-model="selectedFields.price"> 价格</label>
+                                    <label><input type="checkbox" v-model="selectedFields.currency"> 货币</label>
+                                </div>
+                            </div>
+                        </div>
+
+                        <!-- 技术数据 -->
+                        <div v-show="activeTab === 'technical'" class="data-content">
+                            <div class="field-group">
+                                <h5>技术信息</h5>
+                                <div class="field-row">
+                                    <label><input type="checkbox" v-model="selectedFields.uuid"> UUID</label>
+                                    <label><input type="checkbox" v-model="selectedFields.ip"> IP地址</label>
+                                    <label><input type="checkbox" v-model="selectedFields.mac"> MAC地址</label>
+                                    <label><input type="checkbox" v-model="selectedFields.userAgent"> User Agent</label>
+                                </div>
+                                <div class="field-row">
+                                    <label><input type="checkbox" v-model="selectedFields.url"> URL</label>
+                                    <label><input type="checkbox" v-model="selectedFields.domain"> 域名</label>
+                                    <label><input type="checkbox" v-model="selectedFields.password"> 密码</label>
+                                    <label><input type="checkbox" v-model="selectedFields.token"> Token</label>
+                                </div>
+                                <div class="field-row">
+                                    <label><input type="checkbox" v-model="selectedFields.color"> 颜色值</label>
+                                    <label><input type="checkbox" v-model="selectedFields.timestamp"> 时间戳</label>
+                                    <label><input type="checkbox" v-model="selectedFields.filename"> 文件名</label>
+                                    <label><input type="checkbox" v-model="selectedFields.mimeType"> MIME类型</label>
+                                </div>
+                            </div>
+                        </div>
+
+                        <!-- 自定义数据 -->
+                        <div v-show="activeTab === 'custom'" class="data-content">
+                            <div class="field-group">
+                                <h5>自定义规则</h5>
+                                <div class="custom-input">
+                                    <label>字段名称:</label>
+                                    <input type="text" v-model="customField.name" placeholder="请输入字段名">
+                                </div>
+                                <div class="custom-input">
+                                    <label>数据类型:</label>
+                                    <select v-model="customField.type">
+                                        <option value="string">字符串</option>
+                                        <option value="number">数字</option>
+                                        <option value="boolean">布尔值</option>
+                                        <option value="date">日期</option>
+                                        <option value="array">数组</option>
+                                    </select>
+                                </div>
+                                <div class="custom-input">
+                                    <label>生成规则:</label>
+                                    <input type="text" v-model="customField.rule" placeholder="如:长度10-20的随机字符串">
+                                </div>
+                                <button @click="addCustomField" class="btn btn-primary">添加字段</button>
+                            </div>
+                            
+                            <div v-if="customFields.length > 0" class="custom-fields-list">
+                                <h5>已添加字段</h5>
+                                <div v-for="(field, index) in customFields" :key="index" class="custom-field-item">
+                                    <span>{{ field.name }} ({{ field.type }})</span>
+                                    <button @click="removeCustomField(index)" class="btn-remove">删除</button>
+                                </div>
+                            </div>
+                        </div>
+
+                        <!-- 生成配置 -->
+                        <div class="generate-config">
+                            <div class="config-row">
+                                <label>生成数量:</label>
+                                <input type="number" v-model="generateCount" min="1" max="1000" value="10">
+                            </div>
+                            <div class="config-row">
+                                <label>输出格式:</label>
+                                <select v-model="outputFormat">
+                                    <option value="json">JSON</option>
+                                    <option value="csv">CSV</option>
+                                    <option value="sql">SQL INSERT</option>
+                                    <option value="xml">XML</option>
+                                </select>
+                            </div>
+                        </div>
+                    </div>
+
+                    <!-- 右侧结果展示区 -->
+                    <div class="result-section">
+                        <div class="result-panel">
+                            <div class="result-header">
+                                <h4 v-if="generatedData">生成结果 ({{ generateCount }} 条)</h4>
+                                <h4 v-else>数据结果</h4>
+                                <div class="result-actions">
+                                    <span class="data-size" v-if="generatedData">{{ dataSize }}</span>
+                                    <span class="data-size" v-else>等待生成</span>
+                                    <button @click="copyResult" class="btn btn-primary btn-sm" :disabled="!generatedData">复制结果</button>
+                                    <button @click="downloadData" class="btn btn-secondary btn-sm" :disabled="!generatedData">下载文件</button>
+                                </div>
+                            </div>
+                            <div class="result-content">
+                                <div v-if="!generatedData" class="empty-result">
+                                    <div class="empty-icon">📊</div>
+                                    <div class="empty-text">
+                                        <h5>暂无生成结果</h5>
+                                        <p>请选择数据字段并点击"生成数据"按钮</p>
+                                    </div>
+                                </div>
+                                <pre v-else>{{ generatedData }}</pre>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <script type="text/javascript" src="fake-data-lib.js"></script>
+    <script type="text/javascript" src="index.js"></script>
+</body>
+</html> 

+ 575 - 0
apps/mock-data/index.js

@@ -0,0 +1,575 @@
+/**
+ * 假数据生成器主逻辑
+ * 使用Vue.js构建交互式界面
+ */
+
+new Vue({
+    el: '#pageContainer',
+    data: {
+        // 当前激活的标签页
+        activeTab: 'personal',
+        
+        // 选中的字段
+        selectedFields: {
+            // 个人信息
+            name: false,
+            email: false,
+            phone: false,
+            idCard: false,
+            gender: false,
+            age: false,
+            birthday: false,
+            address: false,
+            
+            // 商业数据
+            company: false,
+            department: false,
+            position: false,
+            salary: false,
+            bankCard: false,
+            creditCard: false,
+            price: false,
+            currency: false,
+            
+            // 技术数据
+            uuid: false,
+            ip: false,
+            mac: false,
+            userAgent: false,
+            url: false,
+            domain: false,
+            password: false,
+            token: false,
+            color: false,
+            timestamp: false,
+            filename: false,
+            mimeType: false
+        },
+        
+        // 自定义字段
+        customField: {
+            name: '',
+            type: 'string',
+            rule: ''
+        },
+        customFields: [],
+        
+        // 生成配置
+        generateCount: 10,
+        outputFormat: 'json',
+        
+        // 生成的数据
+        generatedData: '',
+        dataSize: '0 B',
+        
+        // 数据生成器实例
+        generator: null,
+        
+        // 预设模板
+        templates: {
+            user: {
+                name: '用户信息模板',
+                fields: ['name', 'email', 'phone', 'gender', 'age', 'address']
+            },
+            employee: {
+                name: '员工信息模板',
+                fields: ['name', 'email', 'phone', 'company', 'department', 'position', 'salary']
+            },
+            product: {
+                name: '商品信息模板',
+                fields: ['name', 'price', 'currency', 'uuid', 'timestamp']
+            },
+            order: {
+                name: '订单信息模板',
+                fields: ['uuid', 'name', 'email', 'phone', 'address', 'price', 'timestamp']
+            },
+            api: {
+                name: 'API测试数据模板',
+                fields: ['uuid', 'token', 'ip', 'userAgent', 'timestamp', 'boolean']
+            }
+        }
+    },
+    
+    mounted() {
+        // 初始化数据生成器
+        this.generator = new FakeDataGenerator();
+        
+        // 检查URL参数,如果有模板参数则自动加载
+        const urlParams = new URLSearchParams(window.location.search);
+        const template = urlParams.get('template');
+        if (template && this.templates[template]) {
+            this.loadTemplate(template);
+        }
+
+        this.loadPatchHotfix();
+    },
+    
+    methods: {
+        /**
+         * 添加自定义字段
+         */
+        addCustomField() {
+            if (!this.customField.name.trim()) {
+                alert('请输入字段名称');
+                return;
+            }
+            
+            // 检查字段名是否已存在
+            const exists = this.customFields.some(field => field.name === this.customField.name);
+            if (exists) {
+                alert('字段名已存在');
+                return;
+            }
+            
+            this.customFields.push({
+                name: this.customField.name,
+                type: this.customField.type,
+                rule: this.customField.rule
+            });
+            
+            // 重置输入框
+            this.customField = {
+                name: '',
+                type: 'string',
+                rule: ''
+            };
+        },
+        
+        /**
+         * 删除自定义字段
+         */
+        removeCustomField(index) {
+            this.customFields.splice(index, 1);
+        },
+        
+        /**
+         * 全选当前标签页的字段
+         */
+        selectAll() {
+            const fieldGroups = {
+                personal: ['name', 'email', 'phone', 'idCard', 'gender', 'age', 'birthday', 'address'],
+                business: ['company', 'department', 'position', 'salary', 'bankCard', 'creditCard', 'price', 'currency'],
+                technical: ['uuid', 'ip', 'mac', 'userAgent', 'url', 'domain', 'password', 'token', 'color', 'timestamp', 'filename', 'mimeType']
+            };
+            
+            const fields = fieldGroups[this.activeTab] || [];
+            fields.forEach(field => {
+                this.selectedFields[field] = true;
+            });
+        },
+        
+        /**
+         * 清空所有选择
+         */
+        clearAll() {
+            Object.keys(this.selectedFields).forEach(key => {
+                this.selectedFields[key] = false;
+            });
+            this.customFields = [];
+            this.generatedData = '';
+            this.dataSize = '0 B';
+        },
+        
+        /**
+         * 生成假数据
+         */
+        generateData() {
+            const selectedFieldKeys = Object.keys(this.selectedFields).filter(key => this.selectedFields[key]);
+            
+            // 检查是否选择了字段
+            if (selectedFieldKeys.length === 0 && this.customFields.length === 0) {
+                alert('请至少选择一个字段');
+                return;
+            }
+            
+            // 生成数据
+            const data = [];
+            for (let i = 0; i < this.generateCount; i++) {
+                const item = {};
+                
+                // 生成预定义字段
+                selectedFieldKeys.forEach(field => {
+                    item[field] = this.generator.generateByType(field);
+                });
+                
+                // 生成自定义字段
+                this.customFields.forEach(field => {
+                    item[field.name] = this.generateCustomFieldData(field);
+                });
+                
+                data.push(item);
+            }
+            
+            // 格式化输出
+            this.formatOutput(data);
+        },
+        
+        /**
+         * 生成自定义字段数据
+         */
+        generateCustomFieldData(field) {
+            switch (field.type) {
+                case 'string':
+                    return this.generator.randomString(this.generator.randomInt(5, 20));
+                case 'number':
+                    return this.generator.randomInt(1, 1000);
+                case 'boolean':
+                    return this.generator.generateBoolean();
+                case 'date':
+                    return this.generator.generateDate();
+                case 'array':
+                    const arrayLength = this.generator.randomInt(1, 5);
+                    const array = [];
+                    for (let i = 0; i < arrayLength; i++) {
+                        array.push(this.generator.randomString(5));
+                    }
+                    return array;
+                default:
+                    return this.generator.randomString(10);
+            }
+        },
+        
+        /**
+         * 格式化输出数据
+         */
+        formatOutput(data) {
+            switch (this.outputFormat) {
+                case 'json':
+                    this.generatedData = JSON.stringify(data, null, 2);
+                    break;
+                case 'csv':
+                    this.generatedData = this.convertToCSV(data);
+                    break;
+                case 'sql':
+                    this.generatedData = this.convertToSQL(data);
+                    break;
+                case 'xml':
+                    this.generatedData = this.convertToXML(data);
+                    break;
+                default:
+                    this.generatedData = JSON.stringify(data, null, 2);
+            }
+            
+            // 计算数据大小
+            this.dataSize = this.calculateSize(this.generatedData);
+        },
+        
+        /**
+         * 转换为CSV格式
+         */
+        convertToCSV(data) {
+            if (data.length === 0) return '';
+            
+            const headers = Object.keys(data[0]);
+            const csvContent = [
+                headers.join(','),
+                ...data.map(row => 
+                    headers.map(header => {
+                        const value = row[header];
+                        // 处理包含逗号或引号的值
+                        if (typeof value === 'string' && (value.includes(',') || value.includes('"'))) {
+                            return `"${value.replace(/"/g, '""')}"`;
+                        }
+                        return value;
+                    }).join(',')
+                )
+            ].join('\n');
+            
+            return csvContent;
+        },
+        
+        /**
+         * 转换为SQL INSERT语句
+         */
+        convertToSQL(data) {
+            if (data.length === 0) return '';
+            
+            const tableName = 'fake_data';
+            const headers = Object.keys(data[0]);
+            
+            let sql = `-- 表结构\nCREATE TABLE ${tableName} (\n`;
+            sql += headers.map(header => `  ${header} VARCHAR(255)`).join(',\n');
+            sql += '\n);\n\n-- 数据插入\n';
+            
+            data.forEach(row => {
+                const values = headers.map(header => {
+                    const value = row[header];
+                    if (typeof value === 'string') {
+                        return `'${value.replace(/'/g, "''")}'`;
+                    }
+                    return value;
+                }).join(', ');
+                
+                sql += `INSERT INTO ${tableName} (${headers.join(', ')}) VALUES (${values});\n`;
+            });
+            
+            return sql;
+        },
+        
+        /**
+         * 转换为XML格式
+         */
+        convertToXML(data) {
+            if (data.length === 0) return '';
+            
+            let xml = '<?xml version="1.0" encoding="UTF-8"?>\n<data>\n';
+            
+            data.forEach((item, index) => {
+                xml += `  <item id="${index + 1}">\n`;
+                Object.keys(item).forEach(key => {
+                    const value = item[key];
+                    xml += `    <${key}>${this.escapeXML(value)}</${key}>\n`;
+                });
+                xml += '  </item>\n';
+            });
+            
+            xml += '</data>';
+            return xml;
+        },
+        
+        /**
+         * XML字符转义
+         */
+        escapeXML(value) {
+            if (typeof value !== 'string') {
+                value = String(value);
+            }
+            return value
+                .replace(/&/g, '&amp;')
+                .replace(/</g, '&lt;')
+                .replace(/>/g, '&gt;')
+                .replace(/"/g, '&quot;')
+                .replace(/'/g, '&apos;');
+        },
+        
+        /**
+         * 计算数据大小
+         */
+        calculateSize(data) {
+            const bytes = new Blob([data]).size;
+            const sizes = ['B', 'KB', 'MB', 'GB'];
+            
+            if (bytes === 0) return '0 B';
+            
+            const i = Math.floor(Math.log(bytes) / Math.log(1024));
+            const size = (bytes / Math.pow(1024, i)).toFixed(2);
+            
+            return `${size} ${sizes[i]}`;
+        },
+        
+        /**
+         * 复制结果到剪贴板
+         */
+        async copyResult() {
+            if (!this.generatedData) {
+                alert('没有可复制的数据');
+                return;
+            }
+            
+            try {
+                await navigator.clipboard.writeText(this.generatedData);
+                this.showMessage('数据已复制到剪贴板', 'success');
+            } catch (err) {
+                console.error('复制失败:', err);
+                // 备用方法
+                this.fallbackCopyText(this.generatedData);
+            }
+        },
+        
+        /**
+         * 备用复制方法
+         */
+        fallbackCopyText(text) {
+            const textArea = document.createElement('textarea');
+            textArea.value = text;
+            textArea.style.position = 'fixed';
+            textArea.style.left = '-999999px';
+            textArea.style.top = '-999999px';
+            document.body.appendChild(textArea);
+            textArea.focus();
+            textArea.select();
+            
+            try {
+                document.execCommand('copy');
+                this.showMessage('数据已复制到剪贴板', 'success');
+            } catch (err) {
+                this.showMessage('复制失败,请手动选择复制', 'error');
+            }
+            
+            document.body.removeChild(textArea);
+        },
+        
+        /**
+         * 下载数据文件
+         */
+        downloadData() {
+            if (!this.generatedData) {
+                alert('没有可下载的数据');
+                return;
+            }
+            
+            const extensions = {
+                json: 'json',
+                csv: 'csv',
+                sql: 'sql',
+                xml: 'xml'
+            };
+            
+            const mimeTypes = {
+                json: 'application/json',
+                csv: 'text/csv',
+                sql: 'application/sql',
+                xml: 'application/xml'
+            };
+            
+            const extension = extensions[this.outputFormat] || 'txt';
+            const mimeType = mimeTypes[this.outputFormat] || 'text/plain';
+            
+            const blob = new Blob([this.generatedData], { type: mimeType });
+            const url = URL.createObjectURL(blob);
+            
+            const link = document.createElement('a');
+            link.href = url;
+            link.download = `fake-data-${Date.now()}.${extension}`;
+            document.body.appendChild(link);
+            link.click();
+            document.body.removeChild(link);
+            
+            URL.revokeObjectURL(url);
+            this.showMessage('文件下载已开始', 'success');
+        },
+        
+        /**
+         * 加载预设模板
+         */
+        loadTemplate(templateKey) {
+            const template = this.templates[templateKey];
+            if (!template) return;
+            
+            // 清空当前选择
+            this.clearAll();
+            
+            // 选择模板字段
+            template.fields.forEach(field => {
+                if (this.selectedFields.hasOwnProperty(field)) {
+                    this.selectedFields[field] = true;
+                }
+            });
+            
+            // 切换到相应的标签页
+            if (template.fields.some(field => ['name', 'email', 'phone', 'idCard', 'gender', 'age', 'birthday', 'address'].includes(field))) {
+                this.activeTab = 'personal';
+            } else if (template.fields.some(field => ['company', 'department', 'position', 'salary', 'bankCard', 'creditCard', 'price', 'currency'].includes(field))) {
+                this.activeTab = 'business';
+            } else {
+                this.activeTab = 'technical';
+            }
+            
+            // 自动生成数据
+            this.$nextTick(() => {
+                this.generateData();
+            });
+            
+            this.showMessage(`已加载 ${template.name} 并生成数据`, 'success');
+        },
+        
+
+        // 打开工具市场页面
+        openOptionsPage: function(event){
+            event.preventDefault();
+            event.stopPropagation();
+            chrome.runtime.openOptionsPage();
+        },
+
+        openDonateModal: function(event){
+            event.preventDefault();
+            event.stopPropagation();
+            chrome.runtime.sendMessage({
+                type: 'fh-dynamic-any-thing',
+                thing: 'open-donate-modal',
+                params: { toolName: 'mock-data' }
+            });
+        },
+
+        /**
+         * 显示消息提示
+         */
+        showMessage(message, type = 'info') {
+            // 创建消息元素
+            const messageEl = document.createElement('div');
+            messageEl.className = `${type}-message`;
+            messageEl.textContent = message;
+            messageEl.style.position = 'fixed';
+            messageEl.style.top = '20px';
+            messageEl.style.right = '20px';
+            messageEl.style.zIndex = '9999';
+            messageEl.style.padding = '10px 15px';
+            messageEl.style.borderRadius = '4px';
+            messageEl.style.fontSize = '14px';
+            messageEl.style.fontWeight = '500';
+            messageEl.style.boxShadow = '0 2px 12px rgba(0, 0, 0, 0.15)';
+            
+            // 设置样式
+            if (type === 'success') {
+                messageEl.style.background = '#d4edda';
+                messageEl.style.color = '#155724';
+                messageEl.style.border = '1px solid #c3e6cb';
+            } else if (type === 'error') {
+                messageEl.style.background = '#f8d7da';
+                messageEl.style.color = '#721c24';
+                messageEl.style.border = '1px solid #f5c6cb';
+            } else {
+                messageEl.style.background = '#d1ecf1';
+                messageEl.style.color = '#0c5460';
+                messageEl.style.border = '1px solid #bee5eb';
+            }
+            
+            document.body.appendChild(messageEl);
+            
+            // 3秒后自动移除
+            setTimeout(() => {
+                if (messageEl.parentNode) {
+                    messageEl.parentNode.removeChild(messageEl);
+                }
+            }, 3000);
+        },
+
+
+        loadPatchHotfix() {
+            // 页面加载时自动获取并注入页面的补丁
+            chrome.runtime.sendMessage({
+                type: 'fh-dynamic-any-thing',
+                thing: 'fh-get-tool-patch',
+                toolName: 'mock-data'
+            }, patch => {
+                if (patch) {
+                    if (patch.css) {
+                        const style = document.createElement('style');
+                        style.textContent = patch.css;
+                        document.head.appendChild(style);
+                    }
+                    if (patch.js) {
+                        try {
+                            if (window.evalCore && window.evalCore.getEvalInstance) {
+                                window.evalCore.getEvalInstance(window)(patch.js);
+                            }
+                        } catch (e) {
+                            console.error('mock-data补丁JS执行失败', e);
+                        }
+                    }
+                }
+            });
+        },
+    },
+    
+    watch: {
+        // 监听生成数量变化,限制范围
+        generateCount(newVal) {
+            if (newVal < 1) {
+                this.generateCount = 1;
+            } else if (newVal > 1000) {
+                this.generateCount = 1000;
+            }
+        }
+    }
+}); 

+ 2 - 2
apps/naotu/index.html

@@ -10,7 +10,7 @@
 </head>
 <body>
 <div id="mainContainer">
-	<h1 class="editor-title"><a href="https://www.baidufe.com/fehelper/index/index.html" target="_blank" class="x-a-high">
+	<h1 class="editor-title"><a href="https://fehelper.com" target="_blank" class="x-a-high">
 		<img src="../static/img/fe-16.png" alt="fehelper"/> FeHelper</a>:脑图工具
 	</h1>
 	<kityminder-editor on-init="initEditor(editor, minder)"></kityminder-editor>
@@ -20,7 +20,7 @@
 		<div class="mm-tips">
 			很抱歉,按Google Chrome Extension的平台要求,必须升级到MV3;因此脑图功能基本无法在Extension中使用!<br/>
 			你可以选择将内容<a href="#" @click="exportNaotu('json')" class="x-tool">全部导出为JSON</a>,然后到FeHelper的官网上,
-			通过<a href="https://www.baidufe.com/fehelper/naotu/index.html" target="_blank">在线版脑图工具</a>继续使用!
+			通过<a href="https://fehelper.com/v1/naotu/index.html" target="_blank">在线版脑图工具</a>继续使用!
 		</div>
 
 		<div class="x-y">下面是你之前存储的文件

+ 1967 - 234
apps/options/index.css

@@ -1,234 +1,1967 @@
-@import "../static/css/bootstrap.min.css";
-
-.wrapper {
-    width:auto;
-}
-
-#pageContainer .panel-body {
-    margin: 0 auto;
-    width: 960px;
-    padding: 15px;
-}
-.mod-header {
-    position: sticky;
-    top:0;
-    z-index: 100;
-    background-color: #fff;
-    box-shadow: 0 1px 2px rgba(0,0,0,0.05);
-    margin-bottom: 10px;
-}
-#pageContainer label {
-    font-weight: normal;
-    margin-bottom: 0;
-}
-
-#pageContainer h4 {
-    color: #333;
-    font-size: 18px;
-    margin-top: 25px;
-    margin-bottom: 12px;
-    padding-bottom: 8px;
-    border-bottom : 1px solid #eee;
-}
-
-h5,h4 {
-    padding-bottom: 5px;
-}
-.t-hlt {
-    color:#f00;
-    font-style: italic;
-}
-
-#MAX_JSON_KEYS_NUMBER {
-    width: 80px;
-    height: auto;
-    display: inline-block;
-    padding: 4px 10px;
-}
-.x-disabled {
-    opacity: 0.2;
-}
-.box-config {
-    padding: 10px;
-    border: 1px solid #e5e5e5;
-    border-radius: 8px;
-    border-top: 0;
-}
-
-#setShortcuts {
-    font-size: 14px;
-    text-decoration: none;
-    color: #007bff;
-    display: inline-block;
-    margin-bottom: 5px;
-}
-#setShortcuts:hover {
-    color:#0056b3;
-    text-decoration: underline;
-}
-.x-tips {
-    font-size: 12px;
-    color: #999;
-}
-.x-tips b i {
-    text-decoration: underline;
-}
-.x-donate {
-    font-size: 14px;
-    color: #d00;
-    margin-left: 10px;
-    text-decoration: underline;
-    text-decoration-style: double;
-    cursor: default;
-}
-.box-donate .x-donate-img {
-    width: 360px;
-}
-.btn-donate {
-    padding: 0 20px;
-}
-.mod-dynamic-tool {
-    padding-left: 0;
-    border: 1px solid #eee;
-    border-radius: 4px;
-    overflow: hidden;
-}
-.mod-dynamic-tool li {
-    list-style: none;
-    border-bottom: 1px solid #eee;
-    padding: 8px 12px;
-    display: flex;
-    align-items: center;
-    gap: 8px;
-    background-color: #fff;
-}
-.mod-dynamic-tool li:last-child {
-    border-bottom: none;
-}
-.mod-dynamic-tool li:hover {
-    border-bottom-color: #eee;
-    background-color: #f8f9fa;
-}
-.mod-dynamic-tool li .x-btn-box {
-    text-align: right;
-    min-width: auto;
-    margin-left: auto;
-    display: flex;
-    gap: 5px;
-}
-.mod-dynamic-tool li i {
-    width: 16px;
-    height: 16px;
-    display: inline-flex;
-    align-items: center;
-    justify-content: center;
-    margin-right: 4px;
-    color: #495057;
-    font-style: normal;
-    font-size: 14px;
-    flex-shrink: 0;
-}
-.mod-dynamic-tool li b {
-    width: auto;
-    flex-shrink: 0;
-    color: #333;
-}
-.mod-dynamic-tool li .x-tips {
-    flex-grow: 1;
-    white-space: nowrap;
-    overflow: hidden;
-    text-overflow: ellipsis;
-    color: #999;
-}
-.mod-dynamic-tool .btn-xs {
-    padding: 2px 8px;
-    font-size: 12px;
-    border-radius: 3px;
-}
-.mod-dynamic-tool .btn {
-    position: relative;
-}
-.st-item {
-    margin-bottom: 10px;
-}
-.st-item input[type="checkbox"] {
-    margin-right: 5px;
-}
-.st-item label {
-    line-height: normal;
-}
-.st-item label .x-tips {
-}
-.st-item .box-inner {
-}
-.st-item .a-btn {
-    font-size: 12px;
-    text-decoration: underline;
-}
-.btn-disabled,
-.btn-disabled:hover,
-.btn-disabled:active,
-.btn-disabled:focus{
-    color: #aaa;
-    background-color: #ddd;
-    border-color: #ddd;
-    cursor:not-allowed;
-}
-.x-sort-box {
-    margin-right: 10px;
-    user-select: none;
-    display: flex;
-    align-items: center;
-    gap: 5px;
-}
-.x-sort-box i {
-    cursor: pointer;
-    font-weight: bold;
-}
-.x-sort-box i:hover {
-    color:#007bff;
-}
-.x-sort-box i.x-hidden {
-    opacity: 0.2;
-    cursor: default;
-}
-.x-cur-version {
-    font-size: 12px;
-    margin-top: 4px;
-}
-.x-cur-version b,.x-cur-version i {
-    background: #6c757d;
-    padding: 2px 6px;
-    border-radius: 3px 0 0 3px;
-    color: #fff;
-    font-weight: normal;
-    font-style: normal;
-    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
-    line-height: 1.5;
-}
-.x-cur-version i {
-    border-radius: 0 3px 3px 0;
-    background: #007bff;
-}
-[v-cloak] {
-    display: none;
-}
-.x-count-down {
-    color:#f00;
-    font-size: 12px;
-}
-.x-count-down b {
-    color:#0b0;
-}
-.panel-low-version {
-    text-align: center;
-    padding-top: 200px;
-    color: #bbb;
-}
-.panel-low-version a {
-    text-decoration: underline;
-}
-.panel-low-version b {
-    color:#888;
-    font-weight: normal;
-}
+body{
+    margin: 0;
+}
+.market-container {
+    padding: 20px;
+    margin-top: 10px;
+    background: #fff;
+    border-radius: 4px;
+    box-shadow: 0 1px 3px rgba(0,0,0,0.1);
+}
+
+/* 顶部搜索和筛选区 */
+.market-header {
+    background-color: #f8f9fa;
+    padding: 1rem;
+    border-radius: 4px;
+    margin-bottom: 1.5rem;
+    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+    position: sticky;
+    top: 0;
+    z-index: 100;
+    transition: box-shadow 0.3s ease;
+}
+
+/* 滚动时的样式增强 */
+.market-header.scrolled {
+    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
+}
+
+.search-filters-container {
+    display: flex;
+    align-items: center;
+    flex-wrap: wrap;
+    gap: 1rem;
+}
+
+.search-box {
+    position: relative;
+    flex: 1;
+    min-width: 200px;
+}
+
+.search-icon {
+    position: absolute;
+    left: 10px;
+    top: 50%;
+    transform: translateY(-50%);
+    color: #6c757d;
+    font-size: 16px;
+}
+
+.search-box input {
+    width: 100%;
+    padding: 8px 15px 8px 35px;
+    border: 1px solid #ced4da;
+    border-radius: 4px;
+    font-size: 16px;
+    transition: all 0.3s;
+}
+
+.search-box input:focus {
+    border-color: #4ca1af;
+    box-shadow: 0 0 0 0.2rem rgba(76, 161, 175, 0.25);
+    outline: none;
+}
+
+.filter-group {
+    position: relative;
+    min-width: 150px;
+}
+
+.filter-label {
+    position: absolute;
+    top: -22px;
+    left: 0;
+    font-size: 14px;
+    color: #6c757d;
+    font-weight: 500;
+}
+
+.filter-select {
+    width: 100%;
+    padding: 8px 10px;
+    border: 1px solid #ced4da;
+    border-radius: 4px;
+    font-size: 14px;
+    background-color: white;
+    cursor: pointer;
+    transition: all 0.3s;
+}
+
+.filter-select:focus {
+    border-color: #4ca1af;
+    box-shadow: 0 0 0 0.2rem rgba(76, 161, 175, 0.25);
+    outline: none;
+}
+
+.view-toggle {
+    display: flex;
+    border: 1px solid #ced4da;
+    border-radius: 4px;
+    overflow: hidden;
+}
+
+.view-btn {
+    padding: 8px 15px;
+    font-size: 14px;
+    background-color: white;
+    cursor: pointer;
+    transition: all 0.3s;
+    color: #6c757d;
+}
+
+.view-btn:hover {
+    background-color: #f1f1f1;
+}
+
+.view-btn.active {
+    background-color: #4ca1af;
+    color: white;
+}
+
+/* 主体内容区 */
+.market-content {
+    display: flex;
+    gap: 20px;
+    margin-top: 20px;
+    padding-top: 10px;
+    position: relative;
+}
+
+/* 侧边栏样式 */
+.market-sidebar {
+    width: 200px;
+    flex-shrink: 0;
+    position: sticky;
+    top: 80px; /* 与顶部header保持一定距离 */
+    max-height: calc(100vh - 100px); /* 设置最大高度,确保不会超出视窗 */
+    overflow-y: auto; /* 内容过多时可滚动 */
+    scrollbar-width: thin;
+    scrollbar-color: #d0d0d0 #f4f4f4;
+    transition: all 0.3s ease;
+}
+
+/* 侧边栏滚动条样式 */
+.market-sidebar::-webkit-scrollbar {
+    width: 6px;
+}
+
+.market-sidebar::-webkit-scrollbar-track {
+    background: #f4f4f4;
+    border-radius: 3px;
+}
+
+.market-sidebar::-webkit-scrollbar-thumb {
+    background-color: #d0d0d0;
+    border-radius: 3px;
+    border: 1px solid #f4f4f4;
+}
+
+.market-sidebar::-webkit-scrollbar-thumb:hover {
+    background-color: #c0c0c0;
+}
+
+/* 暗黑模式下的侧边栏滚动条 */
+[data-theme='dark'] .market-sidebar {
+    scrollbar-color: #555 #333;
+}
+
+[data-theme='dark'] .market-sidebar::-webkit-scrollbar-track {
+    background: #2a2a2a;
+}
+
+[data-theme='dark'] .market-sidebar::-webkit-scrollbar-thumb {
+    background-color: #555;
+    border: 1px solid #2a2a2a;
+}
+
+[data-theme='dark'] .market-sidebar::-webkit-scrollbar-thumb:hover {
+    background-color: #666;
+}
+
+/* 侧边栏section样式优化 */
+.sidebar-section {
+    background: #fff;
+    border-radius: 4px;
+    padding: 15px;
+    margin-bottom: 20px;
+    box-shadow: 0 1px 3px rgba(0,0,0,0.1);
+    transition: all 0.3s ease;
+}
+
+/* 滚动时的样式增强 */
+.market-sidebar.scrolled .sidebar-section {
+    box-shadow: 0 2px 6px rgba(0,0,0,0.15);
+}
+
+.sidebar-section h4 {
+    margin: 0 0 12px;
+    padding-bottom: 12px;
+    border-bottom: 1px solid #eee;
+    font-size: 18px;
+    color: #333;
+}
+
+.category-list li,
+.my-tools li {
+    padding: 8px 10px;
+    margin: 3px 0;
+    cursor: pointer;
+    border-radius: 4px;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    font-size: 14px;
+}
+
+.category-list li:hover,
+.my-tools li:hover {
+    background: #f0f0f0;
+}
+
+.category-list li.active,
+.my-tools li.active {
+    background: #e6f7ff;
+    color: #1890ff;
+}
+
+.count {
+    font-size: 14px;
+    color: #999;
+}
+
+/* 工具展示区 */
+.tools-grid {
+    flex: 1;
+}
+
+/* 网格视图 */
+.tools-container.grid {
+    display: grid;
+    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+    gap: 20px;
+}
+
+.tool-card {
+    background: #fff;
+    border-radius: 4px;
+    padding: 15px;
+    box-shadow: 0 1px 3px rgba(0,0,0,0.1);
+    display: flex;
+    flex-direction: column;
+    gap: 10px;
+    transition: transform 0.2s;
+}
+
+.tool-card:hover {
+    transform: translateY(-2px);
+    box-shadow: 0 4px 6px rgba(0,0,0,0.1);
+}
+
+.tool-icon {
+    font-size: 32px;
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+    width: 24px;
+    height: 24px;
+    border-radius: 12px;
+    margin-right: 12px;
+    background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
+    color: #2d3436;
+    transition: all 0.3s ease;
+}
+
+.tool-card:hover .tool-icon {
+    transform: scale(1.1);
+    box-shadow: 0 4px 8px rgba(0,0,0,0.1);
+}
+
+/* 为不同类型的工具设置不同的背景色 */
+.tool-card[data-category="dev"] .tool-icon {
+    background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
+}
+
+.tool-card[data-category="encode"] .tool-icon {
+    background: linear-gradient(135deg, #d299c2 0%, #fef9d7 100%);
+}
+
+.tool-card[data-category="image"] .tool-icon {
+    background: linear-gradient(135deg, #89f7fe 0%, #66a6ff 100%);
+}
+
+.tool-card[data-category="productivity"] .tool-icon {
+    background: linear-gradient(135deg, #f6d365 0%, #fda085 100%);
+}
+
+.tool-card[data-category="calculator"] .tool-icon {
+    background: linear-gradient(135deg, #84fab0 0%, #8fd3f4 100%);
+}
+
+.tool-info {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+}
+
+.tool-info h3 {
+    margin: 0 0 10px;
+    font-size: 18px;
+    color: #333;
+    display: flex;
+    align-items: center;
+    gap: 10px;
+}
+
+.tool-desc {
+    margin: 0;
+    font-size: 14px;
+    color: #666;
+    line-height: 1.5;
+}
+
+.tool-meta {
+    display: flex;
+    gap: 15px;
+    margin-top: 10px;
+    font-size: 12px;
+    color: #999;
+}
+
+.tool-actions {
+    display: flex;
+    gap: 8px;
+    margin-top: 10px;
+}
+
+/* 列表视图 */
+.tools-container.list .tool-card {
+    flex-direction: row;
+    align-items: center;
+    margin-bottom: 10px;
+}
+
+.tools-container.list .tool-info {
+    margin: 0 15px;
+}
+
+/* 调整列表视图中图标的大小 */
+.tools-container.list .tool-icon {
+    width: 40px;
+    height: 40px;
+    font-size: 28px;
+    margin-right: 16px;
+}
+
+/* 按钮样式 */
+.btn {
+    padding: 8px 15px;
+    border: none;
+    border-radius: 4px;
+    cursor: pointer;
+    font-size: 14px;
+    transition: all 0.3s;
+}
+
+.btn-xs {
+    padding: 4px 8px;
+    font-size: 13px;
+    line-height: 1.5;
+    border-radius: 3px;
+}
+
+.btn:hover {
+    opacity: 0.9;
+}
+
+.btn:disabled {
+    opacity: 0.6;
+    cursor: not-allowed;
+}
+
+.x-progress {
+    font-size: 13px;
+    margin-left: 3px;
+}
+
+/* 为系统预装的工具添加提示样式 */
+.btn-disabled {
+    background-color: #e0e0e0;
+    color: #999;
+    cursor: not-allowed;
+}
+
+.btn-disabled:hover {
+    opacity: 1;
+}
+
+.btn-success {
+    background: #52c41a;
+    color: #fff;
+}
+
+.btn-danger {
+    background: #ff4d4f;
+    color: #fff;
+}
+
+.btn-info {
+    background: #1890ff;
+    color: #fff;
+}
+
+.btn-warning {
+    background: #faad14;
+    color: #fff;
+}
+
+.btn-default {
+    background: #f0f0f0;
+    color: #666;
+}
+
+.btn-default.active {
+    background-color: #ff4757;
+    color: white;
+    border-color: #ff4757;
+}
+
+.btn-default.active:hover {
+    background-color: #ff6b81;
+    border-color: #ff6b81;
+}
+
+.favorite-icon {
+    color: #ff4757;
+    margin-right: 4px;
+    transition: all 0.3s ease;
+}
+
+.btn-default:not(.active) .favorite-icon {
+    color: #666;
+}
+
+.btn-default.active .favorite-icon {
+    transform: scale(1.2);
+    color: white;
+}
+
+/* 暗黑模式适配 */
+[data-theme='dark'] .market-container {
+    background: #1f1f1f;
+}
+
+[data-theme='dark'] .market-header {
+    background-color: #2a2a2a;
+}
+
+[data-theme='dark'] .sidebar-section,
+[data-theme='dark'] .tool-card {
+    background: #2d2d2d;
+    box-shadow: 0 1px 3px rgba(0,0,0,0.3);
+}
+
+[data-theme='dark'] .tool-info h3 {
+    color: #fff;
+}
+
+[data-theme='dark'] .tool-desc {
+    color: #bbb;
+}
+
+[data-theme='dark'] .category-list li:hover,
+[data-theme='dark'] .my-tools li:hover {
+    background: #3d3d3d;
+}
+
+[data-theme='dark'] .category-list li.active {
+    background: #177ddc;
+    color: #fff;
+}
+
+[data-theme='dark'] .my-tools li.active {
+    background: #177ddc;
+    color: #fff;
+}
+
+[data-theme='dark'] .view-toggle span {
+    background: #3d3d3d;
+    color: #bbb;
+}
+
+[data-theme='dark'] .view-toggle span.active {
+    background: #177ddc;
+    color: #fff;
+}
+
+[data-theme='dark'] .btn-default {
+    background: #3d3d3d;
+    color: #bbb;
+}
+
+/* 暗黑模式下的图标样式调整 */
+[data-theme='dark'] .tool-icon {
+    color: #ffffff;
+    opacity: 0.9;
+}
+
+/* 推荐位和广告位样式 */
+.recommendation-container {
+    margin-bottom: 20px;
+}
+
+.recommendation-section {
+    display: grid;
+    grid-template-columns: repeat(4, 1fr);
+    gap: 15px;
+}
+
+.recommendation-card {
+    display: flex;
+    align-items: center;
+    padding: 15px;
+    background: #fff;
+    border-radius: 6px;
+    box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+    cursor: pointer;
+    transition: all 0.3s ease;
+    position: relative;
+}
+
+.recommendation-card:hover {
+    transform: translateY(-3px);
+    box-shadow: 0 6px 12px rgba(0,0,0,0.15);
+}
+
+.rec-icon {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 60px;
+    height: 60px;
+    border-radius: 12px;
+    background: linear-gradient(135deg, #f6d365 0%, #fda085 100%);
+    font-size: 28px;
+    margin-right: 15px;
+    flex-shrink: 0;
+}
+
+.recommendation-card:nth-child(1) .rec-icon {
+    background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
+}
+
+.recommendation-card:nth-child(2) .rec-icon {
+    background: linear-gradient(135deg, #d299c2 0%, #fef9d7 100%);
+}
+
+.recommendation-card:nth-child(3) .rec-icon {
+    background: linear-gradient(135deg, #89f7fe 0%, #66a6ff 100%);
+}
+
+.rec-content {
+    flex: 1;
+}
+
+.rec-content h4 {
+    margin: 0 0 8px;
+    font-size: 18px;
+    color: #333;
+}
+
+.rec-content p {
+    margin: 0;
+    font-size: 14px;
+    color: #666;
+    line-height: 1.4;
+}
+
+/* 广告卡片特殊样式 */
+.ad-card {
+    position: relative;
+    border: 1px solid #FFD700;
+    background: linear-gradient(to right, #fff, #fffdf0);
+}
+
+.ad-icon {
+    background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
+}
+
+.ad-tag {
+    position: absolute;
+    top: 8px;
+    right: 8px;
+    background: rgba(255, 215, 0, 0.2);
+    border: 1px solid #FFD700;
+    border-radius: 4px;
+    padding: 3px 8px;
+    font-size: 12px;
+    color: #997a00;
+}
+
+/* 必装标签样式 */
+.must-tag {
+    position: absolute;
+    top: 8px;
+    right: 8px;
+    background: rgba(82, 196, 26, 0.2);
+    border: 1px solid #52c41a;
+    border-radius: 4px;
+    padding: 3px 8px;
+    font-size: 12px;
+    color: #52c41a;
+}
+
+/* 最新标签样式 */
+.new-tag {
+    position: absolute;
+    top: 8px;
+    right: 8px;
+    background: rgba(24, 144, 255, 0.2);
+    border: 1px solid #1890ff;
+    border-radius: 4px;
+    padding: 3px 8px;
+    font-size: 12px;
+    color: #1890ff;
+}
+
+/* 推荐标签样式 */
+.recommend-tag {
+    position: absolute;
+    top: 8px;
+    right: 8px;
+    background: rgba(250, 84, 28, 0.2);
+    border: 1px solid #fa541c;
+    border-radius: 4px;
+    padding: 3px 8px;
+    font-size: 12px;
+    color: #fa541c;
+}
+
+/* 暗黑模式适配 */
+[data-theme='dark'] .recommendation-card {
+    background: #2d2d2d;
+    box-shadow: 0 2px 8px rgba(0,0,0,0.3);
+}
+
+[data-theme='dark'] .rec-content h4 {
+    color: #fff;
+}
+
+[data-theme='dark'] .rec-content p {
+    color: #bbb;
+}
+
+[data-theme='dark'] .ad-card {
+    background: linear-gradient(to right, #2d2d2d, #332e1f);
+    border-color: #997a00;
+}
+
+[data-theme='dark'] .ad-tag {
+    background: rgba(255, 215, 0, 0.15);
+    color: #FFD700;
+}
+
+/* 暗黑模式下的必装标签 */
+[data-theme='dark'] .must-tag {
+    background: rgba(82, 196, 26, 0.15);
+    color: #73d13d;
+    border-color: #52c41a;
+}
+
+/* 暗黑模式下的最新标签 */
+[data-theme='dark'] .new-tag {
+    background: rgba(24, 144, 255, 0.15);
+    color: #40a9ff;
+    border-color: #1890ff;
+}
+
+/* 暗黑模式下的推荐标签 */
+[data-theme='dark'] .recommend-tag {
+    background: rgba(250, 84, 28, 0.15);
+    color: #ff7a45;
+    border-color: #fa541c;
+}
+
+/* 轮播Banner区域 */
+
+/* 导航链接样式 */
+.x-nav-links {
+    margin-right: 15px;
+}
+
+.x-nav-links a {
+    color: #1890ff;
+    text-decoration: none;
+    margin-left: 15px;
+    font-size: 14px;
+}
+
+.x-nav-links a:hover {
+    color: #40a9ff;
+    text-decoration: underline;
+}
+
+/* 设置弹窗样式 */
+.settings-modal {
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    background-color: rgba(0, 0, 0, 0.5);
+    z-index: 1000;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    backdrop-filter: blur(2px);
+}
+
+.settings-dialog {
+    background-color: #fff;
+    border-radius: 8px;
+    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
+    width: 80%;
+    max-width: 800px;
+    max-height: 90vh;
+    display: flex;
+    flex-direction: column;
+    animation: fadeIn 0.3s ease;
+    overflow: hidden;
+}
+
+@keyframes fadeIn {
+    from { opacity: 0; transform: translateY(-20px); }
+    to { opacity: 1; transform: translateY(0); }
+}
+
+.settings-header {
+    padding: 16px 20px;
+    border-bottom: 1px solid #e8e8e8;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    position: sticky;
+    top: 0;
+    background-color: #fff;
+    z-index: 10;
+    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
+    flex-shrink: 0;
+}
+
+.settings-header h3 {
+    margin: 0;
+    color: #333;
+    font-size: 20px;
+    font-weight: 600;
+}
+
+.close-btn {
+    font-size: 28px;
+    color: #999;
+    cursor: pointer;
+    transition: all 0.3s;
+    width: 32px;
+    height: 32px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    border-radius: 50%;
+}
+
+.close-btn:hover {
+    color: #666;
+    background-color: #f0f0f0;
+}
+
+.settings-body {
+    padding: 0 20px;
+    overflow-y: auto;
+    flex: 1;
+    scrollbar-width: thin;
+    scrollbar-color: #d0d0d0 #f4f4f4;
+}
+
+.settings-body label {
+    font-weight: normal !important;
+    display: inline-flex;
+    align-items: center;
+}
+
+.settings-body label b,
+.settings-body label i,
+.settings-body label strong {
+    font-weight: bold;
+}
+
+.settings-body::-webkit-scrollbar {
+    width: 8px;
+}
+
+.settings-body::-webkit-scrollbar-track {
+    background: #f4f4f4;
+    border-radius: 4px;
+}
+
+.settings-body::-webkit-scrollbar-thumb {
+    background-color: #d0d0d0;
+    border-radius: 4px;
+    border: 2px solid #f4f4f4;
+}
+
+.settings-body::-webkit-scrollbar-thumb:hover {
+    background-color: #c0c0c0;
+}
+
+.settings-footer {
+    padding: 16px 20px;
+    border-top: 1px solid #e8e8e8;
+    display: flex;
+    justify-content: flex-end;
+    gap: 12px;
+    position: sticky;
+    bottom: 0;
+    background-color: #fff;
+    z-index: 10;
+    box-shadow: 0 -1px 4px rgba(0, 0, 0, 0.05);
+    flex-shrink: 0;
+}
+
+.settings-footer .btn {
+    padding: 8px 20px;
+    border-radius: 4px;
+    font-size: 14px;
+    font-weight: 500;
+    transition: all 0.3s ease;
+    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+}
+
+.settings-footer .btn-success {
+    background: linear-gradient(135deg, #52c41a, #39a012);
+}
+
+.settings-footer .btn-success:hover {
+    background: linear-gradient(135deg, #5bd81e, #43b515);
+    box-shadow: 0 4px 8px rgba(0,0,0,0.15);
+    transform: translateY(-1px);
+}
+
+.settings-footer .btn-danger {
+    background: linear-gradient(135deg, #ff4d4f, #e02b2e);
+}
+
+.settings-footer .btn-danger:hover {
+    background: linear-gradient(135deg, #ff6769, #ed3437);
+    box-shadow: 0 4px 8px rgba(0,0,0,0.15);
+    transform: translateY(-1px);
+}
+
+.dark-mode .settings-footer {
+    border-top-color: #333;
+    background-color: #1f1f1f;
+    box-shadow: 0 -1px 4px rgba(0, 0, 0, 0.2);
+}
+
+.dark-mode .settings-footer .btn {
+    box-shadow: 0 2px 6px rgba(0,0,0,0.2);
+}
+
+.dark-mode .settings-footer .btn-success {
+    background: linear-gradient(135deg, #49b616, #338c0f);
+}
+
+.dark-mode .settings-footer .btn-success:hover {
+    background: linear-gradient(135deg, #52c41a, #39a012);
+}
+
+.dark-mode .settings-footer .btn-danger {
+    background: linear-gradient(135deg, #e63e40, #cc2729);
+}
+
+.dark-mode .settings-footer .btn-danger:hover {
+    background: linear-gradient(135deg, #ff4d4f, #e02b2e);
+}
+
+.a-btn {
+    display: inline-block;
+    background-color: #1890ff;
+    color: white;
+    padding: 2px 8px;
+    border-radius: 3px;
+    margin-left: 10px;
+    font-size: 12px;
+    text-decoration: none;
+}
+
+.a-btn:hover {
+    background-color: #40a9ff;
+    text-decoration: none;
+}
+
+.x-count-down {
+    margin-left: 10px;
+    font-size: 12px;
+    color: #ff4d4f;
+}
+
+.x-tips {
+    color: #666;
+    font-size: 14px;
+    margin-left: 5px;
+}
+
+/* 暗黑模式样式适配 */
+.dark-mode .settings-modal {
+    background-color: rgba(0, 0, 0, 0.7);
+}
+
+.dark-mode .settings-dialog {
+    background-color: #1f1f1f;
+    color: #eee;
+    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
+}
+
+.dark-mode .settings-header {
+    border-bottom-color: #333;
+    background-color: #1f1f1f;
+    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
+}
+
+.dark-mode .settings-header h3 {
+    color: #eee;
+}
+
+.dark-mode .close-btn {
+    color: #aaa;
+}
+
+.dark-mode .close-btn:hover {
+    color: #eee;
+    background-color: #333;
+}
+
+.dark-mode .settings-section h4 {
+    color: #eee;
+}
+
+.dark-mode .box-config {
+    background-color: #2a2a2a;
+}
+
+.dark-mode .box-inner {
+    background-color: #333;
+}
+
+/* 导航栏样式 */
+.main-navbar {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 8px 15px; /* 减少垂直内边距 */
+    background: linear-gradient(to right, #2c3e50, #4ca1af);
+    color: #fff;
+    position: relative;
+    min-height: 48px; /* 限制最小高度 */
+    max-height: 60px; /* 限制最大高度 */
+}
+
+.navbar-brand {
+    display: flex;
+    align-items: center;
+}
+
+.brand-link {
+    display: flex;
+    align-items: center;
+    color: #fff;
+    text-decoration: none;
+    font-weight: 500;
+    font-size: 16px;
+    transition: all 0.3s ease;
+}
+
+.brand-link:hover {
+    opacity: 0.9;
+    text-decoration: none;
+    color: #fff;
+}
+
+.brand-link img {
+    height: 20px;
+    width: 20px;
+    margin-right: 6px;
+}
+
+.brand-text {
+    font-weight: 600;
+    font-size: 16px;
+}
+
+.brand-subtitle {
+    margin-left: 6px;
+    font-size: 14px;
+    opacity: 0.9;
+    font-weight: normal;
+    position: relative;
+    padding-left: 8px;
+}
+
+.brand-subtitle::before {
+    content: '';
+    position: absolute;
+    left: 0;
+    top: 50%;
+    transform: translateY(-50%);
+    height: 70%;
+    width: 1px;
+    background-color: rgba(255, 255, 255, 0.5);
+}
+
+.navbar-actions {
+    display: flex;
+    align-items: center;
+    gap: 15px;
+}
+
+/* 版本信息区域 - 统一样式 */
+.version-info {
+    display: flex;
+    align-items: center;
+    font-size: 12px;
+    color: rgba(255, 255, 255, 0.9);
+    background: rgba(0, 0, 0, 0.15);
+    padding: 6px 12px;
+    border-radius: 6px;
+    gap: 8px;
+    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+    max-height: 36px; /* 限制最大高度,保持导航栏紧凑 */
+    overflow: hidden;
+}
+
+.nav-item {
+    position: relative;
+    transition: all 0.3s ease;
+    overflow: hidden;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    color: #fff;
+    text-decoration: none;
+    padding: 6px 12px; /* 减少内边距 */
+    border-radius: 5px;
+    font-size: 13px; /* 稍微减小字体 */
+    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
+    min-width: 90px; /* 减少最小宽度 */
+    text-align: center;
+    height: 32px; /* 固定高度 */
+    box-sizing: border-box;
+}
+
+.nav-item:hover {
+    background-color: rgba(255, 255, 255, 0.1);
+    transform: translateY(-2px);
+    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
+    color: #fff;
+    text-decoration: none;
+}
+
+.nav-item:active {
+    transform: translateY(0);
+    box-shadow: 0 2px 3px rgba(0, 0, 0, 0.1);
+}
+
+.nav-icon {
+    display: inline-block;
+    transition: all 0.3s ease;
+    margin-right: 6px;
+    font-size: 16px;
+}
+
+.nav-item:hover .nav-icon {
+    transform: scale(1.2) rotate(10deg);
+}
+
+/* 波纹效果 */
+.nav-item::after {
+    content: '';
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    width: 5px;
+    height: 5px;
+    background: rgba(255, 255, 255, 0.3);
+    opacity: 0;
+    border-radius: 100%;
+    transform: scale(1, 1) translate(-50%);
+    transform-origin: 50% 50%;
+}
+
+.nav-item:hover::after {
+    animation: ripple 0.6s ease-out;
+}
+
+/* 打赏鼓励按钮特殊效果 */
+.navbar-actions .donate-link {
+    background: linear-gradient(135deg, #ff9a9e 0%, #fad0c4 99%, #fad0c4 100%);
+    color: #333;
+    position: relative;
+    overflow: hidden;
+}
+
+.navbar-actions .donate-link:hover {
+    background: linear-gradient(45deg, #ff7675, #d63031);
+    transform: translateY(-3px);
+    box-shadow: 0 5px 15px rgba(255, 105, 97, 0.4);
+    color: #fff;
+}
+
+.navbar-actions .donate-link .nav-icon {
+    color: #e74c3c;
+    font-size: 18px;
+}
+
+.navbar-actions .donate-link:hover .nav-icon {
+    animation: heartbeat 1.2s infinite;
+    color: #fff;
+}
+
+/* 设置按钮特殊效果 */
+.navbar-actions .settings-link {
+    position: relative;
+    overflow: hidden;
+    background-color: rgba(255, 255, 255, 0.15);
+}
+
+.navbar-actions .settings-link:hover {
+    background: linear-gradient(45deg, #74b9ff, #0984e3);
+}
+
+.navbar-actions .settings-link .nav-icon {
+    transition: transform 0.5s ease;
+}
+
+.navbar-actions .settings-link:hover .nav-icon {
+    animation: rotate 2s linear infinite;
+}
+
+/* 动画关键帧 */
+@keyframes ripple {
+    0% {
+        transform: scale(0, 0);
+        opacity: 1;
+    }
+    20% {
+        transform: scale(25, 25);
+        opacity: 1;
+    }
+    100% {
+        opacity: 0;
+        transform: scale(40, 40);
+    }
+}
+
+@keyframes heartbeat {
+    0% { transform: scale(1); }
+    25% { transform: scale(1.3); }
+    50% { transform: scale(1); }
+    75% { transform: scale(1.3); }
+    100% { transform: scale(1); }
+}
+
+@keyframes rotate {
+    0% { transform: rotate(0deg); }
+    100% { transform: rotate(360deg); }
+}
+
+/* 响应式调整 */
+@media (max-width: 768px) {
+    .navbar-actions {
+        gap: 10px;
+    }
+    
+    .nav-item {
+        min-width: auto;
+        padding: 8px 12px;
+    }
+    
+    .version-info {
+        padding: 4px 8px;
+        font-size: 10px;
+        gap: 4px;
+        max-height: 28px;
+    }
+    
+    .version-details {
+        gap: 3px;
+    }
+    
+    .current-version,
+    .latest-version-text {
+        font-size: 9px;
+        padding: 1px 3px;
+    }
+    
+    .update-btn {
+        font-size: 9px;
+        padding: 2px 6px;
+        height: 16px;
+    }
+    
+    /* 搜索和筛选区域响应式 */
+    .search-filters-container {
+        flex-direction: column;
+        align-items: stretch;
+    }
+    
+    .filter-group {
+        width: 100%;
+    }
+    
+    .view-toggle {
+        margin-top: 1rem;
+        width: 100%;
+    }
+    
+    .view-btn {
+        flex: 1;
+        text-align: center;
+    }
+    
+    .market-content {
+        flex-direction: column;
+    }
+    
+    .market-sidebar {
+        width: 100%;
+        position: relative;
+        top: 0;
+        max-height: none;
+        margin-bottom: 20px;
+    }
+}
+
+@media (max-width: 600px) {
+    .brand-subtitle {
+        display: none;
+    }
+    
+    .nav-item {
+        padding: 6px 10px;
+    }
+    
+    .nav-item span {
+        display: none;
+    }
+    
+    .nav-icon {
+        margin-right: 0;
+        font-size: 18px;
+    }
+    
+    .version-info {
+        padding: 3px 6px;
+        font-size: 10px;
+        gap: 4px;
+        max-height: 24px;
+    }
+    
+    .version-latest {
+        font-size: 10px;
+        gap: 3px;
+    }
+    
+    .version-number {
+        font-size: 9px;
+        padding: 1px 2px;
+    }
+    
+    .version-details {
+        gap: 3px;
+    }
+    
+    .current-version,
+    .latest-version-text {
+        font-size: 9px;
+        padding: 1px 3px;
+    }
+    
+    .update-btn {
+        font-size: 9px;
+        padding: 2px 6px;
+        height: 16px;
+    }
+    
+    .donate-link span,
+    .settings-link span {
+        display: none;
+    }
+    
+    .donate-link .nav-icon,
+    .settings-link .nav-icon {
+        margin-right: 0;
+    }
+}
+
+/* 暗黑模式下搜索和筛选区样式 */
+.dark-mode .search-box input,
+.dark-mode .filter-select {
+    background-color: #333;
+    border-color: #444;
+    color: #eee;
+}
+
+.dark-mode .filter-label {
+    color: #aaa;
+}
+
+.dark-mode .view-toggle {
+    border-color: #444;
+}
+
+.dark-mode .view-btn {
+    background-color: #333;
+    color: #aaa;
+}
+
+.dark-mode .view-btn.active {
+    background-color: #4ca1af;
+    color: white;
+}
+
+/* 版本号与更新提示 - 优化为水平紧凑布局 */
+.version-details {
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    flex-wrap: wrap; /* 在必要时换行 */
+}
+
+.version-label {
+    font-size: 10px;
+    color: rgba(255,255,255,0.7);
+    white-space: nowrap; /* 防止换行 */
+    margin: 0; /* 移除边距 */
+}
+
+.current-version {
+    font-size: 11px;
+    font-weight: 500;
+    color: #fff;
+    padding: 1px 4px;
+    background-color: rgba(24, 144, 255, 0.5);
+    border-radius: 3px;
+    letter-spacing: 0.2px;
+    white-space: nowrap;
+}
+
+.latest-version-text {
+    font-size: 11px;
+    font-weight: 500;
+    color: #fff;
+    padding: 1px 4px;
+    background-color: rgba(82, 196, 26, 0.5);
+    border-radius: 3px;
+    letter-spacing: 0.2px;
+    white-space: nowrap;
+}
+
+.latest-version-container {
+    height: 18px;
+    display: flex;
+    align-items: center;
+}
+
+.latest-version-img {
+    height: 18px;
+    transition: transform 0.2s ease;
+    cursor: pointer;
+}
+
+.latest-version-img:hover {
+    transform: scale(1.05);
+}
+
+.update-btn {
+    background-color: #ff6b6b;
+    color: white;
+    border: none;
+    padding: 3px 8px;
+    border-radius: 3px;
+    font-size: 10px;
+    font-weight: 500;
+    cursor: pointer;
+    box-shadow: 0 1px 2px rgba(0,0,0,0.2);
+    animation: pulse 1.5s infinite;
+    transition: background-color 0.3s;
+    white-space: nowrap;
+    height: 20px; /* 固定高度 */
+    display: flex;
+    align-items: center;
+}
+
+.update-btn:hover {
+    background-color: #ff5252;
+}
+
+@keyframes pulse {
+    0% {
+        box-shadow: 0 0 0 0 rgba(255, 107, 107, 0.7);
+    }
+    70% {
+        box-shadow: 0 0 0 6px rgba(255, 107, 107, 0);
+    }
+    100% {
+        box-shadow: 0 0 0 0 rgba(255, 107, 107, 0);
+    }
+}
+
+.dark-mode .current-version {
+    background-color: rgba(24, 144, 255, 0.5);
+    color: #fff;
+}
+
+.dark-mode .latest-version-text {
+    background-color: rgba(82, 196, 26, 0.5);
+    color: #fff;
+}
+
+.dark-mode .version-label {
+    color: #aaa;
+}
+
+.dark-mode .update-btn {
+    background-color: #ff7676;
+}
+
+.dark-mode .update-btn:hover {
+    background-color: #ff5e5e;
+}
+
+.version-latest {
+    display: flex;
+    align-items: center;
+    font-size: 12px;
+    font-weight: 500;
+    color: #fff;
+    gap: 6px;
+    white-space: nowrap;
+}
+
+.version-check-icon {
+    color: #52c41a;
+    font-size: 14px;
+    text-shadow: 0 0 5px rgba(82, 196, 26, 0.5);
+    flex-shrink: 0;
+}
+
+.dark-mode .version-latest {
+    color: #f0f0f0;
+}
+
+.dark-mode .version-check-icon {
+    color: #73d13d;
+}
+
+.version-number {
+    background-color: rgba(0, 0, 0, 0.2);
+    border-radius: 3px;
+    padding: 1px 4px;
+    margin-left: 2px;
+    font-weight: 400;
+    color: #fff;
+    font-size: 11px;
+    white-space: nowrap;
+}
+
+.dark-mode .version-number {
+    background-color: rgba(255, 255, 255, 0.15);
+    color: #fff;
+}
+
+/* 暗黑模式滚动条 */
+.dark-mode .settings-body {
+    scrollbar-color: #555 #333;
+}
+
+.dark-mode .settings-body::-webkit-scrollbar-track {
+    background: #2a2a2a;
+}
+
+.dark-mode .settings-body::-webkit-scrollbar-thumb {
+    background-color: #555;
+    border: 2px solid #2a2a2a;
+}
+
+.dark-mode .settings-body::-webkit-scrollbar-thumb:hover {
+    background-color: #666;
+}
+
+.dark-mode .settings-section {
+    border-bottom-color: #333;
+}
+
+.settings-section {
+    margin-bottom: 25px;
+    padding-bottom: 15px;
+    border-bottom: 1px dashed #eee;
+}
+
+.settings-section:last-child {
+    margin-bottom: 0;
+    padding-bottom: 0;
+    border-bottom: none;
+}
+
+.settings-section h4 {
+    margin-top: 0;
+    margin-bottom: 15px;
+    font-size: 18px;
+    font-weight: 600;
+    color: #333;
+}
+
+.box-config {
+    background-color: #f9f9f9;
+    border-radius: 4px;
+    padding: 15px;
+}
+
+.st-item {
+    margin-bottom: 15px;
+}
+
+.box-inner {
+    margin-top: 10px;
+    background-color: #f0f0f0;
+    padding: 10px;
+    border-radius: 4px;
+}
+
+.row-line {
+    margin-bottom: 8px;
+}
+
+.x-disabled {
+    opacity: 0.6;
+    pointer-events: none;
+}
+
+/* 设置打赏按钮样式 */
+.donate-link:hover {
+    transform: translateY(-3px);
+    box-shadow: 0 5px 15px rgba(255, 105, 97, 0.4);
+    background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 99%, #fecfef 100%);
+}
+
+.donate-link:active {
+    transform: translateY(0);
+}
+
+.donate-link .nav-icon {
+    color: #e74c3c;
+    font-size: 18px;
+    animation: heartbeat 1.3s ease-in-out infinite;
+}
+
+.donate-link:hover .nav-icon {
+    animation: heartbeat 0.6s ease-in-out infinite;
+}
+
+@keyframes heartbeat {
+    0% {
+        transform: scale(1);
+    }
+    14% {
+        transform: scale(1.3);
+    }
+    28% {
+        transform: scale(1);
+    }
+    42% {
+        transform: scale(1.3);
+    }
+    70% {
+        transform: scale(1);
+    }
+}
+
+/* 打赏模态框样式 */
+.donate-modal .settings-dialog {
+    max-width: 500px;
+}
+
+.donate-content {
+    text-align: center;
+    padding: 10px;
+}
+
+.donate-desc {
+    font-size: 16px;
+    margin-bottom: 20px;
+    color: #666;
+    line-height: 1.6;
+}
+
+.donate-qrcode {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    margin-bottom: 20px;
+}
+
+.donate-image {
+    max-width: 250px;
+    border-radius: 10px;
+    box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
+    margin-bottom: 15px;
+    transition: transform 0.3s ease;
+}
+
+.donate-image:hover {
+    transform: scale(1.05);
+}
+
+.donate-text {
+    font-size: 18px;
+    font-weight: bold;
+    color: #e74c3c;
+    margin-top: 10px;
+}
+
+/* 暗黑模式下的打赏样式 */
+.dark-mode .donate-link {
+    background: linear-gradient(135deg, #ff6b6b 0%, #f39c12 100%);
+    color: #fff;
+}
+
+.dark-mode .donate-link:hover {
+    background: linear-gradient(135deg, #ff6b6b 0%, #e74c3c 100%);
+}
+
+.dark-mode .donate-link .nav-icon {
+    color: #fff;
+}
+
+.dark-mode .donate-desc {
+    color: #ccc;
+}
+
+.dark-mode .donate-text {
+    color: #ff6b6b;
+}
+
+/* 媒体查询-移动设备 */
+@media (max-width: 600px) {
+    .donate-link span {
+        display: none;
+    }
+    
+    .donate-image {
+        max-width: 180px;
+    }
+}
+
+/* 确认对话框样式 */
+.confirm-modal .settings-dialog {
+    max-width: 450px;
+}
+
+.confirm-content {
+    display: flex;
+    align-items: center;
+    padding: 20px 10px;
+}
+
+.confirm-icon {
+    font-size: 36px;
+    color: #f39c12;
+    margin-right: 20px;
+    background: rgba(243, 156, 18, 0.1);
+    width: 60px;
+    height: 60px;
+    border-radius: 50%;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    flex-shrink: 0;
+}
+
+.confirm-message {
+    font-size: 16px;
+    line-height: 1.6;
+    color: #555;
+    margin: 0;
+}
+
+/* 确认对话框动画 */
+.confirm-modal .settings-dialog {
+    animation: bounceIn 0.4s cubic-bezier(0.68, -0.55, 0.27, 1.55);
+}
+
+@keyframes bounceIn {
+    0% {
+        transform: scale(0.8);
+        opacity: 0;
+    }
+    70% {
+        transform: scale(1.05);
+        opacity: 1;
+    }
+    100% {
+        transform: scale(1);
+    }
+}
+
+/* 黑暗模式下的确认对话框 */
+.dark-mode .confirm-icon {
+    color: #f39c12;
+    background: rgba(243, 156, 18, 0.2);
+}
+
+.dark-mode .confirm-message {
+    color: #ddd;
+}
+
+/* 暗黑模式下的导航栏按钮样式 */
+.dark-mode .navbar-actions .settings-link {
+    background-color: rgba(255, 255, 255, 0.08);
+    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
+}
+
+.dark-mode .navbar-actions .settings-link:hover {
+    background: linear-gradient(45deg, #3498db, #2980b9);
+    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
+}
+
+.dark-mode .navbar-actions .donate-link {
+    background: linear-gradient(135deg, #ff6b6b 0%, #f39c12 100%);
+    color: #fff;
+}
+
+.dark-mode .navbar-actions .donate-link:hover {
+    background: linear-gradient(45deg, #e74c3c, #c0392b);
+    color: #fff;
+}
+
+/* 暗黑模式下的波纹效果 */
+.dark-mode .navbar-actions .nav-item::after {
+    background: rgba(255, 255, 255, 0.4);
+}
+
+.dark-mode .navbar-actions .donate-link .nav-icon,
+.dark-mode .navbar-actions .settings-link .nav-icon {
+    color: #fff;
+}
+
+/* 工具排序样式 */
+.tool-sort-container {
+    background: #f8f9fa;
+    border-radius: 8px;
+    padding: 15px;
+    margin-top: 10px;
+}
+
+.sort-tips {
+    color: #6c757d;
+    font-size: 12px;
+    margin-bottom: 12px;
+    padding: 6px 10px;
+    background: rgba(0, 123, 255, 0.08);
+    border-left: 3px solid #007bff;
+    border-radius: 4px;
+    line-height: 1.4;
+}
+
+.sortable-list {
+    background: white;
+    border-radius: 6px;
+    border: 1px solid #e9ecef;
+    min-height: 180px;
+    max-height: 350px;
+    overflow-y: auto;
+    padding: 6px;
+}
+
+.sortable-item {
+    display: flex;
+    align-items: center;
+    padding: 2px 12px;
+    margin: 2px 0;
+    background: white;
+    border: 1px solid #e9ecef;
+    border-radius: 4px;
+    cursor: move;
+    transition: all 0.2s ease;
+    position: relative;
+}
+
+.sortable-item:hover {
+    background: #f8f9fa;
+    border-color: #007bff;
+    box-shadow: 0 1px 3px rgba(0, 123, 255, 0.15);
+    transform: translateY(-1px);
+}
+
+.sortable-item.dragging {
+    opacity: 0.6;
+    transform: scale(1.02);
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+    background: #fff3cd;
+    border-color: #ffc107;
+}
+
+.sortable-item.drag-over {
+    border-top: 3px solid #007bff;
+    background: rgba(0, 123, 255, 0.05);
+}
+
+.drag-handle {
+    font-size: 14px;
+    color: #6c757d;
+    margin-right: 10px;
+    cursor: grab;
+    padding: 2px 4px;
+    border-radius: 3px;
+    transition: all 0.2s ease;
+    user-select: none;
+    width: 20px;
+    text-align: center;
+    flex-shrink: 0;
+}
+
+.drag-handle:hover {
+    background: #e9ecef;
+    color: #495057;
+}
+
+.drag-handle:active {
+    cursor: grabbing;
+}
+
+.sortable-item .tool-icon {
+    font-size: 16px;
+    margin-right: 10px;
+    text-align: center;
+    flex-shrink: 0;
+}
+
+.sortable-item .tool-name {
+    font-weight: 500;
+    color: #343a40;
+    margin-right: 12px;
+    min-width: 100px;
+    flex-shrink: 0;
+    font-size: 14px;
+}
+
+.sortable-item .tool-desc {
+    color: #6c757d;
+    font-size: 12px;
+    flex: 1;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    line-height: 1.3;
+}
+
+.sort-actions {
+    margin-top: 12px;
+    text-align: right;
+}
+
+.sort-actions .btn {
+    margin-left: 6px;
+}
+
+.btn-sm {
+    padding: 5px 10px;
+    font-size: 12px;
+}
+
+/* 暗黑模式下的排序样式 */
+.dark-mode .tool-sort-container {
+    background: #2a2a2a;
+}
+
+.dark-mode .sort-tips {
+    background: rgba(0, 123, 255, 0.2);
+    color: #d4d4d4;
+}
+
+.dark-mode .sortable-list {
+    background: #1e1e1e;
+    border-color: #404040;
+}
+
+.dark-mode .sortable-item {
+    background: #1e1e1e;
+    border-color: #404040;
+}
+
+.dark-mode .sortable-item:hover {
+    background: #2a2a2a;
+    border-color: #007bff;
+}
+
+.dark-mode .sortable-item.dragging {
+    background: #3a3a2a;
+    border-color: #ffc107;
+}
+
+.dark-mode .sortable-item.drag-over {
+    background: rgba(0, 123, 255, 0.1);
+}
+
+.dark-mode .drag-handle {
+    color: #888;
+}
+
+.dark-mode .drag-handle:hover {
+    background: #404040;
+    color: #d4d4d4;
+}
+
+.dark-mode .sortable-item .tool-name {
+    color: #d4d4d4;
+}
+
+.dark-mode .sortable-item .tool-desc {
+    color: #888;
+}
+
+/* 突出显示修复按钮 */
+.highlight-bugfix {
+    background: linear-gradient(90deg, #ff9800 0%, #ffc107 100%);
+    color: #fff !important;
+    border-radius: 6px;
+    font-weight: bold;
+    margin-left: 10px;
+    padding: 0 16px;
+    box-shadow: 0 2px 8px rgba(255,152,0,0.10);
+    display: flex;
+    align-items: center;
+    transition: transform 0.15s, box-shadow 0.15s;
+    border: 1px solid #ff9800;
+    height: 32px;
+}
+
+.highlight-bugfix:hover {
+    background: linear-gradient(90deg, #ffc107 0%, #ff9800 100%);
+    color: #fff;
+    transform: scale(1.06);
+    box-shadow: 0 4px 16px rgba(255,152,0,0.18);
+    text-decoration: none;
+}
+
+.bugfix-emoji {
+    font-size: 20px;
+    margin-right: 6px;
+}
+
+.bugfix-text {
+    font-size: 15px;
+    letter-spacing: 1px;
+}
+
+

+ 304 - 144
apps/options/index.html

@@ -1,7 +1,7 @@
 <!DOCTYPE HTML>
 <html lang="zh-CN">
 <head>
-    <title>FeHelper-插件配置</title>
+    <title>FeHelper-插件市场</title>
     <meta charset="utf-8">
     <link rel="stylesheet" href="index.css"/>
     <script type="text/javascript" src="../static/vendor/evalCore.min.js"></script>
@@ -9,173 +9,331 @@
 </head>
 <body>
 
-<div class="wrapper" id="pageContainer">
-
-    <div class="panel-low-version" v-if="!es6Support">
-        FeHelper无法在<b>此低版本</b>的浏览器上正常工作!建议<b>升级到最新版</b>后再继续使用哦~~~
-    </div>
-
-    <div v-cloak v-if="es6Support">
-        <div class="panel panel-default mod-header">
-            <div class="panel-heading">
-                <h3 class="panel-title">
-                    <a href="https://www.baidufe.com/fehelper/index/index.html" target="_blank" class="x-a-high">
-                        <img src="../static/img/fe-16.png" alt="fehelper"/> FeHelper</a>:插件配置
-                    <span class="ui-fl-r x-cur-version"><b>当前版本</b><i>v{{manifest.version}}</i></span>
-                </h3>
-            </div>
+<div class="wrapper" id="marketContainer">
+    <div class="main-navbar">
+        <div class="navbar-brand">
+            <a href="https://fehelper.com" target="_blank" class="brand-link">
+                <img src="../static/img/fe-16.png" alt="fehelper"/> 
+                <span class="brand-text">FeHelper</span>
+                <span class="brand-subtitle">插件市场</span>
+            </a>
         </div>
-
-        <div class="panel-body mod-content">
-
-        <!-- ==============开发者板块============== -->
-        <div class="row">
-            <h4># 打赏一下鼓励升级!  <span class="x-tips">(很高兴这个插件能帮助到大家,也感谢大家对这个产品的认可,我尽量抽时间保持更新😄)</span></h4>
-
-            <div class="row ui-mt-10 btn-donate">
-                <button class="btn btn-success" @click="donateToggle(1)">码农不易,来杯咖啡☕️ </button>
-                <button class="btn btn-danger"  @click="donateToggle(0)">我先用着,以后再说😄</button>
-            </div>
-
-            <div class="box-donate hide" ref="boxDonate">
-                <img :src="donate.image" alt="donate" class="x-donate-img">
-            </div>
-            
-        </div>
-
-        <div class="row ui-mt-20">
-            <h4># FH快捷键  <span class="x-tips">(设置一些快捷键,用起来会更方便些)</span></h4>
-
-            <a href="#" @click="setShortcuts" id="setShortcuts">&gt;&gt; 点击配置FeHelper快捷键(当前快捷键为:{{ defaultKey }})&gt;&gt;</a> <br>
-            <div class="x-tips">
-                Tips:按下<b>快捷键</b>,自动打开FeHelper工具!然后用「上/下」方向键选择功能,「回车键」直接打开!
+        <div class="navbar-actions">
+            <a href="#" @click="openDonateModal" class="nav-item donate-link">
+                <i class="nav-icon">❤</i>
+                <span>打赏鼓励</span>
+            </a>
+            <a href="#" @click="showSettings" class="nav-item settings-link">
+                <i class="nav-icon">⚙</i>
+                <span>插件设置</span>
+            </a>
+            <a href="#" @click="autoFixBugs" class="nav-item bugfix-link highlight-bugfix" title="一键修复插件Bug,应用官方补丁">
+                <span class="bugfix-emoji">🛠️</span>
+                <span class="bugfix-text">一键修复Bug</span>
+            </a>
+            <div class="version-info" v-if="versionChecked">
+                <!-- 当版本相同时显示简化信息 -->
+                <div v-if="latestVersion && !needUpdate" class="version-latest">
+                    <i class="version-check-icon">✓</i>
+                    <span>当前已是最新版 </span>
+                    <span class="version-number">v{{manifest.version}}</span>
+                </div>
+                
+                <!-- 当版本不同或未获取到最新版本时显示详细信息 -->
+                <template v-else>
+                    <div class="version-details">
+                        <div class="version-label">当前版本:</div>
+                        <div class="current-version">v{{manifest.version}}</div>
+                    </div>
+                    <div class="version-details">
+                        <div class="version-label">最新版本:</div>
+                        <div class="latest-version-text" v-if="latestVersion">v{{latestVersion}}</div>
+                        <div class="latest-version-container" v-else>
+                            <img class="latest-version-img" @click="openStorePage" src="https://img.shields.io/chrome-web-store/v/pkgccpejnmalmdinmhkkfafefagiiiad?style=flat-square&label=%20" alt="最新版本">
+                        </div>
+                    </div>
+                    <button v-if="needUpdate" @click="openStorePage" class="update-btn">立即更新</button>
+                </template>
             </div>
         </div>
+    </div>
 
-        <div class="row ui-mt-20">
-            <h4># FH基本配置 <span class="x-tips">(插件的一些基础配置,可以在这里统一操作)</span></h4>
-
-            <div class="box-config">
-
-                <div class="st-item">
-                    <input type="checkbox" value="OPT_ITEM_CONTEXTMENUS" id="opt_item_contextMenus" v-model="selectedOpts"/>
-                    <label for="opt_item_contextMenus">将&nbsp;<b><i>FeHelper工具</i></b>&nbsp;添加到右键菜单<span class="x-tips">(如果你喜欢用右键菜单来使用FeHelper的方式,那可以在这里开启)</span></label> <br/>
+    <div class="market-container" v-cloak>
 
-                    <div class="box-inner row ui-ml-20" :class="{'x-disabled':!selectedOpts.includes('OPT_ITEM_CONTEXTMENUS')}">
-                        <div class="row-line">
-                            <input type="checkbox" id="MENU_DOWNLOAD_CRX" v-model="menuDownloadCrx" :disabled="!selectedOpts.includes('OPT_ITEM_CONTEXTMENUS')"/>
-                            <label for="MENU_DOWNLOAD_CRX">将&nbsp;<b><i>插件分享/下载</i></b>&nbsp;添加到右键菜单</label>
+        <!-- 设置弹窗 -->
+        <div class="settings-modal" v-if="showSettingsModal" v-cloak>
+            <div class="settings-dialog">
+                <div class="settings-header">
+                    <h3>FeHelper插件配置</h3>
+                    <span class="close-btn" @click="closeSettings">&times;</span>
+                </div>
+                <div class="settings-body">
+                    <!-- FH快捷键设置 -->
+                    <div class="settings-section">
+                        <h4># FH快捷键 <span class="x-tips">(设置一些快捷键,用起来会更方便些)</span></h4>
+                        <a href="#" @click="setShortcuts" id="setShortcuts">&gt;&gt; 点击配置FeHelper快捷键(当前快捷键为:{{ defaultKey }})&gt;&gt;</a> <br>
+                        <div class="x-tips">
+                            Tips:按下<b>快捷键</b>,自动打开FeHelper工具!然后用「上/下」方向键选择功能,「回车键」直接打开!
                         </div>
-                        <div class="row-line">
-                            <input type="checkbox" id="MENU_FEHELPER_SETTING" v-model="menuFeHelperSeting" :disabled="!selectedOpts.includes('OPT_ITEM_CONTEXTMENUS')"/>
-                            <label for="MENU_FEHELPER_SETTING">将&nbsp;<b><i>FeHelper设置</i></b>&nbsp;添加到右键菜单</label>
+                    </div>
+
+                    <!-- FH工具排序 -->
+                    <div class="settings-section">
+                        <h4># FH工具排序 <span class="x-tips">(自定义已安装工具在弹窗中的显示顺序)</span></h4>
+                        <div class="tool-sort-container">
+                            <div class="sort-tips">拖拽下方工具条目来调整顺序,调整后的顺序将在弹窗中生效。<br/>
+                                <span style="color: #52c41a; font-size: 11px;">💡 提示:点击底部"保存"按钮会同时保存工具排序和插件设置</span>
+                            </div>
+                            <div class="sortable-list" id="sortableList">
+                                <div v-for="(tool, index) in sortableTools" 
+                                     :key="tool.key" 
+                                     class="sortable-item"
+                                     :data-tool-key="tool.key"
+                                     draggable="true"
+                                     @dragstart="handleDragStart($event, index)"
+                                     @dragover="handleDragOver($event)"
+                                     @drop="handleDrop($event, index)"
+                                     @dragend="handleDragEnd">
+                                    <div class="drag-handle">⋮⋮</div>
+                                    <div class="tool-icon">{{tool.icon}}</div>
+                                    <div class="tool-name">{{tool.name}}</div>
+                                    <div class="tool-desc">{{tool.tips}}</div>
+                                </div>
+                            </div>
+                            <div class="sort-actions">
+                                <button class="btn btn-info btn-sm" @click="resetToolOrder">重置为默认顺序</button>
+                                <button class="btn btn-success btn-sm" @click="saveToolOrder">保存排序</button>
+                            </div>
                         </div>
                     </div>
-                </div>
 
-                <div class="st-item">
-                    <input type="checkbox" value="FORBID_OPEN_IN_NEW_TAB" id="FORBID_OPEN_IN_NEW_TAB" v-model="selectedOpts"/>
-                    <label for="FORBID_OPEN_IN_NEW_TAB">禁止在多个 Tab页/窗口 同时打开<span class="x-tips">(如果同一个工具,您只希望保留一个活动窗口,那可以勾选上)</span></label> <br/>
+                    <!-- FH基本配置 -->
+                    <div class="settings-section">
+                        <h4># FH基本配置 <span class="x-tips">(插件的一些基础配置,可以在这里统一操作)</span></h4>
+                        <div class="box-config">
+                            <div class="st-item">
+                                <input type="checkbox" value="OPT_ITEM_CONTEXTMENUS" id="opt_item_contextMenus" v-model="selectedOpts"/>
+                                <label for="opt_item_contextMenus">将&nbsp;<b><i>FeHelper工具</i></b>&nbsp;添加到右键菜单<span class="x-tips">(如果你喜欢用右键菜单来使用FeHelper的方式,那可以在这里开启)</span></label> <br/>
+
+                                <div class="box-inner row ui-ml-20" :class="{'x-disabled':!selectedOpts.includes('OPT_ITEM_CONTEXTMENUS')}">
+                                    <div class="row-line">
+                                        <input type="checkbox" id="MENU_DOWNLOAD_CRX" v-model="menuDownloadCrx" :disabled="!selectedOpts.includes('OPT_ITEM_CONTEXTMENUS')"/>
+                                        <label for="MENU_DOWNLOAD_CRX">将&nbsp;<b><i>插件分享/下载</i></b>&nbsp;添加到右键菜单</label>
+                                    </div>
+                                    <div class="row-line">
+                                        <input type="checkbox" id="MENU_FEHELPER_SETTING" v-model="menuFeHelperSeting" :disabled="!selectedOpts.includes('OPT_ITEM_CONTEXTMENUS')"/>
+                                        <label for="MENU_FEHELPER_SETTING">将&nbsp;<b><i>FeHelper设置</i></b>&nbsp;添加到右键菜单</label>
+                                    </div>
+                                </div>
+                            </div>
+
+                            <div class="st-item">
+                                <input type="checkbox" value="FORBID_OPEN_IN_NEW_TAB" id="FORBID_OPEN_IN_NEW_TAB" v-model="selectedOpts"/>
+                                <label for="FORBID_OPEN_IN_NEW_TAB">禁止在多个 Tab页/窗口 同时打开<span class="x-tips">(如果同一个工具,您只希望保留一个活动窗口,那可以勾选上)</span></label> <br/>
+                            </div>
+
+                            <div class="st-item">
+                                <input type="checkbox" value="CONTENT_SCRIPT_ALLOW_ALL_FRAMES" id="CONTENT_SCRIPT_ALLOW_ALL_FRAMES" v-model="selectedOpts"/>
+                                <label for="CONTENT_SCRIPT_ALLOW_ALL_FRAMES">授权FeHelper访问页面所有iframe<span class="x-tips">(默认不对网页内嵌的iframe支持,如果有需要可自行开启)</span></label> <br/>
+                            </div>
+
+                            <div class="st-item">
+                                <input type="checkbox" value="JSON_PAGE_FORMAT" id="JSON_PAGE_FORMAT" v-model="selectedOpts"/>
+                                <label for="JSON_PAGE_FORMAT">开启JSON自动美化功能(页面自动检测并格式化,你可在JSON自动美化后进行工具高级定制,包括皮肤等)</label> <br/>
+                            </div>
+
+                            <div class="st-item">
+                                <input type="checkbox" value="FORBID_STATISTICS" id="FORBID_STATISTICS" v-model="selectedOpts"/>
+                                <label for="FORBID_STATISTICS">禁止插件做访问统计<span class="x-tips">(勾选后,插件将不会向服务器上报任何统计数据)</span></label> <br/>
+                            </div>
+
+                            <div class="st-item" v-if="!isFirefox">
+                                <input type="checkbox" value="AUTO_DARK_MODE" id="AUTO_DARK_MODE" v-model="selectedOpts"/>
+                                <label for="AUTO_DARK_MODE">自动开启夜间模式(<span class="x-tips">眼睛保卫战,晚上<b><i>19:00~06:00</i></b>使用工具的时候自动开启夜间模式)</span></label>
+                                <a href="#" class="a-btn" @click="turnLight($event)">体验一下夜间模式!</a>
+                                <span class="x-count-down" v-cloak v-if="countDown>0">本次体验将在<b>{{countDown}}秒</b>后自动结束...</span> <br/>
+
+                                <div class="box-inner row ui-ml-20" :class="{'x-disabled':!selectedOpts.includes('AUTO_DARK_MODE')}">
+                                    <div class="row-line">
+                                        <input type="checkbox" value="ALWAYS_DARK_MODE" id="ALWAYS_DARK_MODE" v-model="selectedOpts" :disabled="!selectedOpts.includes('AUTO_DARK_MODE')"/>
+                                        <label for="ALWAYS_DARK_MODE">始终开启夜间模式(<span class="x-tips">忽略时间限制,随时Dark Mode)</span></label>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
                 </div>
-
-                <div class="st-item">
-                    <input type="checkbox" value="CONTENT_SCRIPT_ALLOW_ALL_FRAMES" id="CONTENT_SCRIPT_ALLOW_ALL_FRAMES" v-model="selectedOpts"/>
-                    <label for="CONTENT_SCRIPT_ALLOW_ALL_FRAMES">授权FeHelper访问页面所有iframe<span class="x-tips">(默认不对网页内嵌的iframe支持,如果有需要可自行开启)</span></label> <br/>
+                <div class="settings-footer">
+                    <button class="btn btn-success" @click="saveSettings">保存</button>
+                    <button class="btn btn-danger" @click="closeSettings">取消</button>
                 </div>
+            </div>
+        </div>
 
-                <div class="st-item">
-                    <input type="checkbox" value="JSON_PAGE_FORMAT" id="JSON_PAGE_FORMAT" v-model="selectedOpts"/>
-                    <label for="JSON_PAGE_FORMAT">开启JSON自动美化功能(<span class="x-tips">页面<b><i>自动检测并格式化</i></b>,您可在JSON自动美化页面进行工具<b><i>高级定制</i></b>,包括皮肤自定义等)</span></label> <br/>
+        <!-- 打赏模态框 -->
+        <div class="settings-modal donate-modal" v-if="showDonateModal" v-cloak>
+            <div class="settings-dialog">
+                <div class="settings-header">
+                    <h3># 打赏一下鼓励升级!</h3>
+                    <span class="close-btn" @click="closeDonateModal">&times;</span>
                 </div>
-
-                <div class="st-item" v-if="!isFirefox">
-                    <input type="checkbox" value="AUTO_DARK_MODE" id="AUTO_DARK_MODE" v-model="selectedOpts"/>
-                    <label for="AUTO_DARK_MODE">自动开启夜间模式(<span class="x-tips">眼睛保卫战,晚上<b><i>19:00~06:00</i></b>使用工具的时候自动开启夜间模式)</span></label>
-                    <a href="#" class="a-btn" @click="turnLight($event)">体验一下夜间模式!</a>
-                    <span class="x-count-down" v-cloak v-if="countDown>0">本次体验将在<b>{{countDown}}秒</b>后自动结束...</span> <br/>
-
-                    <div class="box-inner row ui-ml-20" :class="{'x-disabled':!selectedOpts.includes('AUTO_DARK_MODE')}">
-                        <div class="row-line">
-                            <input type="checkbox" value="ALWAYS_DARK_MODE" id="ALWAYS_DARK_MODE" v-model="selectedOpts" :disabled="!selectedOpts.includes('AUTO_DARK_MODE')"/>
-                            <label for="ALWAYS_DARK_MODE">始终开启夜间模式(<span class="x-tips">忽略时间限制,随时Dark Mode)</span></label>
+                <div class="settings-body">
+                    <div class="donate-content">
+                        <p class="donate-desc">{{donate.text}}我会尽量抽时间保持更新😄</p>
+                        <div class="donate-qrcode">
+                            <img :src="donate.image" alt="微信打赏二维码" class="donate-image">
+                            <p class="donate-text">微信打赏,鼓励升级!</p>
                         </div>
                     </div>
                 </div>
-
-                <div class="ui-mt-10">
-                    <button class="btn btn-success" @click="save()">保存</button>
-                    <button class="btn btn-danger"  @click="cancel()">放弃</button>
+                <div class="settings-footer">
+                    <button class="btn btn-success" @click="closeDonateModal">我已打赏</button>
+                    <button class="btn btn-danger" @click="closeDonateModal">以后再说</button>
                 </div>
-                    
             </div>
         </div>
 
-
-        <!-- ==============FH插件市场板块============== -->
-        <div class="row ui-mt-20">
-            <h4># FH应用市场 <span class="x-tips">(严格遵循Google官方要求的插件<span class="t-hlt">「单一用途原则」</span>,所以就按需安装吧!当然,你也可以<a href="#" @click="openFeOnline()">&nbsp;去官网使用在线免安装版&gt;&gt;</a>)</span></h4>
-
-            <ul class="mod-dynamic-tool">
-                <!-- 先展示已安装的 -->
-                <li v-for="(tool,index) in Object.keys(fhTools)" v-if="!fhTools[tool]._devTool && tool !== 'devtools' && fhTools[tool].installed">
-                    <i>{{fhTools[tool].menuConfig && fhTools[tool].menuConfig[0] && fhTools[tool].menuConfig[0].icon || '@'}}</i>
-                    <b>{{fhTools[tool].name}}</b>
-                    <span class="x-tips">({{fhTools[tool].tips}})</span>
-
-                    <span class="ui-fl-r x-btn-box">
-                        <button class="btn btn-danger btn-xs" @click="offLoad(tool,$event)" v-if="!fhTools[tool].systemInstalled">卸载</button>
-                        <button class="btn btn-disabled btn-xs" v-if="fhTools[tool].systemInstalled" title="系统预装,不可被卸载">卸载</button>
-                        <button class="btn btn-xs" :class="fhTools[tool].menu ? 'btn-warning' : 'btn-info'" @click="menuMgr(tool,$event)">{{fhTools[tool].menu ? '移出' : '加入'}}右键</button>
-                    </span>
-
-                    <span class="ui-fl-r x-sort-box" v-if="fhTools[tool].installed">
-                        <i title="上移" :class="index === 0 ? 'x-hidden' : ''" @click="sortUp(index)">⇧</i>
-                        <i title="下移" :class="index === sortArray.length-1 ? 'x-hidden' : ''" @click="sortDown(index)">⇩</i>
-                    </span>
-                </li>
-
-                <!-- 再展示未安装的 -->
-                <li v-for="(tool,index) in Object.keys(fhTools)" v-if="!fhTools[tool]._devTool && tool !== 'devtools' && !fhTools[tool].installed">
-                    <i>{{fhTools[tool].menuConfig && fhTools[tool].menuConfig[0] && fhTools[tool].menuConfig[0].icon || '@'}}</i>
-                    <b>{{fhTools[tool].name}}</b>
-                    <span class="x-tips">({{fhTools[tool].tips}})</span>
-
-                    <span class="ui-fl-r x-btn-box">
-                        <button class="btn btn-success btn-xs" @click="install(tool,$event)">安装<span class="x-progress"></span></button>
-                    </span>
-                </li>
-            </ul>
+        <!-- 自定义确认对话框 -->
+        <div class="settings-modal confirm-modal" v-if="confirmDialog.show" v-cloak>
+            <div class="settings-dialog">
+                <div class="settings-header">
+                    <h3>{{ confirmDialog.title }}</h3>
+                    <span class="close-btn" @click="cancelConfirm">&times;</span>
+                </div>
+                <div class="settings-body">
+                    <div class="confirm-content">
+                        <div class="confirm-icon">❓</div>
+                        <p class="confirm-message">{{ confirmDialog.message }}</p>
+                    </div>
+                </div>
+                <div class="settings-footer">
+                    <button class="btn btn-success" @click="confirmAction">确定</button>
+                    <button class="btn btn-danger" @click="cancelConfirm">取消</button>
+                </div>
+            </div>
         </div>
 
-        <!-- ==============开发者板块============== -->
-        <div class="row ui-mt-10">
-            <hr>
-            <h4># FH开发者板块  <span class="x-tips">(FeHelper开放平台化,你可以在安装<span class="t-hlt">「{{fhTools.devtools && fhTools.devtools.name}}」</span>后,开发属于你自己的FH工具!)</span></h4>
-
-            <ul class="mod-dynamic-tool">
-                <li v-for="(tool,index) in Object.keys(fhTools)" v-if="fhTools[tool]._devTool || tool === 'devtools'">
-                    <i>{{fhTools[tool].menuConfig && fhTools[tool].menuConfig[0] && fhTools[tool].menuConfig[0].icon || '@'}}</i>
-                    <b>{{fhTools[tool].name}}</b>
-                    <span class="x-tips">({{fhTools[tool].tips}})</span>
-
-                    <span class="ui-fl-r x-btn-box">
-                        <template v-if="tool === 'devtools'">
-                            <button class="btn btn-success btn-xs" @click="install(tool,$event)" v-if="!fhTools[tool].installed">安装<span class="x-progress"></span></button>
-                            <button class="btn btn-danger btn-xs" @click="offLoad(tool,$event)" v-if="fhTools[tool].installed">卸载</button>
-                            <button class="btn btn-xs" :class="fhTools[tool].menu ? 'btn-warning' : 'btn-info'" @click="menuMgr(tool,$event)" v-if="fhTools[tool].installed">{{fhTools[tool].menu ? '移出' : '加入'}}右键</button>
-                        </template>
-
-                        <template v-if="tool !== 'devtools'">
-                            <button class="btn btn-xs" :class="fhTools[tool]._enable ? 'btn-success' : 'btn-default'" style="cursor: default">{{fhTools[tool]._enable ? '启用中' : '已停用'}}</button>
-                            <button class="btn btn-xs" :class="fhTools[tool].menu ? 'btn-warning' : 'btn-info'" @click="menuMgr(tool,$event)" v-if="fhTools[tool]._enable">{{fhTools[tool].menu ? '移出' : '加入'}}右键</button>
-                        </template>
-                    </span>
-                </li>
-            </ul>
+        <!-- 推荐位和广告位 -->
+        <div class="recommendation-container">
+            <div class="recommendation-section hot-tools">
+                <div v-for="card in recommendationCards" :key="card.title"
+                     class="recommendation-card" 
+                     :class="{ 'ad-card': card.isAd }"
+                     @click="handleRecommendClick(card)">
+                    <div class="rec-icon" :class="{ 'ad-icon': card.isAd }">{{card.icon}}</div>
+                    <div class="rec-content">
+                        <h4>{{card.title}}</h4>
+                        <p>{{card.desc}}</p>
+                        <span :class="card.tagClass">{{card.tag}}</span>
+                    </div>
+                </div>
+            </div>
         </div>
 
+        
+        <!-- 顶部搜索和筛选区 -->
+        <div class="market-header">
+            <div class="search-filters-container">
+                <div class="search-box">
+                    <i class="search-icon">🔍</i>
+                    <input type="text" v-model="searchKey" placeholder="搜索工具..." @input="handleSearch">
+                </div>
+                <div class="filter-group">
+                    <select v-model="currentCategory" @change="handleCategoryChange(currentCategory)" class="filter-select">
+                        <option value="">全部分类</option>
+                        <option v-for="category in categories" :value="category.key">{{category.name}}</option>
+                    </select>
+                </div>
+                <div class="filter-group">
+                    <select v-model="sortType" @change="handleSort" class="filter-select">
+                        <option value="default">默认排序</option>
+                        <option value="newest">最新</option>
+                        <option value="hot">最热</option>
+                    </select>
+                </div>
+                <div class="view-toggle">
+                    <span class="view-btn" :class="{ active: viewMode === 'grid' }" @click="saveViewMode('grid')">网格视图</span>
+                    <span class="view-btn" :class="{ active: viewMode === 'list' }" @click="saveViewMode('list')">列表视图</span>
+                </div>
+            </div>
+        </div>
+        
+        <!-- 主体内容区 -->
+        <div class="market-content">
+            <!-- 侧边栏 -->
+            <div class="market-sidebar">
+                <div class="sidebar-section">
+                    <h4>工具分类</h4>
+                    <ul class="category-list">
+                        <li :class="{active: currentCategory === '' && currentView === 'all'}" 
+                            @click="handleCategoryChange('')">
+                            全部分类
+                            <span class="count">({{Object.keys(originalTools).length}})</span>
+                        </li>
+                        <li v-for="category in categories" 
+                            :class="{active: currentCategory === category.key && currentView === 'all'}"
+                            @click="handleCategoryChange(category.key)">
+                            {{category.name}}
+                            <span class="count">({{getCategoryCount(category.key)}})</span>
+                        </li>
+                    </ul>
+                </div>
+                <div class="sidebar-section">
+                    <h4>我的工具</h4>
+                    <ul class="my-tools">
+                        <li :class="{active: currentView === 'installed'}" @click="showMyInstalled">
+                            已安装
+                            <span class="count">({{installedCount}})</span>
+                        </li>
+                        <li :class="{active: currentView === 'favorites'}" @click="showMyFavorites">
+                            我的收藏
+                            <span class="count">({{getFavoritesCount()}})</span>
+                        </li>
+                        <li :class="{active: currentView === 'recent'}" @click="showRecentUsed">
+                            最近使用
+                            <span class="count">({{recentCount}})</span>
+                        </li>
+                    </ul>
+                </div>
+            </div>
 
-    </div>
+            <!-- 工具展示区 -->
+            <div class="tools-grid">
+                <div :class="['tools-container', viewMode]">
+                    <div v-for="tool in filteredTools" 
+                         :key="tool.key" 
+                         class="tool-card"
+                         :data-category="getToolCategory(tool.key)">
+                        <div class="tool-info">
+                            <h3>
+                                <span class="tool-icon">{{tool.menuConfig[0].icon}}</span>
+                                {{tool.name}}
+                            </h3>
+                            <p class="tool-desc">{{tool.tips}}</p>
+                        </div>
+                        <div class="tool-actions">
+                            <button :class="['btn', tool.installed ? 'btn-danger' : 'btn-success', 'btn-xs']"
+                                    @click="tool.installed ? uninstallTool(tool.key) : installTool(tool.key)"
+                                    :disabled="tool.systemInstalled && tool.installed"
+                                    :data-tool="tool.key">
+                                {{tool.installed ? '卸载' : '安装'}}<span class="x-progress"></span>
+                            </button>
+                            <button class="btn btn-xs" :class="tool.inContextMenu ? 'btn-warning' : 'btn-info'"
+                                    v-if="tool.installed"
+                                    @click="toggleContextMenu(tool.key)">
+                                {{tool.inContextMenu ? '移出右键' : '加入右键'}}
+                            </button>
+                            <button class="btn btn-default btn-xs" 
+                                    :class="{active: tool.favorite}"
+                                    @click="toggleFavorite(tool.key)">
+                                <span class="favorite-icon">❤</span>
+                                {{tool.favorite ? '已收藏' : '收藏'}}
+                            </button>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
     </div>
 </div>
 
@@ -183,4 +341,6 @@
 <script type="module" src="index.js"></script>
 
 </body>
-</html>
+</html> 
+
+

+ 1430 - 229
apps/options/index.js

@@ -1,329 +1,1530 @@
-/**
- * FeHelper Options Page
- */
-
-import Settings from './settings.js';
 import Awesome from '../background/awesome.js'
 import MSG_TYPE from '../static/js/common.js';
+import Settings from './settings.js';
+import Statistics from '../background/statistics.js';
+import toolMap from '../background/tools.js';
 
+// 工具分类定义
+const TOOL_CATEGORIES = [
+    { key: 'dev', name: '开发工具类', tools: ['json-format', 'json-diff', 'code-beautify', 'code-compress', 'postman', 'websocket', 'regexp','page-timing'] },
+    { key: 'encode', name: '编解码转换类', tools: ['en-decode', 'trans-radix', 'timestamp', 'trans-color'] },
+    { key: 'image', name: '图像处理类', tools: ['qr-code', 'image-base64', 'svg-converter', 'chart-maker', 'poster-maker' ,'screenshot', 'color-picker'] },
+    { key: 'productivity', name: '效率工具类', tools: ['aiagent', 'sticky-notes', 'html2markdown', 'page-monkey'] },
+    { key: 'calculator', name: '计算工具类', tools: ['crontab', 'loan-rate', 'password'] },
+    { key: 'other', name: '其他工具', tools: [] }
+];
+
+// Vue实例
 new Vue({
-    el: '#pageContainer',
+    el: '#marketContainer',
     data: {
-        es6Support:true,
-        defaultKey: 'Alt+Shift+J',
-        selectedOpts: [],
-        manifest: {},
-        fhTools: {},
-        menuFeHelperSeting: false,
-        menuDownloadCrx: false,
-        sortArray: [],
+        manifest: { version: '0.0.0' },
+        searchKey: '',
+        currentCategory: '',
+        sortType: 'default',
+        viewMode: 'list', // 默认网格视图
+        categories: TOOL_CATEGORIES,
+        favorites: new Set(),
+        recentUsed: [],
+        loading: true,
+        originalTools: {}, // 保存原始工具数据
+        currentView: 'all', // 当前视图类型(all/installed/favorites/recent)
+        activeTools: {}, // 当前显示的工具列表
+        installedCount: 0, // 已安装工具数量
+        
+        // 版本相关
+        latestVersion: '', // 最新版本号
+        needUpdate: false, // 是否需要更新
+        
+        // 设置相关
+        showSettingsModal: false,
+        defaultKey: 'Alt+Shift+J', // 默认快捷键
+        countDown: 0, // 夜间模式倒计时
+        selectedOpts: [], // 选中的选项(已支持FORBID_STATISTICS)
+        menuDownloadCrx: false, // 菜单-插件下载
+        menuFeHelperSeting: false, // 菜单-FeHelper设置
+        isFirefox: false, // 是否Firefox浏览器
+
+        // 打赏相关
+        showDonateModal: false,
         donate: {
-            text: '微信打赏!鼓励升级!',
+            text: '感谢你对FeHelper的认可和支持!',
             image: './donate.jpeg'
         },
-        countDown: 0,
-        isFirefox: /Firefox/.test(navigator.userAgent)
+
+        // 确认对话框
+        confirmDialog: {
+            show: false,
+            title: '操作确认',
+            message: '',
+            callback: null,
+            data: null
+        },
+
+        // 工具排序相关
+        sortableTools: [], // 可排序的工具列表
+        draggedIndex: -1, // 拖拽的工具索引
+
+        recentCount: 0,
+        versionChecked: false,
+        
+        // 推荐卡片配置,后续可从服务端获取
+        recommendationCards: [
+            {
+                toolKey: 'qr-code',
+                icon: '📱',
+                title: '二维码工具',
+                desc: '快速生成和识别二维码,支持自定义样式',
+                tag: '必装',
+                tagClass: 'must-tag',
+                isAd: false
+            },
+            {
+                toolKey: 'chart-maker',
+                icon: '📊',
+                title: '图表制作工具',
+                desc: '支持多种数据可视化图表,快速生成专业图表',
+                tag: '最新',
+                tagClass: 'new-tag',
+                isAd: false
+            },
+            {
+                toolKey: 'mock-data',
+                icon: '🎲',
+                title: '数据Mock工具',
+                desc: '快速生成各种测试数据,支持快速模板一键生成',
+                tag: '推荐',
+                tagClass: 'recommend-tag',
+                isAd: false
+            },
+            {
+                icon: '🔔',
+                title: '推广位',
+                desc: '广告位招租,欢迎流量主联系,开放合作,流量主请到github联系',
+                tag: '广告',
+                tagClass: 'ad-tag',
+                isAd: true,
+                url: 'https://github.com/zxlie/FeHelper'
+            }
+        ],
     },
 
-    created: function () {
-        // 页面滤镜:关掉
-        !this.isFirefox && DarkModeMgr.turnLightAuto();
+    async created() {
+        // 1. 读取URL中的query参数并赋值给searchKey
+        try {
+            const urlParams = new URLSearchParams(window.location.search);
+            const query = urlParams.get('query');
+            if (query) {
+                this.searchKey = query;
+            }
+        } catch (e) {
+            // 忽略异常
+        }
+        // 2. 初始化数据
+        await this.initData();
+        this.recentCount = (await Statistics.getRecentUsedTools(10)).length;
+        // 初始化后更新已安装工具数量
+        this.updateInstalledCount();
+        // 恢复用户的视图模式设置
+        this.loadViewMode();
+        // 加载设置项
+        this.loadSettings();
+        // 检查浏览器类型
+        this.checkBrowserType();
+        // 检查版本更新
+        this.checkVersionUpdate();
+        
+        // 加载远程推荐卡片配置
+        this.loadRemoteRecommendationCards();
+        
+        // 检查URL中是否有donate_from参数
+        this.checkDonateParam();
+
+        // 页面加载时自动获取并注入options页面的补丁
+        this.loadPatchHotfix();
 
-        this.initData().then(() => {
-            this.shortCut();
-            this.remoteHotFix();
+        // 埋点:自动触发options
+        chrome.runtime.sendMessage({
+            type: 'fh-dynamic-any-thing',
+            thing: 'statistics-tool-usage',
+            params: {
+                tool_name: 'options'
+            }
         });
     },
 
+    computed: {
+        filteredTools() {
+            if (this.loading) {
+                return [];
+            }
+
+            // 获取当前工具列表
+            let result = Object.values(this.activeTools).map(tool => ({
+                ...tool,
+                favorite: this.favorites.has(tool.key)
+            }));
+
+            // 搜索过滤
+            if (this.searchKey) {
+                const key = this.searchKey.toLowerCase();
+                result = result.filter(tool => 
+                    tool.name.toLowerCase().includes(key) || 
+                    tool.tips.toLowerCase().includes(key)
+                );
+            }
+
+            // 分类过滤,在所有视图下生效
+            if (this.currentCategory) {
+                const category = TOOL_CATEGORIES.find(c => c.key === this.currentCategory);
+                const categoryTools = category ? category.tools : [];
+                result = result.filter(tool => categoryTools.includes(tool.key));
+            }
+
+            // 排序
+            switch (this.sortType) {
+                case 'newest':
+                    result.sort((a, b) => (b.updateTime || 0) - (a.updateTime || 0));
+                    break;
+                case 'hot':
+                    result.sort((a, b) => (b.updateTime || 0) - (a.updateTime || 0));
+                    break;
+                default:
+                    const allTools = TOOL_CATEGORIES.reduce((acc, category) => {
+                        acc.push(...category.tools);
+                        return acc;
+                    }, []);
+                    
+                    result.sort((a, b) => {
+                        const indexA = allTools.indexOf(a.key);
+                        const indexB = allTools.indexOf(b.key);
+                        
+                        // 如果工具不在任何类别中,放到最后
+                        if (indexA === -1 && indexB === -1) {
+                            return a.key.localeCompare(b.key); // 字母顺序排序
+                        }
+                        if (indexA === -1) return 1;
+                        if (indexB === -1) return -1;
+                        
+                        return indexA - indexB;
+                    });
+            }
+
+            return result;
+        }
+    },
+
     methods: {
+        async initData() {
+            try {
+                this.loading = true;
 
-        initData: async function () {
+                // 获取manifest信息
+                const manifest = await chrome.runtime.getManifest();
+                this.manifest = manifest;
 
-            this.manifest = chrome.runtime.getManifest();
+                // 从 Awesome.getAllTools 获取工具列表
+                const tools = await Awesome.getAllTools();
+                
+                // 获取收藏数据
+                const favorites = await this.getFavoritesData();
+                this.favorites = new Set(favorites);
 
-            Settings.getOptions((opts) => {
-                this.selectedOpts = Object.keys(opts).filter(k => String(opts[k]) === 'true');
+                // 获取最近使用数据
+                const recentUsed = await this.getRecentUsedData();
+                this.recentUsed = recentUsed;
+                this.recentCount = recentUsed.length;
+
+                // 获取已安装工具列表
+                const installedTools = await Awesome.getInstalledTools();
+
+                // 处理工具数据
+                const processedTools = {};
+                Object.entries(tools).forEach(([key, tool]) => {
+                    // 检查工具是否已安装
+                    const isInstalled = installedTools.hasOwnProperty(key);
+                    // 检查是否有右键菜单
+                    const hasMenu = tool.menu || false;
+                    
+                    processedTools[key] = {
+                        ...tool,
+                        key, // 添加key到工具对象中
+                        updateTime: Date.now() - Math.floor(Math.random() * 30) * 24 * 60 * 60 * 1000,
+                        installed: isInstalled, // 使用实时安装状态
+                        inContextMenu: hasMenu, // 使用实时菜单状态
+                        systemInstalled: tool.systemInstalled || false, // 是否系统预装
+                        favorite: this.favorites.has(key)
+                    };
+                });
+
+                this.originalTools = processedTools;
+                
+                // 初始化activeTools为所有工具
+                this.activeTools = { ...processedTools };
+                
+                // 更新"其他工具"类别
+                this.updateOtherCategory(Object.keys(processedTools));
+
+                // 默认选中"全部分类"
+                this.currentCategory = '';
+            } catch (error) {
+                console.error('初始化数据失败:', error);
+            } finally {
+                this.loading = false;
+            }
+        },
+        
+        // 更新"其他工具"类别,将未分类的工具添加到此类别
+        updateOtherCategory(allToolKeys) {
+            // 获取所有已分类的工具
+            const categorizedTools = new Set();
+            TOOL_CATEGORIES.forEach(category => {
+                if (category.key !== 'other') {
+                    category.tools.forEach(tool => categorizedTools.add(tool));
+                }
             });
+            
+            // 找出未分类的工具
+            const uncategorizedTools = allToolKeys.filter(key => !categorizedTools.has(key));
+            
+            // 更新"其他工具"类别
+            const otherCategory = TOOL_CATEGORIES.find(category => category.key === 'other');
+            if (otherCategory) {
+                otherCategory.tools = uncategorizedTools;
+            }
+        },
+
+        // 检查版本更新
+        async checkVersionUpdate() {
+            try {
+                // 获取已安装的版本号
+                const currentVersion = this.manifest.version;
+                
+                // 尝试从本地存储获取最新版本信息,避免频繁请求
+                const cachedData = await new Promise(resolve => {
+                    chrome.storage.local.get('fehelper_latest_version_data', data => {
+                        resolve(data.fehelper_latest_version_data || null);
+                    });
+                });
+        
+                // 检查是否需要重新获取版本信息:
+                // 1. 缓存不存在
+                // 2. 缓存已过期(超过24小时)
+                // 3. 缓存的当前版本与实际版本不同(说明插件已更新)
+                const now = Date.now();
+                const cacheExpired = !cachedData || !cachedData.timestamp || (now - cachedData.timestamp > 24 * 60 * 60 * 1000);
+                const versionChanged = cachedData && cachedData.currentVersion !== currentVersion;
+                
+                this.versionChecked = !(cacheExpired || versionChanged);
+                if (!this.versionChecked) {
+                    try {
+                        // 使用shields.io的JSON API获取最新版本号
+                        const response = await fetch('https://img.shields.io/chrome-web-store/v/pkgccpejnmalmdinmhkkfafefagiiiad.json');
+                        if (!response.ok) {
+                            throw new Error(`HTTP错误:${response.status}`);
+                        }
+                        this.versionChecked = true;
+                        
+                        const data = await response.json();
+                        // 提取版本号 - shields.io返回的数据中包含版本信息
+                        let latestVersion = '';
+                        if (data && data.value) {
+                            // 去掉版本号前的'v'字符(如果有)
+                            latestVersion = data.value.replace(/^v/, '');
+                        }
+                        
+                        // 比较版本号
+                        const needUpdate = this.compareVersions(currentVersion, latestVersion) < 0;
+                        
+                        // 保存到本地存储中
+                        await chrome.storage.local.set({
+                            'fehelper_latest_version_data': {
+                                timestamp: now,
+                                currentVersion, // 保存当前检查时的版本号
+                                latestVersion,
+                                needUpdate
+                            }
+                        });
+                        
+                        this.latestVersion = latestVersion;
+                        this.needUpdate = needUpdate;
+                    } catch (fetchError) {
+                        // 获取失败时不显示更新按钮
+                        this.needUpdate = false;
+                        
+                        // 如果是版本变更导致的重新检查,但获取失败,则使用缓存数据
+                        if (versionChanged && cachedData) {
+                            this.latestVersion = cachedData.latestVersion || '';
+                            // 比较新的currentVersion和缓存的latestVersion
+                            this.needUpdate = this.compareVersions(currentVersion, cachedData.latestVersion) < 0;
+                        }
+                    }
+                } else {
+                    // 使用缓存数据
+                    this.latestVersion = cachedData.latestVersion || '';
+                    this.needUpdate = cachedData.needUpdate || false;
+                }
+            } catch (error) {
+                this.needUpdate = false; // 出错时不显示更新提示
+            }
+        },
+        
+        // 比较版本号:如果v1 < v2返回-1,v1 = v2返回0,v1 > v2返回1
+        compareVersions(v1, v2) {
+            // 将版本号拆分为数字数组
+            const v1Parts = v1.split('.').map(Number);
+            const v2Parts = v2.split('.').map(Number);
+            
+            // 计算两个版本号中较长的长度
+            const maxLength = Math.max(v1Parts.length, v2Parts.length);
+            
+            // 比较每一部分
+            for (let i = 0; i < maxLength; i++) {
+                // 获取当前部分,如果不存在则视为0
+                const part1 = v1Parts[i] || 0;
+                const part2 = v2Parts[i] || 0;
+                
+                // 比较当前部分
+                if (part1 < part2) return -1;
+                if (part1 > part2) return 1;
+            }
+            
+            // 所有部分都相等
+            return 0;
+        },
+        
+        // 打开Chrome商店页面
+        openStorePage() {
+            try {
+                // 使用Chrome Extension API请求检查更新
+                // Manifest V3中requestUpdateCheck返回Promise,结果是一个对象而不是数组
+                chrome.runtime.requestUpdateCheck().then(result => {
+                    // 正确获取status和details,它们是result对象的属性
+                    this.handleUpdateStatus(result.status, result.details);
+                }).catch(error => {
+                    this.handleUpdateError(error);
+                });
+            } catch (error) {
+                this.handleUpdateError(error);
+            }
+        },
 
-            this.sortArray = await Awesome.SortToolMgr.get();
+        // 处理更新状态
+        handleUpdateStatus(status, details) {
+            if (status === 'update_available') {
+                // 显示更新通知
+                this.showNotification({
+                    title: 'FeHelper 更新',
+                    message: '已发现新版本,正在更新...'
+                });
+                
+                // 重新加载扩展以应用更新
+                setTimeout(() => {
+                    chrome.runtime.reload();
+                }, 1000);
+            } else if (status === 'no_update') {
+                // 如果没有可用更新,但用户点击了更新按钮
+                this.showNotification({
+                    title: 'FeHelper 更新',
+                    message: '您的FeHelper已经是最新版本。'
+                });
+            } else {
+                // 其他情况,如更新检查失败等
+                // 备选方案:跳转到官方网站
+                chrome.tabs.create({ 
+                    url: 'https://fehelper.com/'
+                });
+                
+                this.showNotification({
+                    title: 'FeHelper 更新',
+                    message: '自动更新失败,请访问FeHelper官网手动获取最新版本。'
+                });
+            }
+        },
 
-            // 获取两个特殊右键菜单项的安装情况
-            Awesome.menuMgr('fehelper-setting', 'get').then(value => {
-                this.menuFeHelperSeting = String(value) !== '0';
+        // 处理更新错误
+        handleUpdateError(error) {
+            // 出错时跳转到官方网站
+            chrome.tabs.create({ 
+                url: 'https://fehelper.com/'
             });
-            Awesome.menuMgr('download-crx', 'get').then(value => {
-                this.menuDownloadCrx = String(value) === '1';
+            
+            this.showNotification({
+                title: 'FeHelper 更新错误',
+                message: '更新过程中出现错误,请手动检查更新。'
             });
+        },
 
-            Awesome.getAllTools().then(tools => {
-                this.fhTools = tools;
-                let isSortArrEmpty = !this.sortArray.length;
+        // 显示通知的统一方法
+        showNotification(options) {
+            try {
+                // 定义通知ID,方便后续关闭
+                const notificationId = 'fehelper-update-notification';
+                const simpleNotificationId = 'fehelper-simple-notification';
 
-                Object.keys(tools).forEach(tool => {                    
-                    if (tools[tool] && tools[tool].installed) {
-                        isSortArrEmpty && (tool !== 'devtools') && this.sortArray.push(tool);
+                // 直接尝试创建通知,不检查权限
+                // Chrome扩展在manifest中已声明notifications权限,应该可以直接使用
+                const notificationOptions = {
+                    type: 'basic',
+                    iconUrl: chrome.runtime.getURL('static/img/fe-48.png'),
+                    title: options.title || 'FeHelper',
+                    message: options.message || '',
+                    priority: 2,
+                    requireInteraction: false, // 改为false,因为我们会手动关闭
+                    silent: false // 播放音效
+                };
+                
+                // 首先尝试直接创建通知
+                chrome.notifications.create(notificationId, notificationOptions, (createdId) => {
+                    const error = chrome.runtime.lastError;
+                    if (error) {
+                        // 通知创建失败,尝试使用alert作为备选方案
+                        alert(`${options.title}: ${options.message}`);
+                        
+                        // 再尝试使用不同的选项创建通知
+                        const simpleOptions = {
+                            type: 'basic',
+                            iconUrl: chrome.runtime.getURL('static/img/fe-48.png'),
+                            title: options.title || 'FeHelper',
+                            message: options.message || ''
+                        };
+                        
+                        // 使用简化选项再次尝试
+                        chrome.notifications.create(simpleNotificationId, simpleOptions, (simpleId) => {
+                            if (chrome.runtime.lastError) {
+                                console.error('简化通知创建也失败:', chrome.runtime.lastError);
+                            } else {
+                                // 3秒后自动关闭简化通知
+                                setTimeout(() => {
+                                    chrome.notifications.clear(simpleId);
+                                }, 3000);
+                            }
+                        });
+                    } else {
+                        // 3秒后自动关闭通知
+                        setTimeout(() => {
+                            chrome.notifications.clear(createdId);
+                        }, 3000);
                     }
                 });
-                this.sortTools();
-            });
+                
+                // 同时使用内置UI显示消息
+                this.showInPageNotification(options);
+            } catch (error) {
+                // 降级为alert
+                alert(`${options.title}: ${options.message}`);
+            }
         },
 
-        shortCut: function () {
-            // 获取当前热键
-            chrome.commands && chrome.commands.getAll && chrome.commands.getAll(keys => {
-                keys.some(key => {
-                    if (key.name === '_execute_browser_action' && key.shortcut) {
-                        this.defaultKey = key.shortcut;
-                        return true;
+        // 在页面内显示通知消息
+        showInPageNotification(options) {
+            try {
+                // 创建一个通知元素
+                const notificationEl = document.createElement('div');
+                notificationEl.className = 'in-page-notification';
+                notificationEl.innerHTML = `
+                    <div class="notification-content">
+                        <div class="notification-title">${options.title || 'FeHelper'}</div>
+                        <div class="notification-message">${options.message || ''}</div>
+                    </div>
+                    <button class="notification-close">×</button>
+                `;
+                
+                // 添加样式
+                const style = document.createElement('style');
+                style.textContent = `
+                    .in-page-notification {
+                        position: fixed;
+                        bottom: 20px;
+                        right: 20px;
+                        background-color: #4285f4;
+                        color: white;
+                        padding: 15px;
+                        border-radius: 8px;
+                        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+                        z-index: 9999;
+                        display: flex;
+                        align-items: center;
+                        justify-content: space-between;
+                        min-width: 300px;
+                        animation: slideIn 0.3s ease-out;
+                    }
+                    .notification-content {
+                        flex: 1;
+                    }
+                    .notification-title {
+                        font-weight: bold;
+                        margin-bottom: 5px;
+                    }
+                    .notification-message {
+                        font-size: 14px;
+                    }
+                    .notification-close {
+                        background: none;
+                        border: none;
+                        color: white;
+                        font-size: 20px;
+                        cursor: pointer;
+                        margin-left: 10px;
+                        padding: 0 5px;
+                    }
+                    @keyframes slideIn {
+                        from { transform: translateX(100%); opacity: 0; }
+                        to { transform: translateX(0); opacity: 1; }
+                    }
+                    @keyframes slideOut {
+                        from { transform: translateX(0); opacity: 1; }
+                        to { transform: translateX(100%); opacity: 0; }
                     }
+                `;
+                
+                // 添加到页面
+                document.head.appendChild(style);
+                document.body.appendChild(notificationEl);
+                
+                // 点击关闭按钮移除通知
+                const closeBtn = notificationEl.querySelector('.notification-close');
+                if (closeBtn) {
+                    closeBtn.addEventListener('click', () => {
+                        notificationEl.style.animation = 'slideOut 0.3s ease-out forwards';
+                        notificationEl.addEventListener('animationend', () => {
+                            notificationEl.remove();
+                        });
+                    });
+                }
+                
+                // 3秒后自动移除(从5秒改为3秒)
+                setTimeout(() => {
+                    notificationEl.style.animation = 'slideOut 0.3s ease-out forwards';
+                    notificationEl.addEventListener('animationend', () => {
+                        notificationEl.remove();
+                    });
+                }, 3000);
+            } catch (error) {
+                console.error('创建页内通知出错:', error);
+            }
+        },
+
+        async getFavoritesData() {
+            return new Promise((resolve) => {
+                chrome.storage.local.get('favorites', (result) => {
+                    resolve(result.favorites || []);
                 });
             });
         },
 
-        sortTools: function (repaintMenu) {
+        async getRecentUsedData() {
+            // 直接从Statistics模块获取最近使用的工具
+            return await Statistics.getRecentUsedTools(10);
+        },
+
+        async saveFavorites() {
+            try {
+                await chrome.storage.local.set({
+                    favorites: Array.from(this.favorites)
+                });
+                // 更新工具的收藏状态
+                Object.keys(this.originalTools).forEach(key => {
+                    this.originalTools[key].favorite = this.favorites.has(key);
+                });
+            } catch (error) {
+                console.error('保存收藏失败:', error);
+            }
+        },
+
+        handleSearch() {
+            // 搜索时不重置视图类型,允许在已过滤的结果中搜索
+        },
+
+        handleCategoryChange(category) {
+            // 切换到全部工具视图
+            if (this.currentView !== 'all') {
+                this.currentView = 'all';
+                this.updateActiveTools('all');
+            }
+            this.currentCategory = category;
+            this.searchKey = '';
+            // 确保工具显示正确
+            this.activeTools = { ...this.originalTools };
+        },
+
+        handleSort() {
+            // 排序逻辑已在computed中实现
+        },
+
+        getCategoryCount(categoryKey) {
+            const category = TOOL_CATEGORIES.find(c => c.key === categoryKey);
+            const categoryTools = category ? category.tools : [];
+            return categoryTools.length;
+        },
+
+        async getInstalledCount() {
+            try {
+                // 使用Awesome.getInstalledTools实时获取已安装工具数量
+                const installedTools = await Awesome.getInstalledTools();
+                return Object.keys(installedTools).length;
+            } catch (error) {
+                // 回退到本地数据
+                return Object.values(this.originalTools).filter(tool => 
+                    tool.installed || tool.systemInstalled || false
+                ).length;
+            }
+        },
+
+        getFavoritesCount() {
+            return this.favorites.size;
+        },
 
-            let tools = {};
-            let installed = {};
-            Object.keys(this.fhTools).forEach(tool => {
-                if (this.fhTools[tool] && this.fhTools[tool].installed) {
-                    installed[tool] = this.fhTools[tool];
+        getToolCategory(toolKey) {
+            for (const category of TOOL_CATEGORIES) {
+                if (category.tools.includes(toolKey)) {
+                    return category.key;
                 }
-            });
-            if (this.sortArray.length) {
-                this.sortArray.forEach(tool => {
-                    if (this.fhTools[tool]) {
-                        tools[tool] = installed[tool];
-                    }else{
-                        Awesome.offLoad(tool);
+            }
+            return 'other';
+        },
+
+        async showMyInstalled() {
+            this.currentView = 'installed';
+            this.currentCategory = '';
+            this.searchKey = '';
+            await this.updateActiveTools('installed');
+            // 更新已安装工具数量
+            await this.updateInstalledCount();
+        },
+
+        showMyFavorites() {
+            this.currentView = 'favorites';
+            this.currentCategory = '';
+            this.searchKey = '';
+            this.updateActiveTools('favorites');
+        },
+
+        // 重置工具列表到原始状态
+        resetTools() {
+            this.currentView = 'all';
+        },
+
+        // 安装工具
+        async installTool(toolKey) {
+            try {
+                // 查找可能存在的按钮元素
+                const btnElement = document.querySelector(`button[data-tool="${toolKey}"]`);
+                let elProgress = null;
+                
+                // 如果是通过按钮点击调用的,获取进度条元素
+                if (btnElement) {
+                    if (btnElement.getAttribute('data-undergoing') === '1') {
+                        return false;
                     }
+                    btnElement.setAttribute('data-undergoing', '1');
+                    elProgress = btnElement.querySelector('span.x-progress');
+                }
+                
+                // 显示安装进度
+                let pt = 1;
+                await Awesome.install(toolKey);
+                
+                // 只有当进度条元素存在时才更新文本内容
+                if (elProgress) {
+                    elProgress.textContent = `(${pt}%)`;
+                    let ptInterval = setInterval(() => {
+                        elProgress.textContent = `(${pt}%)`;
+                        pt += Math.floor(Math.random() * 20);
+                        if(pt > 100) {
+                            clearInterval(ptInterval);
+                            elProgress.textContent = ``;
+                            
+                            // 在进度条完成后显示安装成功的通知
+                            this.showInPageNotification({
+                                message: `${this.originalTools[toolKey].name} 安装成功!`,
+                                type: 'success',
+                                duration: 3000
+                            });
+                        }
+                    }, 100);
+                } else {
+                    // 如果没有进度条元素,直接显示通知
+                    this.showInPageNotification({
+                        message: `${this.originalTools[toolKey].name} 安装成功!`,
+                        type: 'success',
+                        duration: 3000
+                    });
+                }
+                
+                // 更新原始数据和当前活动数据
+                this.originalTools[toolKey].installed = true;
+                if (this.activeTools[toolKey]) {
+                    this.activeTools[toolKey].installed = true;
+                }
+                
+                // 更新已安装工具数量
+                this.updateInstalledCount();
+                
+                // 如果按钮存在,更新其状态
+                if (btnElement) {
+                    btnElement.setAttribute('data-undergoing', '0');
+                }
+                
+                // 发送消息通知后台更新
+                chrome.runtime.sendMessage({
+                    type: MSG_TYPE.DYNAMIC_TOOL_INSTALL_OR_OFFLOAD,
+                    toolName: toolKey,
+                    action: 'install',
+                    showTips: true
+                });
+                
+            } catch (error) {
+                // 显示安装失败的通知
+                this.showInPageNotification({
+                    message: `安装失败:${error.message || '未知错误'}`,
+                    type: 'error',
+                    duration: 5000
                 });
-                Awesome.SortToolMgr.set(this.sortArray);
-            } else {
-                tools = installed;
             }
+        },
 
-            Object.keys(this.fhTools).forEach(tool => {
-                if (!tools[tool]) {
-                    tools[tool] = this.fhTools[tool];
-                }
-            });
-            this.fhTools = tools;
-            this.$forceUpdate();
-
-            // 重绘右键菜单,以确保排序实时更新
-            repaintMenu && chrome.runtime.sendMessage({
-                type: MSG_TYPE.DYNAMIC_TOOL_INSTALL_OR_OFFLOAD,
-                action: `menu-upgrade`,
-                showTips: false,
-                menuOnly: true
-            });
+        // 卸载工具
+        async uninstallTool(toolKey) {
+            try {
+                // 使用自定义确认对话框而非浏览器原生的confirm
+                this.showConfirm({
+                    title: '卸载确认',
+                    message: `确定要卸载"${this.originalTools[toolKey].name}"工具吗?`,
+                    callback: async (key) => {
+                        try {
+                            // 先调用Awesome.offLoad卸载工具(确保存储数据先被删除)
+                            await Awesome.offLoad(key);
+                            
+                            // 再发送消息给background更新browser action
+                            await chrome.runtime.sendMessage({
+                                type: MSG_TYPE.DYNAMIC_TOOL_INSTALL_OR_OFFLOAD,
+                                toolName: key,
+                                action: 'offload',
+                                showTips: true
+                            });
+                            
+                            // 更新原始数据和当前活动数据
+                            this.originalTools[key].installed = false;
+                            this.originalTools[key].inContextMenu = false;
+                            
+                            if (this.activeTools[key]) {
+                                this.activeTools[key].installed = false;
+                                this.activeTools[key].inContextMenu = false;
+                            }
+                            
+                            // 更新已安装工具数量
+                            this.updateInstalledCount();
+                            
+                            // 显示卸载成功的通知
+                            this.showInPageNotification({
+                                message: `${this.originalTools[key].name} 已成功卸载!`,
+                                type: 'success',
+                                duration: 3000
+                            });
+                        } catch (error) {
+                            // 显示卸载失败的通知
+                            this.showInPageNotification({
+                                message: `卸载失败:${error.message || '未知错误'}`,
+                                type: 'error',
+                                duration: 5000
+                            });
+                        }
+                    },
+                    data: toolKey
+                });
+            } catch (error) {
+                console.error('准备卸载过程中出错:', error);
+            }
         },
-        sortUp: function (index) {
-            if(index == 0) return;
-            this.sortArray[index] = this.sortArray.splice(index - 1, 1, this.sortArray[index])[0];
-            this.sortTools(true);
+
+        // 切换右键菜单
+        async toggleContextMenu(toolKey) {
+            try {
+                const tool = this.originalTools[toolKey];
+                const newState = !tool.inContextMenu;
+                
+                // 更新菜单状态
+                await Awesome.menuMgr(toolKey, newState ? 'install' : 'offload');
+                
+                // 更新原始数据和当前活动数据
+                tool.inContextMenu = newState;
+                if (this.activeTools[toolKey]) {
+                    this.activeTools[toolKey].inContextMenu = newState;
+                }
+                
+                // 发送消息通知后台更新右键菜单
+                chrome.runtime.sendMessage({
+                    type: MSG_TYPE.DYNAMIC_TOOL_INSTALL_OR_OFFLOAD,
+                    action: `menu-${newState ? 'install' : 'offload'}`,
+                    showTips: false,
+                    menuOnly: true
+                });
+            } catch (error) {
+                console.error('切换右键菜单失败:', error);
+            }
         },
-        sortDown: function (index) {
-            if(index == this.sortArray.length-1) return;
-            this.sortArray[index] = this.sortArray.splice(index + 1, 1, this.sortArray[index])[0];
-            this.sortTools(true);
+
+        // 切换收藏状态
+        async toggleFavorite(toolKey) {
+            try {
+                if (this.favorites.has(toolKey)) {
+                    this.favorites.delete(toolKey);
+                    // 更新原始数据和当前活动数据
+                    this.originalTools[toolKey].favorite = false;
+                    if (this.activeTools[toolKey]) {
+                        this.activeTools[toolKey].favorite = false;
+                    }
+                } else {
+                    this.favorites.add(toolKey);
+                    // 更新原始数据和当前活动数据
+                    this.originalTools[toolKey].favorite = true;
+                    if (this.activeTools[toolKey]) {
+                        this.activeTools[toolKey].favorite = true;
+                    }
+                }
+                await this.saveFavorites();
+                
+                // 如果是在收藏视图,需要更新视图
+                if (this.currentView === 'favorites') {
+                    this.updateActiveTools('favorites');
+                }
+            } catch (error) {
+                console.error('切换收藏状态失败:', error);
+            }
         },
 
-        remoteHotFix: function () {
-            let hotfix = () => {
-                // 从服务器同步最新添加的一些工具,实现远程更新,无需提审FeHelper
-                let remoteHotfixUrl = `${this.manifest.homepage_url}/static/js/hotfix.js?cur_ver=${new Date().toLocaleDateString()}`;
-                fetch(remoteHotfixUrl).then(resp => resp.text()).then(jsText => {
+        async updateActiveTools(view) {
+            if (this.loading || Object.keys(this.originalTools).length === 0) {
+                return;
+            }
+
+            switch (view) {
+                case 'installed':
+                    // 使用Awesome.getInstalledTools实时获取已安装工具
                     try {
-                        if (!jsText) return false;
-                        window.evalCore.getEvalInstance(window)(jsText);
-                    } catch (e) {
+                        const installedTools = await Awesome.getInstalledTools();
+                        // 合并installedTools与originalTools的数据
+                        this.activeTools = Object.fromEntries(
+                            Object.entries(this.originalTools).filter(([key]) => 
+                                installedTools.hasOwnProperty(key)
+                            )
+                        );
+                    } catch (error) {
+                        // 回退到本地数据
+                        this.activeTools = Object.fromEntries(
+                            Object.entries(this.originalTools).filter(([_, tool]) => 
+                                tool.installed || tool.systemInstalled || false
+                            )
+                        );
                     }
-                }).catch(error => console.log('远程热修复失败:', error));
-            };
-            setTimeout(hotfix, 2000);
+                    break;
+                case 'favorites':
+                    this.activeTools = Object.fromEntries(
+                        Object.entries(this.originalTools).filter(([key]) => this.favorites.has(key))
+                    );
+                    break;
+                case 'recent':
+                    // 切换recent时,recentUsed已在showRecentUsed中实时拉取
+                    this.activeTools = Object.fromEntries(
+                        Object.entries(this.originalTools).filter(([key]) => this.recentUsed.includes(key))
+                    );
+                    break;
+                case 'all':
+                default:
+                    this.activeTools = { ...this.originalTools };
+                    // 分类过滤在computed属性中处理
+                    break;
+            }
         },
 
-        close: () => {
-            chrome.tabs.query({active: true, currentWindow: true}, (tabs) => {
-                chrome.tabs.remove(tabs[0].id);
-            });
+        // 新增更新已安装工具数量的方法
+        async updateInstalledCount() {
+            this.installedCount = await this.getInstalledCount();
         },
 
-        cancel: function () {
-            this.close();
+        // 加载用户保存的视图模式
+        async loadViewMode() {
+            try {
+                const result = await new Promise(resolve => {
+                    chrome.storage.local.get('fehelper_view_mode', result => {
+                        resolve(result.fehelper_view_mode);
+                    });
+                });
+                
+                if (result) {
+                    this.viewMode = result;
+                }
+            } catch (error) {
+                console.error('加载视图模式失败:', error);
+            }
         },
 
-        save: function () {
-            // 还要保存两个特殊的菜单配置项
-            let settingAction = this.menuFeHelperSeting ? 'install' : 'offload';
-            let crxAction = this.menuDownloadCrx ? 'install' : 'offload';
-            Awesome.menuMgr('fehelper-setting', settingAction).then(() => {
-                Awesome.menuMgr('download-crx', crxAction).then(() => {
-                    chrome.runtime.sendMessage({
-                        type: MSG_TYPE.DYNAMIC_TOOL_INSTALL_OR_OFFLOAD,
-                        action: `menu-${crxAction}`,
-                        showTips: false,
-                        menuOnly: true
-                    }).then(() => {
-                        Settings.setOptions(this.selectedOpts, () => {
-                            // 保存成功提示,同时更新Menu
-                            chrome.runtime.sendMessage({
-                                type: MSG_TYPE.DYNAMIC_ANY_THING,
-                                thing: 'save-options'
-                            });
+        // 保存用户的视图模式选择
+        async saveViewMode(mode) {
+            try {
+                this.viewMode = mode;
+                await chrome.storage.local.set({
+                    'fehelper_view_mode': mode
+                });
+            } catch (error) {
+                console.error('保存视图模式失败:', error);
+            }
+        },
 
-                            // 自动开关灯一次
-                            DarkModeMgr.turnLightAuto();
-                        });
+        // 加载设置项
+        async loadSettings() {
+            try {
+                Settings.getOptions(async (opts) => {
+                    let selectedOpts = [];
+                    Object.keys(opts).forEach(key => {
+                        if(String(opts[key]) === 'true') {
+                            selectedOpts.push(key);
+                        }
+                    });
+                    this.selectedOpts = selectedOpts;
+                    
+                    // 加载右键菜单设置
+                    this.menuDownloadCrx = await Awesome.menuMgr('download-crx', 'get') === '1';
+                    this.menuFeHelperSeting = await Awesome.menuMgr('fehelper-setting', 'get') !== '0';
+                    
+                    // 获取快捷键
+                    chrome.commands.getAll((commands) => {
+                        for (let command of commands) {
+                            if (command.name === '_execute_action') {
+                                this.defaultKey = command.shortcut || 'Alt+Shift+J';
+                                break;
+                            }
+                        }
                     });
                 });
-            });
+            } catch (error) {
+                console.error('加载设置项失败:', error);
+            }
+        },
+        
+        // 检查浏览器类型
+        checkBrowserType() {
+            try {
+                this.isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
+            } catch (error) {
+                this.isFirefox = false;
+            }
+        },
+        
+        // 显示设置模态框
+        async showSettings() {
+            this.showSettingsModal = true;
+            // 加载可排序的工具列表
+            await this.loadSortableTools();
         },
 
-        setShortcuts: function () {
-            chrome.tabs.create({
-                url: 'chrome://extensions/shortcuts'
-            });
-            return false;
+        // 关闭设置模态框
+        closeSettings() {
+            this.showSettingsModal = false;
         },
 
-        donateToggle: function (type) {
-            let box = this.$refs.boxDonate;
-            if (type === 1) {
-                box.classList.remove('hide');
-            } else {
-                box.classList.add('hide');
-            }
+        // 显示打赏模态框
+        openDonateModal() {
+            this.showDonateModal = true;
         },
 
-        install: function (tool, event) {
+        // 关闭打赏模态框
+        closeDonateModal() {
+            this.showDonateModal = false;
+        },
 
-            let btn = event.target;
-            if (btn.tagName.toLowerCase() === 'i') {
-                btn = btn.parentNode;
-            }
+        // 显示确认对话框
+        showConfirm(options) {
+            this.confirmDialog = {
+                show: true,
+                title: options.title || '操作确认',
+                message: options.message || '确定要执行此操作吗?',
+                callback: options.callback || null,
+                data: options.data || null
+            };
+        },
 
-            if (btn.getAttribute('data-undergoing') === '1') {
-                return false;
+        // 确认操作
+        confirmAction() {
+            if (this.confirmDialog.callback) {
+                this.confirmDialog.callback(this.confirmDialog.data);
             }
-            btn.setAttribute('data-undergoing', 1);
-            let elProgress = btn.querySelector('span.x-progress');
+            this.confirmDialog.show = false;
+        },
 
-            // 显示安装进度
-            let pt = 1;
-            Awesome.install(tool).then(() => {
-                elProgress.textContent = `(${pt}%)`;
-                let ptInterval = setInterval(() => {
-                    elProgress.textContent = `(${pt}%)`;
-                    pt+= Math.floor(Math.random() * 20);
-                    if(pt>100) {
-                        clearInterval(ptInterval);
-                        elProgress.textContent = ``;
-                        this.fhTools[tool].installed = true;
-                        if (!this.sortArray.includes(tool) && (tool !== 'devtools')) {
-                            this.sortArray.push(tool);
-                        }
-                        // 按照安装状态进行排序
-                        this.sortTools();
-                        btn.setAttribute('data-undergoing', 0);
+        // 取消确认
+        cancelConfirm() {
+            this.confirmDialog.show = false;
+        },
 
+        // 保存设置
+        async saveSettings() {
+            try {
+                // 构建设置对象
+                let opts = {};
+                [
+                    'OPT_ITEM_CONTEXTMENUS',
+                    'FORBID_OPEN_IN_NEW_TAB',
+                    'CONTENT_SCRIPT_ALLOW_ALL_FRAMES',
+                    'JSON_PAGE_FORMAT',
+                    'AUTO_DARK_MODE',
+                    'ALWAYS_DARK_MODE',
+                    'FORBID_STATISTICS' // 新增,确保保存
+                ].forEach(key => {
+                    opts[key] = this.selectedOpts.includes(key).toString();
+                });
+                
+                // 先保存工具排序(如果用户有修改)
+                if (this.sortableTools && this.sortableTools.length > 0) {
+                    try {
+                        const toolOrder = this.sortableTools.map(tool => tool.key);
+                        await chrome.storage.local.set({
+                            tool_custom_order: JSON.stringify(toolOrder)
+                        });
+                    } catch (sortError) {
+                        console.warn('保存工具排序时出现错误:', sortError);
+                        // 工具排序保存失败不应该阻止设置保存
+                    }
+                }
+                
+                // 保存设置 - 直接传递对象,settings.js已增加对对象类型的支持
+                Settings.setOptions(opts, async () => {
+                    try {
+                        // 处理右键菜单
+                        const crxAction = this.menuDownloadCrx ? 'install' : 'offload';
+                        const settingAction = this.menuFeHelperSeting ? 'install' : 'offload';
+                        
+                        await Promise.all([
+                            Awesome.menuMgr('download-crx', crxAction),
+                            Awesome.menuMgr('fehelper-setting', settingAction)
+                        ]);
+                        
+                        // 通知后台更新右键菜单
                         chrome.runtime.sendMessage({
                             type: MSG_TYPE.DYNAMIC_TOOL_INSTALL_OR_OFFLOAD,
-                            toolName: tool,
-                            action: 'install',
-                            showTips: true
+                            action: 'menu-change',
+                            menuOnly: true
+                        });
+                        
+                        // 关闭弹窗
+                        this.closeSettings();
+                        
+                        // 显示提示
+                        this.showNotification({
+                            title: 'FeHelper 设置',
+                            message: '设置和工具排序已保存!'
+                        });
+                    } catch (innerError) {
+                        this.showNotification({
+                            title: 'FeHelper 设置错误',
+                            message: '保存菜单设置失败: ' + innerError.message
                         });
                     }
-                },100);
+                });
+            } catch (error) {
+                this.showNotification({
+                    title: 'FeHelper 设置错误',
+                    message: '保存设置失败: ' + error.message
+                });
+            }
+        },
+        
+        // 设置快捷键
+        setShortcuts() {
+            chrome.tabs.create({
+                url: 'chrome://extensions/shortcuts'
             });
         },
-
-        offLoad: function (tool, event) {
-
-            if(!confirm('防止误操作;请再次确认是否要卸载这个工具?')) {
-                return;
+        
+        // 体验夜间模式
+        turnLight(event) {
+            event.preventDefault();
+            
+            // 获取body元素
+            const body = document.body;
+            
+            // 切换夜间模式
+            if (body.classList.contains('dark-mode')) {
+                body.classList.remove('dark-mode');
+            } else {
+                body.classList.add('dark-mode');
+                
+                // 设置倒计时
+                this.countDown = 10;
+                
+                // 启动倒计时
+                const timer = setInterval(() => {
+                    this.countDown--;
+                    if (this.countDown <= 0) {
+                        clearInterval(timer);
+                        body.classList.remove('dark-mode');
+                    }
+                }, 1000);
             }
+        },
+
+        // 检查URL中的donate_from参数并显示打赏弹窗
+        checkDonateParam() {
+            try {
+                const urlParams = new URLSearchParams(window.location.search);
+                const donateFrom = urlParams.get('donate_from');
+                
+                if (donateFrom) {
+                    // 记录打赏来源
+                    chrome.storage.local.set({
+                        'fehelper_donate_from': donateFrom,
+                        'fehelper_donate_time': Date.now()
+                    });
+                    
+                    // 等待工具数据加载完成
+                    this.$nextTick(() => {
+                        // 在所有工具中查找匹配项
+                        let matchedTool = null;
+                        
+                        // 首先尝试直接匹配工具key
+                        if (this.originalTools && this.originalTools[donateFrom]) {
+                            matchedTool = this.originalTools[donateFrom];
+                        } else if (this.originalTools) {
+                            // 如果没有直接匹配,尝试在所有工具中查找部分匹配
+                            for (const [key, tool] of Object.entries(this.originalTools)) {
+                                if (key.includes(donateFrom) || donateFrom.includes(key) ||
+                                    (tool.name && tool.name.includes(donateFrom)) || 
+                                    (donateFrom && donateFrom.includes(tool.name))) {
+                                    matchedTool = tool;
+                                    break;
+                                }
+                            }
+                        }
+                        
+                        // 更新打赏文案
+                        if (matchedTool) {
+                            this.donate.text = `看起来【${matchedTool.name}】工具帮助到了你,感谢你的认可!`;
+                        } else {
+                            // 没有匹配到特定工具,使用通用文案
+                            this.donate.text = `感谢你对FeHelper的认可和支持!`;
+                        }
+                        
+                        // 显示打赏弹窗
+                        this.showDonateModal = true;
+                    });
 
-            if (event.target.getAttribute('data-undergoing') === '1') {
-                return false;
+                    // 埋点:自动触发options
+                    chrome.runtime.sendMessage({
+                        type: 'fh-dynamic-any-thing',
+                        thing: 'statistics-tool-usage',
+                        params: {
+                            tool_name: 'donate'
+                        }
+                    });
+                }
+            } catch (error) {
+                console.error('处理打赏参数时出错:', error);
             }
-            event.target.setAttribute('data-undergoing', 1);
+        },
 
-            Awesome.offLoad(tool).then(() => {
-                chrome.runtime.sendMessage({
-                    type: MSG_TYPE.DYNAMIC_TOOL_INSTALL_OR_OFFLOAD,
-                    action: 'offload',
-                    showTips: true
-                });
+        // 补充 getRecentCount,保证模板调用不报错,且数据源唯一
+        async getRecentCount() {
+            const recent = await Statistics.getRecentUsedTools(10);
+            return recent.length;
+        },
 
-                this.fhTools[tool].installed = false;
-                event.target.setAttribute('data-undergoing', 0);
-                let index = this.sortArray.indexOf(tool);
-                index !== -1 && this.sortArray.splice(index, 1);
+        async showRecentUsed() {
+            this.currentView = 'recent';
+            this.currentCategory = '';
+            this.searchKey = '';
+            // 重新获取最近使用的工具数据
+            this.recentUsed = await Statistics.getRecentUsedTools(10);
+            this.recentCount = this.recentUsed.length;
+            // activeTools会通过currentView的watcher自动更新
+        },
 
-                // 继续移除右键菜单
-                Awesome.menuMgr(tool, 'offload').then(() => {
-                    this.fhTools[tool].menu = false;
+        handleRecommendClick(card) {
+            if (card.isAd && card.url) {
+                window.open(card.url, '_blank');
+            } else if (card.toolKey) {
+                this.installTool(card.toolKey);
+            }
+        },
 
+        // 加载远程推荐卡片配置
+        async loadRemoteRecommendationCards() {
+            try {
+                // 通过background代理请求,解决CORS问题
+                const result = await new Promise((resolve) => {
                     chrome.runtime.sendMessage({
-                        type: MSG_TYPE.DYNAMIC_TOOL_INSTALL_OR_OFFLOAD,
-                        action: `menu-offload`,
-                        showTips: false,
-                        menuOnly: true
+                        type: 'fh-dynamic-any-thing',
+                        thing: 'fetch-hotfix-json',
+                    }, resolve);
+                });
+                if (!result || !result.success) {
+                    throw new Error('获取远程配置失败: ' + (result && result.error ? result.error : '未知错误'));
+                }
+                // 获取脚本内容
+                const scriptContent = result.content;
+                // 解析脚本内容,提取GlobalRecommendationCards变量
+                let remoteCards = null;
+                try {
+                    remoteCards = JSON.parse(scriptContent);
+                } catch (parseError) {
+                    console.error('解析远程推荐卡片配置失败:', parseError);
+                }
+                
+                // 如果成功解析到配置,则更新本地配置
+                if (remoteCards && Array.isArray(remoteCards) && remoteCards.length > 0) {
+                    remoteCards.forEach((card, idx) => {
+                        if (
+                            card &&
+                            card.toolKey &&
+                            this.originalTools &&
+                            !this.originalTools.hasOwnProperty(card.toolKey)
+                        ) {
+                            // toolKey 不存在于本地工具,跳过赋值,保留本地默认
+                            return;
+                        }
+                        // 没有 toolKey 字段,或者 toolKey 存在于本地工具,直接覆盖
+                        this.recommendationCards[idx] = card;
                     });
+                    this.$forceUpdate();
+                }
+            } catch (error) {
+                console.error('获取远程推荐卡片配置失败:', error);
+            }
+        },
 
-                    // 按照安装状态进行排序
-                    this.sortTools();
-                });
+        // 工具排序相关方法
+        async loadSortableTools() {
+            try {
+                const installedTools = await Awesome.getInstalledTools();
+                
+                // 从存储中加载自定义排序
+                const customOrder = await chrome.storage.local.get('tool_custom_order');
+                const savedOrder = customOrder.tool_custom_order ? JSON.parse(customOrder.tool_custom_order) : null;
+                
+                // 转换为可排序的数组格式
+                let toolsArray = Object.entries(installedTools).map(([key, tool]) => ({
+                    key,
+                    name: tool.name,
+                    tips: tool.tips,
+                    icon: tool.icon || (tool.menuConfig && tool.menuConfig[0] ? tool.menuConfig[0].icon : '🔧')
+                }));
+                
+                // 如果有保存的自定义排序,按照该顺序排列
+                if (savedOrder && Array.isArray(savedOrder)) {
+                    const orderedTools = [];
+                    const unorderedTools = [...toolsArray];
+                    
+                    // 按照保存的顺序添加工具
+                    savedOrder.forEach(toolKey => {
+                        const toolIndex = unorderedTools.findIndex(t => t.key === toolKey);
+                        if (toolIndex !== -1) {
+                            orderedTools.push(unorderedTools.splice(toolIndex, 1)[0]);
+                        }
+                    });
+                    
+                    // 添加新安装的工具(不在保存的顺序中的)
+                    orderedTools.push(...unorderedTools);
+                    toolsArray = orderedTools;
+                }
+                
+                this.sortableTools = toolsArray;
+            } catch (error) {
+                console.error('加载可排序工具失败:', error);
+            }
+        },
+
+        // 拖拽开始
+        handleDragStart(event, index) {
+            this.draggedIndex = index;
+            event.target.classList.add('dragging');
+            event.dataTransfer.setData('text/plain', index);
+        },
+
+        // 拖拽经过
+        handleDragOver(event) {
+            event.preventDefault();
+            // 移除所有 drag-over 类
+            document.querySelectorAll('.sortable-item').forEach(item => {
+                item.classList.remove('drag-over');
             });
+            // 添加到当前元素
+            event.currentTarget.classList.add('drag-over');
         },
 
-        menuMgr: function (tool, event) {
+        // 放置
+        handleDrop(event, dropIndex) {
+            event.preventDefault();
+            
+            if (this.draggedIndex === -1 || this.draggedIndex === dropIndex) {
+                return;
+            }
 
-            if (event.target.getAttribute('data-undergoing') === '1') {
-                return false;
+            // 重新排列数组
+            const draggedItem = this.sortableTools[this.draggedIndex];
+            const newTools = [...this.sortableTools];
+            
+            // 移除被拖拽的项目
+            newTools.splice(this.draggedIndex, 1);
+            
+            // 在新位置插入
+            if (dropIndex > this.draggedIndex) {
+                newTools.splice(dropIndex - 1, 0, draggedItem);
+            } else {
+                newTools.splice(dropIndex, 0, draggedItem);
             }
-            event.target.setAttribute('data-undergoing', 1);
+            
+            this.sortableTools = newTools;
+            
+            // 清理样式
+            this.cleanupDragStyles();
+        },
 
-            let offLoadMode = this.fhTools[tool].menu;
-            let action = offLoadMode ? 'offload' : 'install';
+        // 拖拽结束
+        handleDragEnd(event) {
+            this.cleanupDragStyles();
+            this.draggedIndex = -1;
+        },
 
-            Awesome.menuMgr(tool, action).then(() => {
-                chrome.runtime.sendMessage({
-                    type: MSG_TYPE.DYNAMIC_TOOL_INSTALL_OR_OFFLOAD,
-                    action: `menu-${action}`,
-                    showTips: true,
-                    menuOnly: true
-                });
+        // 清理拖拽样式
+        cleanupDragStyles() {
+            document.querySelectorAll('.sortable-item').forEach(item => {
+                item.classList.remove('dragging', 'drag-over');
+            });
+        },
 
-                this.fhTools[tool].menu = !offLoadMode;
-                event.target.setAttribute('data-undergoing', 0);
+        // 重置工具顺序为默认
+        async resetToolOrder() {
+            try {
+                // 移除保存的自定义排序
+                await chrome.storage.local.remove('tool_custom_order');
+                
+                // 重新加载工具列表(会使用默认顺序)
+                await this.loadSortableTools();
+                
+                this.showInPageNotification({
+                    message: '工具顺序已重置为默认排序',
+                    type: 'success'
+                });
+            } catch (error) {
+                console.error('重置工具顺序失败:', error);
+                this.showInPageNotification({
+                    message: '重置失败,请重试',
+                    type: 'error'
+                });
+            }
+        },
 
-                // 页面强制刷新渲染
-                this.$forceUpdate();
-            });
+        // 保存工具排序
+        async saveToolOrder() {
+            try {
+                const toolOrder = this.sortableTools.map(tool => tool.key);
+                await chrome.storage.local.set({
+                    tool_custom_order: JSON.stringify(toolOrder)
+                });
+                
+                this.showInPageNotification({
+                    message: '工具排序已保存!弹窗中的工具将按此顺序显示',
+                    type: 'success'
+                });
+            } catch (error) {
+                console.error('保存工具排序失败:', error);
+                this.showInPageNotification({
+                    message: '保存失败,请重试',
+                    type: 'error'
+                });
+            }
         },
 
-        turnLight(event) {
-            event.preventDefault();
-            event.stopPropagation();
-            DarkModeMgr.turnLight(true);
-            this.countDown = 5;
-            let intervalId = setInterval(() => {
-                if (this.countDown === 0) {
-                    DarkModeMgr.turnLight(false);
-                    clearInterval(intervalId);
-                } else {
-                    this.countDown--;
+        async autoFixBugs() {
+            this.showNotification({ message: '正在拉取修复补丁,请稍候...' });
+            chrome.runtime.sendMessage({
+                type: 'fh-dynamic-any-thing',
+                thing: 'fetch-fehelper-patchs'
+            }, (resp) => {
+                if (!resp || !resp.success) {
+                    this.showNotification({ message: '补丁拉取失败,请稍后重试!', type: 'error' });
+                    return;
                 }
-            }, 1000);
+                this.showNotification({
+                    message: '当前FeHelper插件中的已知Bug都已修复,你可以去验证了。',
+                    type: 'success',
+                    duration: 5000
+                });
+                // 当前页面的bug立即更新
+                this.loadPatchHotfix();
+            });
         },
 
-        openFeOnline: function () {
-            chrome.tabs.create({
-                url: this.manifest.homepage_url
+        loadPatchHotfix() {
+            // 页面加载时自动获取并注入options页面的补丁
+            chrome.runtime.sendMessage({
+                type: 'fh-dynamic-any-thing',
+                thing: 'fh-get-tool-patch',
+                toolName: 'options'
+            }, patch => {
+                if (patch) {
+                    if (patch.css) {
+                        const style = document.createElement('style');
+                        style.textContent = patch.css;
+                        document.head.appendChild(style);
+                    }
+                    if (patch.js) {
+                        try {
+                            if (window.evalCore && window.evalCore.getEvalInstance) {
+                                window.evalCore.getEvalInstance(window)(patch.js);
+                            }
+                        } catch (e) {
+                            console.error('options补丁JS执行失败', e);
+                        }
+                    }
+                }
             });
         }
+    },
+
+    watch: {
+        // 监听currentView变化
+        currentView: {
+            immediate: true,
+            handler(newView) {
+                this.updateActiveTools(newView);
+            }
+        },
+        
+        // 监听currentCategory变化
+        currentCategory: {
+            handler(newCategory) {
+                // 保证在视图模式之外的分类切换也能正确显示
+                if (this.currentView === 'all') {
+                    this.activeTools = { ...this.originalTools };
+                }
+                // 重置搜索条件
+                if (this.searchKey) {
+                    this.searchKey = '';
+                }
+            }
+        },
+    },
+});
+
+// 添加滚动事件监听
+window.addEventListener('scroll', () => {
+    const header = document.querySelector('.market-header');
+    const sidebar = document.querySelector('.market-sidebar');
+    
+    if (window.scrollY > 10) {
+        header.classList.add('scrolled');
+        sidebar && sidebar.classList.add('scrolled');
+    } else {
+        header.classList.remove('scrolled');
+        sidebar && sidebar.classList.remove('scrolled');
     }
 });
+
+// 页面加载后自动采集
+if (window.chrome && chrome.runtime && chrome.runtime.sendMessage) {
+    Awesome.collectAndSendClientInfo();
+} 
+
+
+

+ 51 - 18
apps/options/settings.js

@@ -14,7 +14,8 @@ export default (() => {
         'FORBID_OPEN_IN_NEW_TAB': false,
         'AUTO_DARK_MODE': false,
         'ALWAYS_DARK_MODE': false,
-        'CONTENT_SCRIPT_ALLOW_ALL_FRAMES': false
+        'CONTENT_SCRIPT_ALLOW_ALL_FRAMES': false,
+        'FORBID_STATISTICS': false
     };
 
     /**
@@ -32,15 +33,20 @@ export default (() => {
     let _getOptions = function (callback) {
         let rst = {};
         chrome.storage.local.get(_getAllOpts(),(objs) => {
-            Object.keys(objs).forEach(item => {
-                let opt = objs[item];
-                if (opt !== null) {
-                    rst[item] = opt;
+            // 确保objs是一个对象
+            objs = objs || {};
+            
+            // 遍历所有配置项,确保每个配置项都有值
+            _getAllOpts().forEach(item => {
+                if (objs.hasOwnProperty(item) && objs[item] !== null) {
+                    rst[item] = objs[item];
                 } else {
+                    // 使用默认值
                     rst[item] = optionItemsWithDefaultValue[item];
                 }
             });
-            callback.call(null, rst);
+            
+            callback && callback.call(null, rst);
         });
     };
 
@@ -51,20 +57,46 @@ export default (() => {
      * @private
      */
     let _setOptions = function (items, callback) {
+        // 确保items是数组类型
+        if (!Array.isArray(items)) {
+            // 如果传入的是对象类型,转换为数组形式
+            if (typeof items === 'object' && items !== null) {
+                let tempItems = [];
+                Object.keys(items).forEach(key => {
+                    let obj = {};
+                    obj[key] = items[key];
+                    tempItems.push(obj);
+                });
+                items = tempItems;
+            } else {
+                items = [];
+            }
+        }
+
         _getAllOpts().forEach((opt) => {
-            let found = items.some(it => {
-                if (typeof(it) === 'string' && it === opt) {
-                    Awesome.StorageMgr.set(opt,'true');
-                    return true;
-                }
-                else if (typeof(it) === 'object' && it.hasOwnProperty(opt)) {
-                    Awesome.StorageMgr.set(opt,it[opt]);
-                    return true;
+            try {
+                let found = items.some(it => {
+                    if (!it) return false;
+                    
+                    if (typeof(it) === 'string' && it === opt) {
+                        chrome.storage.local.set({[opt]: 'true'});
+                        return true;
+                    }
+                    else if (typeof(it) === 'object' && it !== null && it.hasOwnProperty(opt)) {
+                        chrome.storage.local.set({[opt]: it[opt]});
+                        return true;
+                    }
+                    return false;
+                });
+                if (!found) {
+                    chrome.storage.local.set({[opt]: 'false'});
                 }
-                return false;
-            });
-            if (!found) {
-                Awesome.StorageMgr.set(opt,'false');
+            } catch (e) {
+                console.error('保存设置出错:', e, opt);
+                // 出错时设置为默认值
+                chrome.storage.local.set({
+                    [opt]: optionItemsWithDefaultValue[opt] === true ? 'true' : 'false'
+                });
             }
         });
 
@@ -77,3 +109,4 @@ export default (() => {
         setOptions: _setOptions
     };
 })();
+

+ 0 - 3
apps/page-monkey/index.css

@@ -14,9 +14,6 @@
 .panel-title a.x-tooltip:hover {
 	text-decoration: underline;
 }
-.x-toolbox {
-	float: right;
-}
 .x-toolbox .x-line {
 	font-size: 12px;
 	margin-left: 10px;

+ 3 - 1
apps/page-monkey/index.html

@@ -14,7 +14,7 @@
             <div class="panel panel-default" style="margin-bottom: 0px;">
                 <div class="panel-heading">
                     <h3 class="panel-title">
-                        <a href="https://www.baidufe.com/fehelper/index/index.html" target="_blank" class="x-a-high">
+                        <a href="https://fehelper.com" target="_blank" class="x-a-high">
                             <img src="../static/img/fe-16.png" alt="fehelper"/> FeHelper</a>:网页油猴工具
 
                         <span class="x-toolbox" v-cloak v-show="!editing">
@@ -31,6 +31,8 @@
                             <input type="button" @click="closeEditor()" value="返回" class="btn btn-sm btn-warning">
                         </span>
 
+                        <a href="#" class="x-donate-link" @click="openDonateModal($event)"><i class="nav-icon">❤&nbsp;</i>打赏鼓励</a>
+                        <a class="x-other-tools" @click="openOptionsPage($event)"><i class="icon-plus-circle"></i> 探索更多实用工具 <span class="tool-market-badge">工具市场</span></a>
                     </h3>
                 </div>
             </div>

+ 45 - 1
apps/page-monkey/index.js

@@ -105,9 +105,38 @@ new Vue({
         this.getPageMonkeyConfigs((cmList) => {
             this.cachedMonkeys = (cmList || []).filter(cm => cm.mName && cm.mPattern);
         });
+
+        this.loadPatchHotfix();
     },
 
     methods: {
+
+        loadPatchHotfix() {
+            // 页面加载时自动获取并注入页面的补丁
+            chrome.runtime.sendMessage({
+                type: 'fh-dynamic-any-thing',
+                thing: 'fh-get-tool-patch',
+                toolName: 'page-monkey'
+            }, patch => {
+                if (patch) {
+                    if (patch.css) {
+                        const style = document.createElement('style');
+                        style.textContent = patch.css;
+                        document.head.appendChild(style);
+                    }
+                    if (patch.js) {
+                        try {
+                            if (window.evalCore && window.evalCore.getEvalInstance) {
+                                window.evalCore.getEvalInstance(window)(patch.js);
+                            }
+                        } catch (e) {
+                            console.error('page-monkey补丁JS执行失败', e);
+                        }
+                    }
+                }
+            });
+        },
+
         getPageMonkeyConfigs: function (callback) {
 
             chrome.storage.local.get(PAGE_MONKEY_LOCAL_STORAGE_KEY, (resps) => {
@@ -526,7 +555,22 @@ new Vue({
             window.feHelperAlertMsgTid = window.setTimeout(function () {
                 elAlertMsg.style.display = 'none';
             }, 1000);
-        }
+        },
 
+        openDonateModal: function(event) {
+            event.preventDefault();
+            event.stopPropagation();
+            chrome.runtime.sendMessage({
+                type: 'fh-dynamic-any-thing',   
+                thing: 'open-donate-modal',
+                params: { toolName: 'page-monkey' }
+            });
+        },
+
+        openOptionsPage: function(event) {
+            event.preventDefault();
+            event.stopPropagation();
+            chrome.runtime.openOptionsPage();
+        }
     }
 });

+ 268 - 21
apps/page-timing/content-script.js

@@ -4,40 +4,283 @@
  */
 window.pagetimingContentScript = function () {
 
-    let __importScript = (filename) => {
-        pleaseLetJsLoaded = 100;
-        let url = filename;
+    /**
+     * Navigation Timing API helpers
+     * timing.getTimes();
+     **/
+    window.timing = window.timing || {
+        /**
+         * Outputs extended measurements using Navigation Timing API
+         * @param  Object opts Options (simple (bool) - opts out of full data view)
+         * @return Object      measurements
+         */
+        getTimes: function(opts) {
+            var performance = window.performance || window.webkitPerformance || window.msPerformance || window.mozPerformance;
 
-        if (location.protocol === 'chrome-extension:' || chrome.runtime && chrome.runtime.getURL) {
-            url = chrome.runtime.getURL('page-timing/' + filename);
-        }
-        fetch(url).then(resp => resp.text()).then(jsText => {
-            if(window.evalCore && window.evalCore.getEvalInstance){
-                return window.evalCore.getEvalInstance(window)(jsText);
+            if (performance === undefined) {
+                return false;
             }
-            let el = document.createElement('script');
-            el.textContent = jsText;
-            document.head.appendChild(el);
-        });
+
+            var timing = performance.timing;
+            var api = {};
+            opts = opts || {};
+
+            if (timing) {
+                if(opts && !opts.simple) {
+                    for (var k in timing) {
+                        if(isNumeric(timing[k])) {
+                            api[k] = parseFloat(timing[k]);
+                        }
+                    }
+                }
+
+                // Time to first paint
+                if (api.firstPaint === undefined) {
+                    var firstPaint = 0;
+
+                    // IE
+                    if (typeof timing.msFirstPaint === 'number') {
+                        firstPaint = timing.msFirstPaint;
+                        api.firstPaintTime = firstPaint - timing.navigationStart;
+                    } else if (performance.getEntriesByName !== undefined) {
+                        var firstPaintPerformanceEntry = performance.getEntriesByName('first-paint');
+                        if (firstPaintPerformanceEntry.length === 1) {
+                            var firstPaintTime = firstPaintPerformanceEntry[0].startTime;
+                            firstPaint = performance.timeOrigin + firstPaintTime;
+                            api.firstPaintTime = firstPaintTime;
+                        }
+                    }
+                    if (opts && !opts.simple) {
+                        api.firstPaint = firstPaint;
+                    }
+                }
+
+                // Total time from start to load
+                api.loadTime = timing.loadEventEnd - timing.fetchStart;
+                // Time spent constructing the DOM tree
+                api.domReadyTime = timing.domComplete - timing.domInteractive;
+                // Time consumed preparing the new page
+                api.readyStart = timing.fetchStart - timing.navigationStart;
+                // Time spent during redirection
+                api.redirectTime = timing.redirectEnd - timing.redirectStart;
+                // AppCache
+                api.appcacheTime = timing.domainLookupStart - timing.fetchStart;
+                // Time spent unloading documents
+                api.unloadEventTime = timing.unloadEventEnd - timing.unloadEventStart;
+                // DNS query time
+                api.lookupDomainTime = timing.domainLookupEnd - timing.domainLookupStart;
+                // TCP connection time
+                api.connectTime = timing.connectEnd - timing.connectStart;
+                // Time spent during the request
+                api.requestTime = timing.responseEnd - timing.requestStart;
+                // Request to completion of the DOM loading
+                api.initDomTreeTime = timing.domInteractive - timing.responseEnd;
+                // Load event time
+                api.loadEventTime = timing.loadEventEnd - timing.loadEventStart;
+            }
+
+            return api;
+        },
+        /**
+         * Uses console.table() to print a complete table of timing information
+         * @param  Object opts Options (simple (bool) - opts out of full data view)
+         */
+        printTable: function(opts) {
+            var table = {};
+            var data  = this.getTimes(opts) || {};
+            Object.keys(data).sort().forEach(function(k) {
+                table[k] = {
+                    ms: data[k],
+                    s: +((data[k] / 1000).toFixed(2))
+                };
+            });
+            console.table(table);
+        },
+        /**
+         * Uses console.table() to print a summary table of timing information
+         */
+        printSimpleTable: function() {
+            this.printTable({simple: true});
+        }
     };
 
-    __importScript('timing.js');
+    function isNumeric(n) {
+        return !isNaN(parseFloat(n)) && isFinite(n);
+    }
+
+    // 获取资源加载性能数据
+    function getResourceTiming() {
+        const resources = performance.getEntriesByType('resource');
+        return resources.map(resource => ({
+            name: resource.name,
+            entryType: resource.entryType,
+            startTime: resource.startTime,
+            duration: resource.duration,
+            transferSize: resource.transferSize,
+            decodedBodySize: resource.decodedBodySize,
+            encodedBodySize: resource.encodedBodySize,
+            dnsTime: resource.domainLookupEnd - resource.domainLookupStart,
+            tcpTime: resource.connectEnd - resource.connectStart,
+            ttfb: resource.responseStart - resource.requestStart,
+            downloadTime: resource.responseEnd - resource.responseStart
+        }));
+    }
+
+    // 获取核心Web指标
+    function getCoreWebVitals() {
+        return new Promise(resolve => {
+            let webVitals = {};
+            
+            // LCP (Largest Contentful Paint)
+            new PerformanceObserver((entryList) => {
+                const entries = entryList.getEntries();
+                const lastEntry = entries[entries.length - 1];
+                webVitals.lcp = lastEntry.renderTime || lastEntry.loadTime;
+            }).observe({entryTypes: ['largest-contentful-paint']});
+
+            // FID (First Input Delay)
+            new PerformanceObserver((entryList) => {
+                const firstInput = entryList.getEntries()[0];
+                if (firstInput) {
+                    webVitals.fid = firstInput.processingTime;
+                    webVitals.firstInputTime = firstInput.startTime;
+                }
+            }).observe({entryTypes: ['first-input']});
+
+            // CLS (Cumulative Layout Shift)
+            let clsValue = 0;
+            new PerformanceObserver((entryList) => {
+                for (const entry of entryList.getEntries()) {
+                    if (!entry.hadRecentInput) {
+                        clsValue += entry.value;
+                    }
+                }
+                webVitals.cls = clsValue;
+            }).observe({entryTypes: ['layout-shift']});
+
+            setTimeout(() => resolve(webVitals), 3000);
+        });
+    }
+
+    // 获取性能指标
+    function getPerformanceMetrics() {
+        if (window.performance.memory) {
+            return {
+                jsHeapSizeLimit: performance.memory.jsHeapSizeLimit,
+                totalJSHeapSize: performance.memory.totalJSHeapSize,
+                usedJSHeapSize: performance.memory.usedJSHeapSize
+            };
+        }
+        return null;
+    }
+
+    // 监控长任务
+    function observeLongTasks() {
+        const longTasks = [];
+        new PerformanceObserver((list) => {
+            for (const entry of list.getEntries()) {
+                longTasks.push({
+                    duration: entry.duration,
+                    startTime: entry.startTime,
+                    name: entry.name
+                });
+            }
+        }).observe({entryTypes: ['longtask']});
+        return longTasks;
+    }
+
+    // 获取网络信息
+    function getNetworkInfo() {
+        if ('connection' in navigator) {
+            const connection = navigator.connection;
+            return {
+                effectiveType: connection.effectiveType,
+                downlink: connection.downlink,
+                rtt: connection.rtt,
+                saveData: connection.saveData
+            };
+        }
+        return null;
+    }
+
+    // 创建进度提示框
+    function createProgressTip() {
+        const tipContainer = document.createElement('div');
+        tipContainer.id = 'fe-helper-timing-tip';
+        tipContainer.style.cssText = `
+            position: fixed;
+            top: 20px;
+            right: 20px;
+            background: rgba(0, 0, 0, 0.8);
+            color: white;
+            padding: 15px 20px;
+            border-radius: 8px;
+            font-size: 14px;
+            z-index: 999999;
+            box-shadow: 0 2px 10px rgba(0,0,0,0.2);
+            transition: opacity 0.3s;
+        `;
+        document.body.appendChild(tipContainer);
+        return tipContainer;
+    }
+
+    // 更新提示框内容
+    function updateProgressTip(message, progress) {
+        const tipContainer = document.getElementById('fe-helper-timing-tip') || createProgressTip();
+        tipContainer.innerHTML = `
+            <div>${message}</div>
+            ${progress ? `<div style="margin-top:8px;background:rgba(255,255,255,0.2);height:2px;border-radius:1px">
+                <div style="width:${progress}%;height:100%;background:#4CAF50;border-radius:1px"></div>
+            </div>` : ''}
+        `;
+    }
+
+    // 移除提示框
+    function removeProgressTip() {
+        const tipContainer = document.getElementById('fe-helper-timing-tip');
+        if (tipContainer) {
+            tipContainer.style.opacity = '0';
+            setTimeout(() => tipContainer.remove(), 300);
+        }
+    }
 
     window.pagetimingNoPage = function() {
+        updateProgressTip('正在收集页面基础信息...', 20);
 
         let wpoInfo = {
             pageInfo: {
                 title: document.title,
                 url: location.href
             },
-            time: window.timing.getTimes({simple: true})
+            time: window.timing.getTimes({simple: true}),
+            resources: getResourceTiming(),
+            networkInfo: getNetworkInfo(),
+            performanceMetrics: getPerformanceMetrics(),
+            longTasks: []
         };
 
+        updateProgressTip('正在监控页面性能...', 40);
+        // 初始化长任务监控
+        const longTasksMonitor = observeLongTasks();
+
         let sendWpoInfo = function () {
-            chrome.runtime.sendMessage({
-                type: 'fh-dynamic-any-thing',
-                thing: 'set-page-timing-data',
-                wpoInfo: wpoInfo
+            updateProgressTip('正在处理性能数据...', 60);
+            // 合并长任务数据
+            wpoInfo.longTasks = longTasksMonitor;
+
+            // 获取核心Web指标
+            getCoreWebVitals().then(webVitals => {
+                updateProgressTip('正在完成数据采集...', 80);
+                wpoInfo.webVitals = webVitals;
+                
+                chrome.runtime.sendMessage({
+                    type: 'fh-dynamic-any-thing',
+                    thing: 'set-page-timing-data',
+                    wpoInfo: wpoInfo
+                }, () => {
+                    updateProgressTip('数据采集完成!', 100);
+                    setTimeout(removeProgressTip, 1000);
+                });
             });
         };
 
@@ -45,6 +288,7 @@ window.pagetimingContentScript = function () {
             if (wpoInfo.header && wpoInfo.time && wpoInfo.pageInfo) {
                 sendWpoInfo();
             } else {
+                updateProgressTip('正在获取页面请求头信息...', 50);
                 fetch(location.href).then(resp => {
                     let header = {};
                     for (let pair of resp.headers.entries()) {
@@ -54,7 +298,11 @@ window.pagetimingContentScript = function () {
                 }).then(header => {
                     wpoInfo.header = header;
                     sendWpoInfo();
-                }).catch(console.log);
+                }).catch(error => {
+                    console.log(error);
+                    updateProgressTip('获取请求头信息失败,继续其他数据采集...', 50);
+                    sendWpoInfo();
+                });
             }
         };
 
@@ -69,5 +317,4 @@ window.pagetimingContentScript = function () {
 
         detect();
     };
-
 };

+ 391 - 35
apps/page-timing/index.css

@@ -1,35 +1,391 @@
-@import url("../static/css/bootstrap.min.css");
-
-body {
-    font-family: Tahoma, sans-serif;
-    font-size: 10pt;
-    color:#333;
-    margin: 0 auto;
-}
-#loadTotal {
-    background:#f1f1f1;
-    font-weight:bold;
-}
-
-#pageInfo {
-    padding: 10px 0;
-    line-height: 24px;
-    color: #800;
-    font-size: 14px;
-}
-#pageInfo span {
-    color: #888;
-    word-wrap: break-word;
-}
-#pageLoadTime {
-    width:300px;
-}
-#pageLoadTime,#pageHeaderInfo {
-    float: left;
-}
-#pageHeaderInfo {
-    margin-left: 50px;
-}
-h3 {
-    color:#666;
-}
+@import url("../static/css/bootstrap.min.css");
+
+body {
+    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+    font-size: 13px;
+    color: #2c3e50;
+    margin: 0 auto;
+    background-color: #f5f7fa;
+}
+
+.panel-body.mod-json {
+    padding: 15px;
+}
+
+/* 页面基本信息样式 */
+.info-card {
+    background: #fff;
+    border-radius: 8px;
+    box-shadow: 0 1px 3px rgba(0,0,0,0.1);
+    padding: 15px;
+    margin-bottom: 20px;
+}
+
+.info-item {
+    margin-bottom: 8px;
+    display: flex;
+    align-items: center;
+}
+
+.info-item label {
+    color: #666;
+    min-width: 100px;
+    margin-bottom: 0;
+}
+
+.info-item span {
+    color: #2c3e50;
+    word-break: break-all;
+}
+
+.update-time {
+    margin-top: 10px;
+    padding-top: 10px;
+    border-top: 1px solid #eee;
+    color: #999;
+    font-size: 12px;
+    text-align: right;
+}
+
+/* 性能概览区域样式 */
+.performance-overview {
+    margin-bottom: 20px;
+}
+
+.performance-card {
+    background: #fff;
+    border-radius: 8px;
+    box-shadow: 0 1px 3px rgba(0,0,0,0.1);
+    padding: 15px;
+    margin-bottom: 15px;
+}
+
+/* 核心Web指标网格布局 */
+.metrics-grid {
+    display: grid;
+    grid-template-columns: repeat(3, 1fr);
+    gap: 15px;
+    margin-top: 15px;
+}
+
+.metric-item {
+    padding: 15px;
+    border-radius: 6px;
+    text-align: center;
+    background: #f8f9fa;
+    transition: all 0.3s ease;
+}
+
+.metric-item.text-success {
+    background: #d4edda;
+    border: 1px solid #c3e6cb;
+}
+
+.metric-item.text-warning {
+    background: #fff3cd;
+    border: 1px solid #ffeeba;
+}
+
+.metric-item.text-danger {
+    background: #f8d7da;
+    border: 1px solid #f5c6cb;
+}
+
+.metric-value {
+    font-size: 24px;
+    font-weight: 600;
+    margin-bottom: 5px;
+}
+
+.metric-label {
+    font-size: 13px;
+    color: #666;
+    margin-bottom: 5px;
+}
+
+.metric-status {
+    font-size: 12px;
+    font-weight: 500;
+}
+
+/* 页面加载时间和内存使用布局 */
+.flex-container {
+    display: flex;
+    gap: 15px;
+    margin-top: 15px;
+}
+
+.half-width {
+    flex: 1;
+    min-width: 0;
+}
+
+/* 时间列表样式 */
+.timing-list {
+    display: flex;
+    flex-direction: column;
+    gap: 8px;
+}
+
+.timing-item {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 8px;
+    background: #f8f9fa;
+    border-radius: 4px;
+}
+
+.timing-label {
+    color: #666;
+}
+
+.timing-value {
+    font-weight: 500;
+    color: #2c3e50;
+}
+
+/* 内存使用样式 */
+.memory-usage {
+    padding: 10px;
+}
+
+.memory-item {
+    margin-bottom: 10px;
+}
+
+.memory-label {
+    color: #666;
+    margin-bottom: 5px;
+}
+
+.memory-value {
+    font-weight: 500;
+    color: #2c3e50;
+}
+
+.memory-bar {
+    height: 20px;
+    background: #f8f9fa;
+    border-radius: 10px;
+    position: relative;
+    overflow: hidden;
+}
+
+.used-bar {
+    position: absolute;
+    height: 100%;
+    background: #4CAF50;
+    border-radius: 10px;
+    transition: width 0.3s ease;
+}
+
+.total-bar {
+    position: absolute;
+    height: 100%;
+    background: rgba(76, 175, 80, 0.3);
+    border-radius: 10px;
+    transition: width 0.3s ease;
+}
+
+/* 详细信息区域样式 */
+.detail-card {
+    margin-bottom: 20px;
+    border: 1px solid #e0e0e0;
+    border-radius: 8px;
+    overflow: hidden;
+}
+
+.card-header {
+    padding: 15px;
+    background: #f8f9fa;
+    cursor: pointer;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    border-bottom: 1px solid #eee;
+}
+
+.card-header h3 {
+    margin: 0;
+    font-size: 15px;
+    font-weight: 600;
+}
+
+.toggle-icon {
+    font-size: 12px;
+    transition: transform 0.3s ease;
+}
+
+.card-header[aria-expanded="true"] .toggle-icon {
+    transform: rotate(180deg);
+}
+
+.card-content {
+    padding: 15px;
+}
+
+/* 表格样式优化 */
+.table {
+    margin-bottom: 0;
+    font-size: 13px;
+}
+
+.table th {
+    background: #f8f9fa;
+    font-weight: 600;
+    padding: 10px;
+}
+
+.table td {
+    padding: 8px 10px;
+    vertical-align: middle;
+}
+
+/* 响应式布局 */
+@media (max-width: 768px) {
+    .metrics-grid {
+        grid-template-columns: 1fr;
+    }
+
+    .flex-container {
+        flex-direction: column;
+    }
+
+    .half-width {
+        width: 100%;
+    }
+}
+
+/* 滚动条样式 */
+::-webkit-scrollbar {
+    width: 8px;
+    height: 8px;
+}
+
+::-webkit-scrollbar-track {
+    background: #f1f1f1;
+    border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb {
+    background: #c1c1c1;
+    border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+    background: #a8a8a8;
+}
+
+/* 响应式布局 */
+@media screen and (max-width: 1200px) {
+    .wrapper {
+        min-width: 600px;
+    }
+    
+    .table {
+        display: block;
+        overflow-x: auto;
+        -webkit-overflow-scrolling: touch;
+    }
+}
+
+/* 工具提示样式 */
+[title] {
+    cursor: help;
+}
+
+/* 优化建议样式 */
+#optimizationSuggestions {
+    margin: 20px 0;
+}
+
+.performance-section {
+    margin: 25px 0;
+    padding: 20px;
+    background: #fff;
+    border-radius: 10px;
+    box-shadow: 0 2px 6px rgba(0,0,0,0.08);
+}
+
+.performance-section h3 {
+    margin: 0 0 20px;
+    color: #2c3e50;
+    font-size: 18px;
+    font-weight: 600;
+    border-bottom: 2px solid #eee;
+    padding-bottom: 10px;
+}
+
+.suggestion-item {
+    margin-bottom: 20px;
+    padding: 20px;
+    border: 1px solid #e0e0e0;
+    border-radius: 8px;
+    background: #fff;
+    transition: all 0.3s ease;
+}
+
+.suggestion-item:hover {
+    box-shadow: 0 4px 8px rgba(0,0,0,0.1);
+}
+
+.suggestion-category {
+    display: inline-block;
+    padding: 6px 12px;
+    margin-bottom: 12px;
+    border-radius: 4px;
+    font-size: 12px;
+    font-weight: 600;
+    text-transform: uppercase;
+}
+
+.category-warning {
+    background: #fff3e0;
+    color: #f57c00;
+}
+
+.category-critical {
+    background: #ffe0e0;
+    color: #d32f2f;
+}
+
+.category-info {
+    background: #e3f2fd;
+    color: #1976d2;
+}
+
+.suggestion-title {
+    margin: 10px 0;
+    font-size: 16px;
+    font-weight: 600;
+    color: #2c3e50;
+}
+
+.suggestion-description {
+    margin: 12px 0;
+    color: #5c6b7a;
+    line-height: 1.6;
+}
+
+.suggestion-list {
+    margin: 15px 0 5px;
+    padding-left: 20px;
+}
+
+.suggestion-list li {
+    margin: 8px 0;
+    color: #3c4858;
+    line-height: 1.5;
+    position: relative;
+    padding-left: 5px;
+}
+
+.suggestion-list li::before {
+    content: "•";
+    color: #666;
+    position: absolute;
+    left: -15px;
+}
+
+/* 卡片头部样式 */

+ 169 - 34
apps/page-timing/index.html

@@ -13,52 +13,187 @@
         <div class="panel panel-default" style="margin-bottom: 0px;">
             <div class="panel-heading">
                 <h3 class="panel-title">
-                    <a href="https://www.baidufe.com/fehelper/index/index.html" target="_blank" class="x-a-high">
-                        <img src="../static/img/fe-16.png" alt="fehelper"/> FeHelper</a>:页面性能检测</h3>
+                    <a href="https://fehelper.com" target="_blank" class="x-a-high">
+                        <img src="../static/img/fe-16.png" alt="fehelper"/> FeHelper</a>:页面性能检测
+                        <a href="#" class="x-donate-link" @click="openDonateModal($event)"><i class="nav-icon">❤&nbsp;</i>打赏鼓励</a>
+                        <a class="x-other-tools" @click="openOptionsPage($event)"><i class="icon-plus-circle"></i> 探索更多实用工具 <span class="tool-market-badge">工具市场</span></a>
+                    </h3>
             </div>
         </div>
 
         <div class="panel-body mod-json">
+            <!-- 页面基本信息 -->
+            <div id="pageInfo" class="row info-card">
+                <div class="info-item">
+                    <label>被检测的页面:</label>
+                    <span id="pageTitle">{{pageTitle}}</span>
+                </div>
+                <div class="info-item">
+                    <label>页面完整地址:</label>
+                    <span id="pageUrl">{{pageUrl}}</span>
+                </div>
+                <div class="info-item" v-if="networkInfo">
+                    <label>网络状态:</label>
+                    <span>{{networkInfo.effectiveType}} / {{networkInfo.downlink}}Mbps / RTT:{{networkInfo.rtt}}ms</span>
+                </div>
+                <div class="update-time">
+                    <span>数据更新时间:{{new Date().toLocaleString()}}</span>
+                </div>
+            </div>
+            
+            <!-- 网页优化建议 -->
+            <div class="performance-section">
+                <h3>性能优化建议</h3>
+                <div v-for="suggestion in optimizationSuggestions" :key="suggestion.title" class="suggestion-item">
+                    <span :class="['suggestion-category', 'category-' + suggestion.type]">{{suggestion.category}}</span>
+                    <h4 class="suggestion-title">{{suggestion.title}}</h4>
+                    <p class="suggestion-description">{{suggestion.description}}</p>
+                    <ul class="suggestion-list">
+                        <li v-for="item in suggestion.suggestions" :key="item">{{item}}</li>
+                    </ul>
+                </div>
+            </div>
+
+            <!-- 性能概览区域 -->
+            <div class="performance-overview">
+                <!-- 核心Web指标 -->
+                <div id="coreWebVitals" class="row performance-card" v-if="webVitals">
+                    <h3>核心Web指标 (Core Web Vitals)</h3>
+                    <div class="metrics-grid">
+                        <div class="metric-item" :class="getLCPStatus(webVitals.lcp)">
+                            <div class="metric-value">{{Math.ceil(webVitals.lcp)}} ms</div>
+                            <div class="metric-label">最大内容绘制 (LCP)</div>
+                            <div class="metric-status">{{getLCPStatusText(webVitals.lcp)}}</div>
+                        </div>
+                        <div class="metric-item" :class="getFIDStatus(webVitals.fid)">
+                            <div class="metric-value">{{webVitals.fid ? Math.ceil(webVitals.fid) + ' ms' : '未触发'}}</div>
+                            <div class="metric-label">首次输入延迟 (FID)</div>
+                            <div class="metric-status">{{getFIDStatusText(webVitals.fid)}}</div>
+                        </div>
+                        <div class="metric-item" :class="getCLSStatus(webVitals.cls)">
+                            <div class="metric-value">{{webVitals.cls ? webVitals.cls.toFixed(3) : '0'}}</div>
+                            <div class="metric-label">累积布局偏移 (CLS)</div>
+                            <div class="metric-status">{{getCLSStatusText(webVitals.cls)}}</div>
+                        </div>
+                    </div>
+                </div>
 
-            <div id="pageInfo" class="row">
-                被检测的页面:<span id="pageTitle">{{pageTitle}}</span><br>
-                页面完整地址:<span id="pageUrl">{{pageUrl}}</span>
+                <!-- 页面加载时间和内存使用 -->
+                <div class="flex-container">
+                    <div id="pageLoadTime" class="row performance-card half-width">
+                        <h3>页面加载时间</h3>
+                        <div class="timing-list" v-if="timing">
+                            <div class="timing-item" v-for="t in Object.keys(tmDefination)">
+                                <span class="timing-label">{{tmDefination[t]}}</span>
+                                <span class="timing-value">{{Math.ceil(timing[t])}} ms</span>
+                            </div>
+                        </div>
+                    </div>
+
+                    <div id="performanceMetrics" class="row performance-card half-width" v-if="performanceMetrics">
+                        <h3>内存使用情况</h3>
+                        <div class="memory-usage">
+                            <div class="memory-item">
+                                <div class="memory-label">已用/总量/限制</div>
+                                <div class="memory-value">
+                                    {{formatSize(performanceMetrics.usedJSHeapSize)}} / 
+                                    {{formatSize(performanceMetrics.totalJSHeapSize)}} / 
+                                    {{formatSize(performanceMetrics.jsHeapSizeLimit)}}
+                                </div>
+                            </div>
+                            <div class="memory-bar">
+                                <div class="used-bar" :style="{width: (performanceMetrics.usedJSHeapSize / performanceMetrics.jsHeapSizeLimit * 100) + '%'}"></div>
+                                <div class="total-bar" :style="{width: (performanceMetrics.totalJSHeapSize / performanceMetrics.jsHeapSizeLimit * 100) + '%'}"></div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            
+            <!-- HTTP Header信息 -->
+            <div id="pageHeaderInfo" class="row detail-card" v-show="headerInfo">
+                <div class="card-header" @click="toggleSection('headerInfo')">
+                    <h3>HTTP Response Header分析</h3>
+                    <span class="toggle-icon">▼</span>
+                </div>
+                <div class="card-content" v-show="sectionsVisible.headerInfo">
+                    <table class="table table-bordered table-striped table-condensed table-hover">
+                        <thead>
+                            <th>Header</th>
+                            <th>值</th>
+                        </thead>
+                        <tbody>
+                            <tr v-for="(value,key) in headerInfo">
+                                <td>{{key}}</td>
+                                <td>{{value || '-'}}</td>
+                            </tr>
+                        </tbody>
+                    </table>
+                </div>
             </div>
 
-            <div id="pageLoadTime" class="row">
-                <h3>页面各阶段耗时分析</h3>
-                <table class="table table-bordered table-striped table-condensed table-hover">
-                    <thead>
-                        <th >事件</th>
-                        <th >耗时</th>
-                    </thead>
-                    <tbody>
-                        <tr v-if="timing" v-for="t in Object.keys(tmDefination)">
-                            <td>{{tmDefination[t]}}</td>
-                            <td>{{Math.ceil(timing[t])}} ms</td>
-                        </tr>
-                    </tbody>
-                </table>
+            <!-- 长任务监控 -->
+            <div id="longTasks" class="row detail-card" v-if="longTasks && longTasks.length">
+                <div class="card-header" @click="toggleSection('longTasks')">
+                    <h3>长任务监控 (超过50ms的任务)</h3>
+                    <span class="toggle-icon">▼</span>
+                </div>
+                <div class="card-content" v-show="sectionsVisible.longTasks">
+                    <table class="table table-bordered table-striped table-condensed table-hover">
+                        <thead>
+                            <th>开始时间</th>
+                            <th>持续时间</th>
+                            <th>任务名称</th>
+                        </thead>
+                        <tbody>
+                            <tr v-for="task in longTasks">
+                                <td>{{formatTime(task.startTime)}}</td>
+                                <td>{{Math.ceil(task.duration)}} ms</td>
+                                <td>{{task.name}}</td>
+                            </tr>
+                        </tbody>
+                    </table>
+                </div>
             </div>
 
-            <div id="pageHeaderInfo" class="row" v-show="headerInfo">
-                <h3>页面HTTP Response Header分析</h3>
-                <table class="table table-bordered table-striped table-condensed table-hover">
-                    <thead>
-                        <th >网页信息</th>
-                        <th >数据</th>
-                    </thead>
-                    <tbody>
-                        <tr v-for="(value,key) in headerInfo">
-                            <td>{{key}}</td>
-                            <td>{{value || '-'}}</td>
-                        </tr>
-                    </tbody>
-                </table>
+            <!-- 详细信息区域 -->
+            <div class="details-section">
+                <!-- 资源加载性能 -->
+                <div id="resourceTiming" class="row detail-card" v-if="resources && resources.length">
+                    <div class="card-header" @click="toggleSection('resourceTiming')">
+                        <h3>资源加载性能分析</h3>
+                        <span class="toggle-icon">▼</span>
+                    </div>
+                    <div class="card-content" v-show="sectionsVisible.resourceTiming">
+                        <table class="table table-bordered table-striped table-condensed table-hover">
+                            <thead>
+                                <th>资源</th>
+                                <th>类型</th>
+                                <th>大小</th>
+                                <th>总耗时</th>
+                                <th>DNS查询</th>
+                                <th>TCP连接</th>
+                                <th>请求响应</th>
+                                <th>内容下载</th>
+                            </thead>
+                            <tbody>
+                                <tr v-for="resource in resources">
+                                    <td :title="resource.name">{{getFileName(resource.name)}}</td>
+                                    <td>{{resource.entryType}}</td>
+                                    <td>{{formatSize(resource.transferSize)}}</td>
+                                    <td>{{Math.ceil(resource.duration)}} ms</td>
+                                    <td>{{Math.ceil(resource.dnsTime)}} ms</td>
+                                    <td>{{Math.ceil(resource.tcpTime)}} ms</td>
+                                    <td>{{Math.ceil(resource.ttfb)}} ms</td>
+                                    <td>{{Math.ceil(resource.downloadTime)}} ms</td>
+                                </tr>
+                            </tbody>
+                        </table>
+                    </div>
+                </div>
 
             </div>
         </div>
-
     </div>
 
     <script type="text/javascript" src="index.js"></script>

+ 406 - 12
apps/page-timing/index.js

@@ -8,6 +8,18 @@ new Vue({
         pageUrl: '无',
         timing: null,
         headerInfo: null,
+        webVitals: null,
+        resources: null,
+        performanceMetrics: null,
+        longTasks: null,
+        networkInfo: null,
+        // 控制各个部分的显示状态
+        sectionsVisible: {
+            resourceTiming: true,
+            longTasks: true,
+            headerInfo: true,
+            optimization: true
+        },
         tmDefination: {
             lookupDomainTime: 'DNS查询耗时',
             connectTime: 'TCP连接耗时',
@@ -23,35 +35,417 @@ new Vue({
             loadTime: '加载总耗时'
         }
     },
+    computed: {
+        // 获取优化建议
+        optimizationSuggestions() {
+            const suggestions = [];
+            
+            // 性能指标建议
+            if (this.webVitals) {
+                // LCP建议
+                if (this.webVitals.lcp > 2500) {
+                    suggestions.push({
+                        category: '页面加载性能',
+                        type: 'warning',
+                        title: '最大内容绘制(LCP)需要优化',
+                        description: '当前LCP时间为 ' + Math.ceil(this.webVitals.lcp) + 'ms,超过了推荐的2500ms',
+                        suggestions: [
+                            '优化服务器响应时间,考虑使用CDN',
+                            '优化关键渲染路径,减少阻塞资源',
+                            '优化和压缩图片,考虑使用WebP格式',
+                            '实施懒加载策略',
+                            '优化CSS和JavaScript的加载顺序'
+                        ]
+                    });
+                }
+
+                // FID建议
+                if (this.webVitals.fid > 100) {
+                    suggestions.push({
+                        category: '交互响应性能',
+                        type: 'warning',
+                        title: '首次输入延迟(FID)需要改进',
+                        description: '当前FID时间为 ' + Math.ceil(this.webVitals.fid) + 'ms,超过了推荐的100ms',
+                        suggestions: [
+                            '减少主线程工作量,拆分长任务',
+                            '优化JavaScript执行时间',
+                            '延迟加载非关键JavaScript',
+                            '移除未使用的JavaScript代码',
+                            '使用Web Workers处理复杂计算'
+                        ]
+                    });
+                }
+
+                // CLS建议
+                if (this.webVitals.cls > 0.1) {
+                    suggestions.push({
+                        category: '视觉稳定性',
+                        type: 'warning',
+                        title: '累积布局偏移(CLS)需要改进',
+                        description: '当前CLS值为 ' + this.webVitals.cls.toFixed(3) + ',超过了推荐的0.1',
+                        suggestions: [
+                            '为图片和视频元素设置明确的宽高比',
+                            '避免在已存在的内容上方插入内容',
+                            '使用transform动画代替改变位置的动画',
+                            '预留足够的空间给动态加载的内容',
+                            '优化字体加载策略'
+                        ]
+                    });
+                }
+            }
+
+            // 资源优化建议
+            if (this.resources && this.resources.length) {
+                let totalSize = 0;
+                let largeResources = [];
+                let uncompressedResources = [];
+                let longLoadingResources = [];
+
+                this.resources.forEach(resource => {
+                    totalSize += resource.transferSize || 0;
+                    
+                    // 检查大文件
+                    if (resource.transferSize > 500 * 1024) { // 大于500KB
+                        largeResources.push(resource);
+                    }
+                    
+                    // 检查加载时间长的资源
+                    if (resource.duration > 1000) { // 超过1秒
+                        longLoadingResources.push(resource);
+                    }
+                });
+
+                if (totalSize > 2 * 1024 * 1024) { // 总大小超过2MB
+                    suggestions.push({
+                        category: '资源优化',
+                        type: 'warning',
+                        title: '总资源大小过大',
+                        description: '页面总资源大小为 ' + this.formatSize(totalSize),
+                        suggestions: [
+                            '使用代码分割和懒加载',
+                            '优化和压缩图片资源',
+                            '启用Gzip/Brotli压缩',
+                            '使用合适的缓存策略',
+                            '移除未使用的CSS和JavaScript代码'
+                        ]
+                    });
+                }
+
+                if (largeResources.length > 0) {
+                    suggestions.push({
+                        category: '资源优化',
+                        type: 'info',
+                        title: '发现大体积资源文件',
+                        description: '有 ' + largeResources.length + ' 个资源文件大于500KB',
+                        suggestions: largeResources.map(r => 
+                            `优化 ${this.getFileName(r.name)} (${this.formatSize(r.transferSize)})`
+                        )
+                    });
+                }
+
+                if (longLoadingResources.length > 0) {
+                    suggestions.push({
+                        category: '资源加载优化',
+                        type: 'warning',
+                        title: '资源加载时间过长',
+                        description: '有 ' + longLoadingResources.length + ' 个资源加载时间超过1秒',
+                        suggestions: longLoadingResources.map(r =>
+                            `优化 ${this.getFileName(r.name)} (${Math.ceil(r.duration)}ms)`
+                        )
+                    });
+                }
+            }
+
+            // JavaScript性能建议
+            if (this.performanceMetrics) {
+                const usedHeapRatio = this.performanceMetrics.usedJSHeapSize / this.performanceMetrics.jsHeapSizeLimit;
+                if (usedHeapRatio > 0.7) {
+                    suggestions.push({
+                        category: '内存使用优化',
+                        type: 'warning',
+                        title: 'JavaScript内存使用率过高',
+                        description: '当前内存使用率达到 ' + (usedHeapRatio * 100).toFixed(1) + '%',
+                        suggestions: [
+                            '检查内存泄漏问题',
+                            '优化大对象的创建和销毁',
+                            '使用防抖和节流控制频繁操作',
+                            '及时清理不再使用的事件监听器',
+                            '优化闭包使用,避免过度引用'
+                        ]
+                    });
+                }
+            }
+
+            // 长任务优化建议
+            if (this.longTasks && this.longTasks.length > 3) {
+                suggestions.push({
+                    category: '性能优化',
+                    type: 'warning',
+                    title: '检测到多个长任务',
+                    description: '发现 ' + this.longTasks.length + ' 个执行时间超过50ms的任务',
+                    suggestions: [
+                        '将长任务拆分为更小的任务',
+                        '使用Web Workers处理复杂计算',
+                        '优化事件处理函数',
+                        '使用requestAnimationFrame进行视觉更新',
+                        '使用requestIdleCallback处理非关键任务'
+                    ]
+                });
+            }
+
+            // 网络优化建议
+            if (this.networkInfo) {
+                if (this.networkInfo.effectiveType !== '4g') {
+                    suggestions.push({
+                        category: '网络优化',
+                        type: 'info',
+                        title: '检测到非4G网络环境',
+                        description: '当前网络类型: ' + this.networkInfo.effectiveType + ', RTT: ' + this.networkInfo.rtt + 'ms',
+                        suggestions: [
+                            '实施渐进式加载策略',
+                            '优先加载关键资源',
+                            '使用自适应加载',
+                            '考虑使用Service Worker缓存',
+                            '优化资源大小和加载顺序'
+                        ]
+                    });
+                }
+            }
+
+            // HTTP Header优化建议
+            if (this.headerInfo) {
+                const headerSuggestions = [];
+                
+                if (!this.headerInfo['cache-control']) {
+                    headerSuggestions.push('添加Cache-Control头以优化缓存策略');
+                }
+                if (!this.headerInfo['content-encoding']) {
+                    headerSuggestions.push('启用Gzip/Brotli压缩以减少传输大小');
+                }
+                if (!this.headerInfo['x-content-type-options']) {
+                    headerSuggestions.push('添加X-Content-Type-Options头以提高安全性');
+                }
+                if (!this.headerInfo['x-frame-options']) {
+                    headerSuggestions.push('添加X-Frame-Options头以防止点击劫持');
+                }
+
+                if (headerSuggestions.length > 0) {
+                    suggestions.push({
+                        category: 'HTTP优化',
+                        type: 'info',
+                        title: 'HTTP响应头优化建议',
+                        description: '发现 ' + headerSuggestions.length + ' 个HTTP头部优化建议',
+                        suggestions: headerSuggestions
+                    });
+                }
+            }
+
+            return suggestions;
+        }
+    },
     mounted: function () {
+        // 清理过期数据(7天前的数据)
+        this.cleanExpiredData();
+
         // 在tab创建或者更新时候,监听事件,看看是否有参数传递过来
         if (location.protocol === 'chrome-extension:') {
-            chrome.tabs.query({currentWindow: true,active: true, }, (tabs) => {
-                let activeTab = tabs.filter(tab => tab.active)[0];
+            chrome.tabs.query({currentWindow: true, active: true}, (tabs) => {
+                if (!tabs || !tabs.length) {
+                    console.warn('未找到活动标签页');
+                    return;
+                }
+                let activeTab = tabs[0];
                 chrome.runtime.sendMessage({
                     type: 'fh-dynamic-any-thing',
                     thing: 'request-page-content',
                     tabId: activeTab.id
                 }).then(resp => {
-                    console.log(resp)
-                    if(!resp && !resp.content) return ;
-                    sessionStorage.setItem('wpo-data', JSON.stringify(resp.content));
-                    this.showTiming(resp.content);
+                    if (!resp) {
+                        console.warn('未收到响应数据');
+                        return;
+                    }
+                    if (!resp.content) {
+                        console.warn('响应数据中没有content字段');
+                        return;
+                    }
+                    try {
+                        // 保存数据到localStorage,带上时间戳
+                        const storageData = {
+                            timestamp: Date.now(),
+                            data: resp.content
+                        };
+                        localStorage.setItem('wpo-data', JSON.stringify(storageData));
+                        this.showTiming(resp.content);
+                    } catch (e) {
+                        console.error('处理性能数据时出错:', e);
+                    }
+                }).catch(err => {
+                    console.error('获取页面性能数据失败:', err);
                 });
             });
+        } else {
+            try {
+                // 从localStorage读取数据
+                let wpoStorageData = localStorage.getItem('wpo-data');
+                if (wpoStorageData) {
+                    let storage = JSON.parse(wpoStorageData);
+                    this.showTiming(storage.data);
+                }
+            } catch (e) {
+                console.error('读取缓存的性能数据失败:', e);
+            }
         }
 
-        let wpo = JSON.parse(sessionStorage.getItem('wpo-data'));
-        wpo && this.showTiming(wpo);
+        this.loadPatchHotfix();
     },
 
     methods: {
+
+        loadPatchHotfix() {
+            // 页面加载时自动获取并注入页面的补丁
+            chrome.runtime.sendMessage({
+                type: 'fh-dynamic-any-thing',
+                thing: 'fh-get-tool-patch',
+                toolName: 'page-timing'
+            }, patch => {
+                if (patch) {
+                    if (patch.css) {
+                        const style = document.createElement('style');
+                        style.textContent = patch.css;
+                        document.head.appendChild(style);
+                    }
+                    if (patch.js) {
+                        try {
+                            if (window.evalCore && window.evalCore.getEvalInstance) {
+                                window.evalCore.getEvalInstance(window)(patch.js);
+                            }
+                        } catch (e) {
+                            console.error('page-timing补丁JS执行失败', e);
+                        }
+                    }
+                }
+            });
+        },
+
+        // 切换部分的显示/隐藏
+        toggleSection(section) {
+            this.sectionsVisible[section] = !this.sectionsVisible[section];
+            // 更新aria-expanded属性
+            this.$nextTick(() => {
+                const header = document.querySelector(`#${section} .card-header`);
+                if (header) {
+                    header.setAttribute('aria-expanded', this.sectionsVisible[section]);
+                }
+            });
+        },
+
+        // 清理过期数据(7天前的数据)
+        cleanExpiredData() {
+            try {
+                const wpoStorageData = localStorage.getItem('wpo-data');
+                if (wpoStorageData) {
+                    const storage = JSON.parse(wpoStorageData);
+                    const expirationTime = 7 * 24 * 60 * 60 * 1000; // 7天
+                    if (Date.now() - storage.timestamp > expirationTime) {
+                        localStorage.removeItem('wpo-data');
+                    }
+                }
+            } catch (e) {
+                console.error('清理过期数据时出错:', e);
+            }
+        },
+
         showTiming(wpo) {
-            this.pageTitle = wpo.pageInfo.title || "无";
-            this.pageUrl = wpo.pageInfo.url || "无";
+            if (!wpo || typeof wpo !== 'object') {
+                console.warn('性能数据格式不正确');
+                return;
+            }
+
+            try {
+                this.pageTitle = wpo.pageInfo?.title || "无";
+                this.pageUrl = wpo.pageInfo?.url || "无";
+                this.timing = wpo.time || null;
+                this.headerInfo = wpo.header || null;
+                this.webVitals = wpo.webVitals || null;
+                this.resources = wpo.resources || null;
+                this.performanceMetrics = wpo.performanceMetrics || null;
+                this.longTasks = wpo.longTasks || null;
+                this.networkInfo = wpo.networkInfo || null;
+            } catch (e) {
+                console.error('显示性能数据时出错:', e);
+            }
+        },
 
-            this.timing = wpo.time;
-            this.headerInfo = wpo.header;
+        // Core Web Vitals 状态判断
+        getLCPStatus(lcp) {
+            if (!lcp) return '';
+            return lcp <= 2500 ? 'text-success' : lcp <= 4000 ? 'text-warning' : 'text-danger';
+        },
+        getLCPStatusText(lcp) {
+            if (!lcp) return '未知';
+            return lcp <= 2500 ? '良好' : lcp <= 4000 ? '需要改进' : '较差';
+        },
+        getFIDStatus(fid) {
+            if (!fid) return '';
+            return fid <= 100 ? 'text-success' : fid <= 300 ? 'text-warning' : 'text-danger';
+        },
+        getFIDStatusText(fid) {
+            if (!fid) return '未触发';
+            return fid <= 100 ? '良好' : fid <= 300 ? '需要改进' : '较差';
+        },
+        getCLSStatus(cls) {
+            if (!cls) return '';
+            return cls <= 0.1 ? 'text-success' : cls <= 0.25 ? 'text-warning' : 'text-danger';
+        },
+        getCLSStatusText(cls) {
+            if (!cls) return '未知';
+            return cls <= 0.1 ? '良好' : cls <= 0.25 ? '需要改进' : '较差';
         },
+
+        // 工具函数
+        formatSize(bytes) {
+            if (!bytes) return '0 B';
+            const units = ['B', 'KB', 'MB', 'GB'];
+            let i = 0;
+            while (bytes >= 1024 && i < units.length - 1) {
+                bytes /= 1024;
+                i++;
+            }
+            return bytes.toFixed(2) + ' ' + units[i];
+        },
+
+        formatTime(timestamp) {
+            return new Date(timestamp).toLocaleTimeString('zh-CN', {
+                hour12: false,
+                hour: '2-digit',
+                minute: '2-digit',
+                second: '2-digit',
+                fractionalSecondDigits: 3
+            });
+        },
+
+        getFileName(url) {
+            try {
+                return new URL(url).pathname.split('/').pop() || url;
+            } catch (e) {
+                return url;
+            }
+        },
+
+        openDonateModal: function(event) {
+            event.preventDefault();
+            event.stopPropagation();
+            chrome.runtime.sendMessage({
+                type: 'fh-dynamic-any-thing',
+                thing: 'open-donate-modal',
+                params: { toolName: 'page-timing' }
+            });
+        },
+
+        openOptionsPage: function(event) {
+            event.preventDefault();
+            event.stopPropagation();
+            chrome.runtime.openOptionsPage();
+        }   
     }
 });

+ 0 - 123
apps/page-timing/timing.js

@@ -1,123 +0,0 @@
-/**
- * Timing.js 1.2.0
- * Copyright 2016 Addy Osmani
- * @ref https://github.com/addyosmani/timing.js/blob/master/timing.js
- */
-(function(window) {
-    'use strict';
-
-    /**
-     * Navigation Timing API helpers
-     * timing.getTimes();
-     **/
-    window.timing = window.timing || {
-        /**
-         * Outputs extended measurements using Navigation Timing API
-         * @param  Object opts Options (simple (bool) - opts out of full data view)
-         * @return Object      measurements
-         */
-        getTimes: function(opts) {
-            var performance = window.performance || window.webkitPerformance || window.msPerformance || window.mozPerformance;
-
-            if (performance === undefined) {
-                return false;
-            }
-
-            var timing = performance.timing;
-            var api = {};
-            opts = opts || {};
-
-            if (timing) {
-                if(opts && !opts.simple) {
-                    for (var k in timing) {
-                        // hasOwnProperty does not work because properties are
-                        // added by modifying the object prototype
-                        if(isNumeric(timing[k])) {
-                            api[k] = parseFloat(timing[k]);
-                        }
-                    }
-                }
-
-
-                // Time to first paint
-                if (api.firstPaint === undefined) {
-                    // All times are relative times to the start time within the
-                    // same objects
-                    var firstPaint = 0;
-
-                    // IE
-                    if (typeof timing.msFirstPaint === 'number') {
-                        firstPaint = timing.msFirstPaint;
-                        api.firstPaintTime = firstPaint - timing.navigationStart;
-                    } else if (performance.getEntriesByName !== undefined) {
-                        var firstPaintPerformanceEntry = performance.getEntriesByName('first-paint');
-                        if (firstPaintPerformanceEntry.length === 1) {
-                            var firstPaintTime = firstPaintPerformanceEntry[0].startTime;
-                            firstPaint = performance.timeOrigin + firstPaintTime;
-                            api.firstPaintTime = firstPaintTime;
-                        }
-                    }
-                    if (opts && !opts.simple) {
-                        api.firstPaint = firstPaint;
-                    }
-                }
-
-                // Total time from start to load
-                api.loadTime = timing.loadEventEnd - timing.fetchStart;
-                // Time spent constructing the DOM tree
-                api.domReadyTime = timing.domComplete - timing.domInteractive;
-                // Time consumed preparing the new page
-                api.readyStart = timing.fetchStart - timing.navigationStart;
-                // Time spent during redirection
-                api.redirectTime = timing.redirectEnd - timing.redirectStart;
-                // AppCache
-                api.appcacheTime = timing.domainLookupStart - timing.fetchStart;
-                // Time spent unloading documents
-                api.unloadEventTime = timing.unloadEventEnd - timing.unloadEventStart;
-                // DNS query time
-                api.lookupDomainTime = timing.domainLookupEnd - timing.domainLookupStart;
-                // TCP connection time
-                api.connectTime = timing.connectEnd - timing.connectStart;
-                // Time spent during the request
-                api.requestTime = timing.responseEnd - timing.requestStart;
-                // Request to completion of the DOM loading
-                api.initDomTreeTime = timing.domInteractive - timing.responseEnd;
-                // Load event time
-                api.loadEventTime = timing.loadEventEnd - timing.loadEventStart;
-            }
-
-            return api;
-        },
-        /**
-         * Uses console.table() to print a complete table of timing information
-         * @param  Object opts Options (simple (bool) - opts out of full data view)
-         */
-        printTable: function(opts) {
-            var table = {};
-            var data  = this.getTimes(opts) || {};
-            Object.keys(data).sort().forEach(function(k) {
-                table[k] = {
-                    ms: data[k],
-                    s: +((data[k] / 1000).toFixed(2))
-                };
-            });
-            console.table(table);
-        },
-        /**
-         * Uses console.table() to print a summary table of timing information
-         */
-        printSimpleTable: function() {
-            this.printTable({simple: true});
-        }
-    };
-
-    function isNumeric(n) {
-        return !isNaN(parseFloat(n)) && isFinite(n);
-    }
-
-    // Expose as a commonjs module
-    if (typeof module !== 'undefined' && module.exports) {
-        module.exports = window.timing;
-    }
-
-})(typeof window !== 'undefined' ? window : {});

+ 115 - 0
apps/password/index.css

@@ -29,3 +29,118 @@
 #btnCopy {
     margin-left:20px;
 }
+
+/* 密码生成工具美化样式 */
+.password-card {
+    background: #fff;
+    border-radius: 16px;
+    box-shadow: 0 4px 24px rgba(0,0,0,0.10), 0 1.5px 4px rgba(0,0,0,0.08);
+    padding: 32px 24px 24px 24px;
+    max-width: 980px;
+    margin: 40px auto 0 auto;
+    font-family: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', Arial, sans-serif;
+}
+.password-card .checkbox {
+    margin-bottom: 18px;
+    font-size: 16px;
+    padding-left: 0;
+    margin-left: 0;
+    border-bottom: 1px dashed #ccc;
+    margin-left: 20px;
+}
+.password-card .checkbox.no-border {
+    border: none;
+}
+.password-card .checkbox input[type=checkbox] {
+    accent-color: #0078d7;
+    width: 18px;
+    height: 18px;
+    margin-right: 8px;
+    vertical-align: middle;
+    position: relative;
+    top: -4px;
+}
+.password-card .checkbox label {
+    cursor: pointer;
+    font-weight: 500;
+}
+.password-card input[type=number] {
+    width: 80px;
+    margin-left: 8px;
+    border-radius: 6px;
+    border: 1px solid #e0e0e0;
+    padding: 4px 8px;
+    font-size: 15px;
+}
+.password-card .btn-group {
+    display: flex;
+    gap: 0;
+    margin-top: 12px;
+}
+.password-card .btn-group .main-btn {
+    margin-right: 20px;
+}
+.password-card .main-btn {
+    border-radius: 8px;
+}
+.password-card .main-btn {
+    min-width: 120px;
+    font-size: 16px;
+    border-radius: 8px;
+    box-shadow: 0 2px 8px rgba(0,120,215,0.08);
+    transition: background 0.2s, box-shadow 0.2s;
+    font-weight: 600;
+    padding: 8px 0;
+}
+.password-card .btn-warning.main-btn {
+    background: linear-gradient(90deg, #ffb300 0%, #ff9800 100%);
+    color: #fff;
+    border: none;
+}
+.password-card .btn-success.main-btn {
+    background: linear-gradient(90deg, #0078d7 0%, #00c6fb 100%);
+    color: #fff;
+    border: none;
+}
+.password-card .main-btn:hover {
+    filter: brightness(1.08);
+    box-shadow: 0 4px 16px rgba(0,120,215,0.12);
+}
+.result-area {
+    margin-top: 28px;
+    background: #f6f8fa;
+    border-radius: 12px;
+    padding: 18px 16px;
+    box-shadow: 0 2px 8px rgba(0,0,0,0.04);
+}
+.result-area textarea {
+    width: 100%;
+    min-height: 60px;
+    font-size: 20px;
+    color: #0078d7;
+    background: transparent;
+    border: none;
+    resize: none;
+    font-family: 'Fira Mono', 'Consolas', 'Menlo', monospace;
+    font-weight: bold;
+    outline: none;
+}
+#pageContainer>.panel-body {
+    margin: 20px auto;
+}
+
+.fh-toast {
+    position: fixed;
+    left: 50%;
+    top: 80px;
+    transform: translateX(-50%);
+    background: rgba(0,0,0,0.85);
+    color: #fff;
+    padding: 12px 32px;
+    border-radius: 8px;
+    font-size: 16px;
+    z-index: 9999;
+    box-shadow: 0 2px 8px rgba(0,0,0,0.18);
+    pointer-events: none;
+    transition: opacity 0.3s;
+}

+ 11 - 7
apps/password/index.html

@@ -2,7 +2,7 @@
 <html lang="zh-CN">
 
 <head>
-    <title>随机密码生成器</title>
+    <title>密码生成工具</title>
     <meta charset="UTF-8">
     <link rel="shortcut icon" href="../static/img/favicon.ico">
     <link rel="stylesheet" href="index.css" />
@@ -16,12 +16,14 @@
         <div class="panel panel-default" style="margin-bottom: 0px;">
             <div class="panel-heading">
                 <h3 class="panel-title">
-                    <a href="https://www.baidufe.com/fehelper/index/index.html" target="_blank" class="x-a-high">
+                    <a href="https://fehelper.com" target="_blank" class="x-a-high">
                         <img src="../static/img/fe-16.png" alt="fehelper" /> FeHelper</a>:随机密码生成器
+                    <a href="#" class="x-donate-link" @click="openDonateModal($event)"><i class="nav-icon">❤&nbsp;</i>打赏鼓励</a>
+                    <a class="x-other-tools" @click="openOptionsPage($event)"><i class="icon-plus-circle"></i> 探索更多实用工具 <span class="tool-market-badge">工具市场</span></a>
                 </h3>
             </div>
         </div>
-        <div class="panel-body mod-password">
+        <div class="panel-body mod-password password-card">
 
             <div class="row">
                 <div class="checkbox">
@@ -40,17 +42,19 @@
                     <label>密码长度:<input class="form-control" type="number" min="0" v-model="length" @click="convert()"></label>
                 </div>
 
-                <div class="checkbox no-border">
-                    <input id="btnCopy" class="btn btn-warning" type="button" value="复制" @click="copyResult()" v-show="resultContent.length > 0">
-                    <input id="btnCodeChange" class="btn btn-success ui-fl-l" type="button" value="生成随机密码" @click="convert()">
+                <div class="checkbox ">
+                    <input id="btnCodeChange" class="btn btn-success main-btn" type="button" value="生成随机密码" @click="convert()">
+                    <input id="btnCopy" class="btn btn-warning main-btn" type="button" value="复制" @click="copyResult()" v-show="resultContent.length > 0">
                 </div>
             </div>
 
-            <div id="rst" class="row" v-show="resultContent.length > 0">
+            <div id="rst" class="row result-area" v-show="resultContent.length > 0">
                 <textarea class="form-control mod-textarea" id="rstCode" ref="rstCode" readonly v-model="resultContent" @mouseover="getResult()"></textarea>
             </div>
         </div>
+        <div v-show="showToast" class="fh-toast">{{ toastMsg }}</div>
     </div>
+
     <script type="text/javascript" src="index.js"></script>
 
     <script src="../static/vendor/jquery/jquery-3.3.1.min.js"></script>

+ 58 - 2
apps/password/index.js

@@ -15,7 +15,13 @@ new Vue({
             upperLetter: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
             specialChar: '~!@#$%^&*()[{]}-_=+\|;:\'\",<.>/?`'
         },
-        resultContent: ''
+        resultContent: '',
+        showToast: false,
+        toastMsg: ''
+    },
+
+    mounted: function () {
+        this.loadPatchHotfix();
     },
 
     methods: {
@@ -50,12 +56,62 @@ new Vue({
 
             if ('clipboard' in navigator) {
                 navigator.clipboard.writeText(this.resultContent)
+                .then(() => {
+                    this.showToastMsg('复制成功!');
+                })
                 .catch(err => {
                     console.error('复制失败: ', err);
                 });
             }else{
                 alert("您的浏览器不支持 clipboard API, 请手动复制")
             }
-        }
+        },
+        showToastMsg: function(msg) {
+            this.toastMsg = msg;
+            this.showToast = true;
+            setTimeout(() => {
+                this.showToast = false;
+            }, 1500);
+        },
+        openOptionsPage: function(event) {
+            event.preventDefault();
+            event.stopPropagation();
+            chrome.runtime.openOptionsPage();
+        },
+        openDonateModal: function(event) {
+            event.preventDefault();
+            event.stopPropagation();
+            chrome.runtime.sendMessage({
+                type: 'fh-dynamic-any-thing',
+                thing: 'open-donate-modal',
+                params: { toolName: 'password' }
+            });
+        },
+        
+        loadPatchHotfix() {
+            // 页面加载时自动获取并注入页面的补丁
+            chrome.runtime.sendMessage({
+                type: 'fh-dynamic-any-thing',
+                thing: 'fh-get-tool-patch',
+                toolName: 'password'
+            }, patch => {
+                if (patch) {
+                    if (patch.css) {
+                        const style = document.createElement('style');
+                        style.textContent = patch.css;
+                        document.head.appendChild(style);
+                    }
+                    if (patch.js) {
+                        try {
+                            if (window.evalCore && window.evalCore.getEvalInstance) {
+                                window.evalCore.getEvalInstance(window)(patch.js);
+                            }
+                        } catch (e) {
+                            console.error('password补丁JS执行失败', e);
+                        }
+                    }
+                }
+            });
+        },
     }
 });

+ 234 - 33
apps/popup/index.css

@@ -1,12 +1,20 @@
+html, body {
+    margin: 0;
+    padding: 0;
+}
 
 .fe-whole-page {
+    width: 152px;
+    min-height: 140px;
     font-size: 14px;
     user-select: none;
     padding: 0;
     margin: 0;
-    width: 152px;
     position: relative;
+    display: flex;
+    flex-direction: column;
     overflow: hidden;
+    transition: min-height 0.2s ease-out;
 }
 
 .fe-hide {
@@ -14,38 +22,83 @@
 }
 
 .fe-function-title {
-    padding: 5px;
-    color: #522;
-    font-weight: bolder;
+    padding: 8px 12px;
+    color: #2b2d42;
+    font-weight: 600;
     cursor: default;
-    border-bottom: 1px dotted #aaa;
-    text-align: center;
+    border-bottom: 1px solid rgba(67, 97, 238, 0.1);
+    background: linear-gradient(to right, #f8f9ff, #f0f2ff);
     font-size: 12px;
+    letter-spacing: 0.5px;
+    flex-shrink: 0; /* 防止被压缩 */
 }
 
 .fe-function-title span {
     font-weight: normal;
-    color: #e1e1e1;
+    color: #999;
+    font-size: 10px;
 }
 
 ul.fe-function-list {
-    padding: 0 10px;
+    padding: 0;
     list-style: none;
-    width: 130px;
+    width: 100%;
+    margin: 0;
+    flex: 1;
+    min-height: 40px;
+    overflow-y: auto; /* 允许垂直滚动 */
+    overflow-x: hidden; /* 隐藏水平滚动 */
+    max-height: 500px; /* 不再限制最大高度 */
+    display: inline-table;
+    scrollbar-width: thin; /* Firefox 下的滚动条样式 */
+    scrollbar-color: rgba(67, 97, 238, 0.3) transparent;
+}
+
+/* 自定义滚动条样式 - Webkit内核浏览器 */
+ul.fe-function-list::-webkit-scrollbar {
+    width: 6px;
+}
+
+ul.fe-function-list::-webkit-scrollbar-track {
+    background: transparent;
+}
+
+ul.fe-function-list::-webkit-scrollbar-thumb {
+    background: rgba(67, 97, 238, 0.3);
+    border-radius: 3px;
+    transition: background 0.2s ease;
+}
+
+ul.fe-function-list::-webkit-scrollbar-thumb:hover {
+    background: rgba(67, 97, 238, 0.5);
+}
+
+/* 暗黑模式下的滚动条 */
+html[dark-mode="on"] ul.fe-function-list::-webkit-scrollbar-thumb {
+    background: rgba(255, 255, 255, 0.3);
+}
+
+html[dark-mode="on"] ul.fe-function-list::-webkit-scrollbar-thumb:hover {
+    background: rgba(255, 255, 255, 0.5);
 }
 
 ul.fe-function-list li {
-    padding: 3px 5px 3px 5px;
+    padding: 2px 10px 2px 38px;
     cursor: pointer;
-    color: #555;
-    -webkit-transition: all .4s ease;
-    text-align: justify;
+    color: #4a4c6d;
+    /* 只对安全的属性进行过渡,避免布局变化 */
+    transition: background-color 0.2s ease, color 0.2s ease, box-shadow 0.2s ease;
     display: block;
-    height: 20px;
-    line-height: 20px;
-    width: 120px;
-    border-bottom: 1px dashed #e5e5e5;
-    overflow: hidden;
+    align-items: center;
+    height: 28px; /* 固定高度,不变化 */
+    border-radius: 6px;
+    margin: 2px 0; /* 固定边距,不变化 */
+    border-bottom: 1px solid rgba(67, 97, 238, 0.05);
+    text-align: justify;
+    position: relative;
+    line-height: 28px; /* 固定行高,不变化 */
+    flex-shrink: 0; /* 防止被压缩 */
+    box-sizing: border-box; /* 确保padding不影响总宽度 */
 }
 
 ul.fe-function-list li:last-child {
@@ -54,8 +107,11 @@ ul.fe-function-list li:last-child {
 
 ul.fe-function-list li.x-hovered,
 ul.fe-function-list li:hover {
-    color: #ff3C11;
-    background: #f1f1f1;
+    color: #000;
+    background: rgba(67, 97, 238, 0.08);
+    box-shadow: 0 2px 6px rgba(67, 97, 238, 0.08);
+    /* 保持字体粗细不变,避免文字宽度变化导致布局抖动 */
+    font-weight: 500;
 }
 
 ul.fe-function-list li > span {
@@ -71,15 +127,33 @@ ul.fe-function-list li:after {
 }
 
 ul.fe-function-list li > b {
-    width: 14px;
-    height: 14px;
-    -webkit-transition: all .5s ease;
-    display: inline-block;
-    margin-right: 5px;
-    color:#48f;
+    width: 20px;
+    height: 20px;
+    /* 只对transform进行过渡,避免其他属性变化影响布局 */
+    transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+    background: rgba(0,0,0,0.1);
+    padding: 0px;
+    border-radius: 50%;
+    margin-right: 2px;
+    color: currentColor;
+    filter: drop-shadow(0 2px 4px rgba(67, 97, 238, 0.1));
+    position: absolute;
+    left: 10px;
+    top: 6px;
+    /* 确保图标的变换不影响文档流 */
+    transform-origin: center center;
+}
+
+/* 暗黑模式适配 */
+.dark-mode .fe-function-list li > b {
+    background: rgba(255,255,255,0.1);
 }
 
 ul.fe-function-list li:hover > b {
+    transform: rotate(360deg);
     -webkit-transform: rotate(360deg);
 }
 
@@ -90,15 +164,21 @@ ul.fe-function-list li i {
 }
 
 .fe-feedback {
-    font-size: 12px;
-    border-top: 1px dotted #ddd;
-    padding: 5px 15px;
-    line-height: 20px;
-    color: #888;
+    font-size: 13px;
+    border-top: 1px solid rgba(67, 97, 238, 0.08);
+    padding: 8px 16px;
+    background: linear-gradient(to right, rgba(67, 97, 238, 0.05), rgba(67, 97, 238, 0.05));
+    color: var(--text-color);
+    opacity: 0.9;
+    flex-shrink: 0; /* 防止被压缩 */
+    position: relative; /* 改为相对定位 */
+    width: 100%;
+    box-sizing: border-box;
+    margin-top: auto; /* 确保在容器底部 */
 }
 
 .fe-feedback a {
-    color: #888;
+    color: #666;
     text-decoration: none;
     text-align: left;
 }
@@ -112,9 +192,10 @@ svg:not(:root) {
 }
 
 .fe-feedback svg {
-    vertical-align: text-bottom;
+    vertical-align: middle;
     display: inline-block;
     fill: currentColor;
+    margin-right: 3px;
 }
 
 .fe-feedback img {
@@ -130,6 +211,9 @@ svg:not(:root) {
     float: right;
     cursor: pointer;
     margin-left: 5px;
+    display: inline-flex;
+    align-items: center;
+    color: #666;
 }
 
 .fe-feedback .x-settings:hover {
@@ -140,3 +224,120 @@ svg:not(:root) {
 {
     float: right;
 }
+
+/* 加载状态样式 */
+.fe-loading {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    padding: 20px;
+    min-height: 60px;
+    flex: 1;
+}
+
+.loading-spinner {
+    width: 20px;
+    height: 20px;
+    border: 2px solid rgba(67, 97, 238, 0.1);
+    border-left: 2px solid #4361ee;
+    border-radius: 50%;
+    animation: spin 1s linear infinite;
+    margin-bottom: 8px;
+}
+
+.loading-text {
+    color: #666;
+    font-size: 12px;
+    text-align: center;
+}
+
+@keyframes spin {
+    0% { transform: rotate(0deg); }
+    100% { transform: rotate(360deg); }
+}
+
+/* 无工具状态样式 */
+.fe-no-tools {
+    padding: 20px;
+    text-align: center;
+    flex: 1;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+
+.no-tools-message p {
+    color: #666;
+    font-size: 12px;
+    margin: 0 0 12px 0;
+}
+
+.install-tools-btn {
+    background: #4361ee;
+    color: white;
+    border: none;
+    border-radius: 4px;
+    padding: 6px 12px;
+    font-size: 11px;
+    cursor: pointer;
+    transition: all 0.2s ease;
+}
+
+.install-tools-btn:hover {
+    background: #3651d4;
+    transform: translateY(-1px);
+}
+
+/* 暗色模式适配 */
+html[dark-mode="on"] .loading-text,
+html[dark-mode="on"] .no-tools-message p {
+    color: #ccc;
+}
+
+html[dark-mode="on"] .loading-spinner {
+    border-color: rgba(255, 255, 255, 0.1);
+    border-left-color: #4361ee;
+}
+
+
+/* 工具列表为空时的基本样式 */
+.fe-function-list:empty {
+    min-height: 20px;
+}
+
+/* 1个工具时的优化 */
+.fe-function-list li:only-child {
+    margin: 4px 0;
+    height: 32px;
+    line-height: 32px;
+}
+
+.fe-function-list li:only-child > b {
+    top: 6px;
+}
+
+/* 当只有极少工具时的特殊优化 */
+.very-few-tools .fe-function-list {
+    padding: 6px 0;
+}
+
+.very-few-tools .fe-function-list li {
+    height: 34px;
+    line-height: 34px;
+    margin: 4px 0;
+}
+
+
+/* 当没有工具时,优化无工具提示的显示 */
+.fe-no-tools .no-tools-message {
+    text-align: center;
+}
+
+/* 优化加载状态的显示 */
+.fe-loading {
+    justify-content: center;
+    align-items: center;
+    display: flex;
+    height: 120px;
+}

+ 27 - 17
apps/popup/index.html

@@ -7,32 +7,42 @@
 		<script type="text/javascript" src="../static/vendor/evalCore.min.js"></script>
 		<script type="text/javascript" src="../static/vendor/vue/vue.js"></script>
     </head>
-    <body class="fe-whole-page">
-        <div id="pageContainer">
+    <body>
+        <div id="pageContainer" class="fe-whole-page" :class="getLayoutClasses()">
             <div class="fe-function-title">FeHelper<span>({{manifest.version}})</span></div>
-            <ul class="fe-function-list">
+            
+            <!-- 加载状态 -->
+            <div v-if="isLoading" class="fe-loading">
+                <div class="loading-spinner"></div>
+                <div class="loading-text">加载工具列表中...</div>
+            </div>
+            
+            <!-- 工具列表 -->
+            <ul v-else-if="installedToolsCount > 0" class="fe-function-list" :class="{'few-tools': installedToolsCount <= 3}">
                 <li v-for="tool in Object.keys(fhTools)" :class="'-x-' + tool" @click="runHelper(tool)" v-if="fhTools[tool].installed">
                     <b>{{ fhTools[tool].icon || fhTools[tool].menuConfig[0].icon}}</b>{{fhTools[tool].name}}
                 </li>
             </ul>
+            
+            <!-- 如果没有工具 -->
+            <div v-else-if="!isLoading" class="fe-no-tools">
+                <div class="no-tools-message">
+                    <p>暂无已安装的工具</p>
+                    <button @click="openOptionsPage()" class="install-tools-btn">去安装工具</button>
+                </div>
+            </div>
+            
             <div class="fe-feedback">
-
-                <a href="https://github.com/zxlie/FeHelper" @click="openUrl($event)" target="_blank" tabindex="-1" class="x-github" title="访问Github">
-                    <svg height="16" class="octicon octicon-mark-github" viewBox="0 0 16 16" version="1.1" width="16" aria-hidden="true">
-                        <path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"></path>
-                    </svg>
-                </a>
-
-                <span class="x-settings" @click="openOptionsPage()" title="FeHelper配置项">
-                    <svg aria-hidden="true" height="16" version="1.1" viewBox="0 0 14 16" width="14">
-                        <path fill-rule="evenodd" d="M14 8.77v-1.6l-1.94-.64-.45-1.09.88-1.84-1.13-1.13-1.81.91-1.09-.45-.69-1.92h-1.6l-.63 1.94-1.11.45-1.84-.88-1.13 1.13.91 1.81-.45 1.09L0 7.23v1.59l1.94.64.45 1.09-.88 1.84 1.13 1.13 1.81-.91 1.09.45.69 1.92h1.59l.63-1.94 1.11-.45 1.84.88 1.13-1.13-.92-1.81.47-1.09L14 8.75v.02zM7 11c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3z"></path>
-                    </svg>设置</span>
-
-                <a href="https://github.com/zxlie/FeHelper/issues" @click="openUrl($event)" target="_blank" tabindex="-1" class="x-fb" title="提交意见反馈">
+                <a href="https://github.com/zxlie/FeHelper/issues" @click="openUrl($event)" target="_blank" tabindex="-1" class="x-github" title="提交意见反馈">
                     <svg version="1.1" width="14" height="14" viewBox="0 0 426.667 426.667" style="enable-background:new 0 0 426.667 426.667;" xml:space="preserve">
                         <path d="M384,0H42.667C19.093,0,0.213,19.093,0.213,42.667L0,426.667l85.333-85.333H384c23.573,0,42.667-19.093,42.667-42.667v-256C426.667,19.093,407.573,0,384,0z M234.667,256H192v-42.667h42.667V256z M234.667,170.667H192V85.333h42.667V170.667z"/>
                     </svg>反馈
-                </a>
+                </a> 
+
+                <span class="x-settings" @click="openOptionsPage()" title="FeHelper配置">
+                    <svg aria-hidden="true" height="16" version="1.1" viewBox="0 0 14 16" width="14">
+                        <path fill="currentColor" d="M12 2H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2zm-1 9H5V9h6v2zm0-4H5V5h6v2z"/>
+                    </svg>更多</span>
             </div>
         </div>
         <script src="../static/js/dark-mode.js"></script>

+ 237 - 55
apps/popup/index.js

@@ -5,84 +5,243 @@
 import Awesome from '../background/awesome.js'
 import MSG_TYPE from '../static/js/common.js';
 
+function triggerScreenshot() {
+    chrome.tabs.query({active: true, currentWindow: true}, tabs => {
+        if (!tabs || !tabs.length || !tabs[0].id) return;
+        
+        const tabId = tabs[0].id;
+        
+        // 先尝试直接发送消息给content script
+        chrome.tabs.sendMessage(tabId, {
+            type: 'fh-screenshot-start'
+        }).then(response => {
+            console.log('截图工具触发成功');
+            window.close();
+        }).catch(error => {
+            console.log('无法直接触发截图工具,尝试使用noPage模式', error);
+            // 如果发送消息失败,使用noPage模式
+            chrome.runtime.sendMessage({
+                type: 'fh-dynamic-any-thing',
+                thing: 'trigger-screenshot',
+                tabId: tabId
+            });
+            window.close();
+        });
+    });
+}
+
 new Vue({
     el: '#pageContainer',
     data: {
         manifest: {},
-        fhTools: {}
+        fhTools: {},
+        isLoading: true
+    },
+
+    computed: {
+        // 计算已安装的工具数量
+        installedToolsCount() {
+            return Object.values(this.fhTools).filter(tool => tool.installed).length;
+        }
     },
 
     created: function () {
         // 获取当前ctx的version
         this.manifest = chrome.runtime.getManifest();
+        
+        // 立即开始加载工具列表,不阻塞页面渲染
+        this.loadTools();
 
-        Awesome.getInstalledTools().then(tools => {
-            this.fhTools = tools;
-        });
-
-        // 自动开关灯
-        DarkModeMgr.turnLightAuto();
+        // 页面加载时自动获取并注入popup页面的补丁
+        this.loadPatchHotfix();
     },
 
     mounted: function () {
+        // 页面DOM渲染完成后,执行非关键操作
+        this.$nextTick(() => {
+            // 延迟执行非关键操作,避免阻塞UI渲染
+            setTimeout(() => {
+                // 自动开关灯
+                if (typeof DarkModeMgr !== 'undefined') {
+                    DarkModeMgr.turnLightAuto();
+                }
 
-        // 整个popup窗口支持上线选择
-        document.body.addEventListener('keydown', e => {
-            let keyCode = e.keyCode || e.which;
-            if (![38, 40, 13].includes(keyCode)) {
-                return false;
-            }
-            let ul = document.querySelector('#pageContainer ul');
-            let hovered = ul.querySelector('li.x-hovered');
-            let next, prev;
-            if (hovered) {
-                hovered.classList.remove('x-hovered');
-                next = hovered.nextElementSibling;
-                prev = hovered.previousElementSibling;
-            }
-            if (!next) {
-                next = ul.querySelector('li:first-child');
-            }
-            if (!prev) {
-                prev = ul.querySelector('li:last-child');
-            }
+                // 记录工具使用(非关键操作)
+                this.recordUsage();
 
-            switch (keyCode) {
-                case 38: // 方向键:↑
-                    prev.classList.add('x-hovered');
-                    break;
-                case 40: // 方向键:↓
-                    next.classList.add('x-hovered');
-                    break;
-                case 13: // 回车键:选择
-                    hovered.click();
-            }
+                // 页面加载后自动采集(非关键操作)
+                if (window.chrome && chrome.runtime && chrome.runtime.sendMessage) {
+                    Awesome.collectAndSendClientInfo();
+                }
+            }, 50); // 延迟50ms执行,让UI先渲染
+        });
 
-        }, false);
+        // 整个popup窗口支持上下键选择
+        this.setupKeyboardNavigation();
+        
+        // 查找截图按钮并绑定事件
+        this.setupScreenshotButton();
     },
 
     methods: {
 
-        runHelper: async function (toolName) {
-            // 如果是aiagent工具,我们就用sidePanel打开
+        loadPatchHotfix() {
+            // 页面加载时自动获取并注入options页面的补丁
+            chrome.runtime.sendMessage({
+                type: 'fh-dynamic-any-thing',
+                thing: 'fh-get-tool-patch',
+                toolName: 'popup'
+            }, patch => {
+                if (patch) {
+                    if (patch.css) {
+                        const style = document.createElement('style');
+                        style.textContent = patch.css;
+                        document.head.appendChild(style);
+                    }
+                    if (patch.js) {
+                        try {
+                            if (window.evalCore && window.evalCore.getEvalInstance) {
+                                window.evalCore.getEvalInstance(window)(patch.js);
+                            }
+                        } catch (e) {
+                            console.error('popup补丁JS执行失败', e);
+                        }
+                    }
+                }
+            });
+        },
+
+        getLayoutClasses() {
+            const installedCount = this.installedToolsCount;
+            const classes = [];
             
-            if(toolName === 'aiagent'){ 
-                const [tab] = await chrome.tabs.query({
-                    active: true,
-                    lastFocusedWindow: true
-                  });
-                  
-                const tabId = tab.id;
-                await chrome.sidePanel.setOptions({
-                  tabId,
-                  path: '/aiagent/index.html',
-                  enabled: true
+            if (installedCount <= 1) {
+                classes.push('very-few-tools');
+            } else if (installedCount <= 3) {
+                classes.push('few-tools');
+            }
+            
+            return classes;
+        },
+
+        async loadTools() {
+            try {
+                const tools = await Awesome.getInstalledTools();
+                
+                // 获取用户自定义的工具排序
+                const customOrder = await chrome.storage.local.get('tool_custom_order');
+                const savedOrder = customOrder.tool_custom_order ? JSON.parse(customOrder.tool_custom_order) : null;
+                
+                // 如果有自定义排序,重新排列工具
+                if (savedOrder && Array.isArray(savedOrder)) {
+                    const orderedTools = {};
+                    const unorderedTools = { ...tools };
+                    
+                    // 按照保存的顺序添加工具
+                    savedOrder.forEach(toolKey => {
+                        if (unorderedTools[toolKey]) {
+                            orderedTools[toolKey] = unorderedTools[toolKey];
+                            delete unorderedTools[toolKey];
+                        }
+                    });
+                    
+                    // 添加新安装的工具(不在保存的顺序中的)
+                    Object.assign(orderedTools, unorderedTools);
+                    
+                    this.fhTools = orderedTools;
+                } else {
+                    this.fhTools = tools;
+                }
+                
+                this.isLoading = false;
+                
+                // 根据工具数量添加相应的CSS类来优化显示
+                this.$nextTick(() => {
+                    this.updateLayoutClasses();
+                });
+            } catch (error) {
+                console.error('加载工具列表失败:', error);
+                this.isLoading = false;
+                // 即使加载失败,也不应该让popup完全无法使用
+                this.fhTools = {};
+                
+                // 加载失败时也需要更新布局类
+                this.$nextTick(() => {
+                    this.updateLayoutClasses();
                 });
-                await chrome.sidePanel.open({ tabId });
-                return window.close();
             }
+        },
+
+        recordUsage() {
+            try {
+                // 埋点:自动触发popup统计
+                chrome.runtime.sendMessage({
+                    type: 'fh-dynamic-any-thing',
+                    thing: 'statistics-tool-usage',
+                    params: {
+                        tool_name: 'popup'
+                    }
+                });
+            } catch (error) {
+                // 忽略统计错误,不影响主功能
+                console.warn('统计记录失败:', error);
+            }
+        },
+
+        setupKeyboardNavigation() {
+            document.body.addEventListener('keydown', e => {
+                let keyCode = e.keyCode || e.which;
+                if (![38, 40, 13].includes(keyCode)) {
+                    return false;
+                }
+                let ul = document.querySelector('#pageContainer ul');
+                if (!ul) return false;
+                
+                let hovered = ul.querySelector('li.x-hovered');
+                let next, prev;
+                if (hovered) {
+                    hovered.classList.remove('x-hovered');
+                    next = hovered.nextElementSibling;
+                    prev = hovered.previousElementSibling;
+                }
+                if (!next) {
+                    next = ul.querySelector('li:first-child');
+                }
+                if (!prev) {
+                    prev = ul.querySelector('li:last-child');
+                }
+
+                switch (keyCode) {
+                    case 38: // 方向键:↑
+                        if (prev) prev.classList.add('x-hovered');
+                        break;
+                    case 40: // 方向键:↓
+                        if (next) next.classList.add('x-hovered');
+                        break;
+                    case 13: // 回车键:选择
+                        if (hovered) hovered.click();
+                }
+            }, false);
+        },
+
+        setupScreenshotButton() {
+            // 查找截图按钮并绑定事件
+            const screenshotButtons = Array.from(document.querySelectorAll('a[data-tool="screenshot"], button[data-tool="screenshot"]'));
+            
+            screenshotButtons.forEach(button => {
+                // 移除原有的点击事件
+                button.onclick = function(e) {
+                    e.preventDefault();
+                    e.stopPropagation();
+                    triggerScreenshot();
+                    return false;
+                };
+            });
+        },
 
-            // 其他的情形,就不在sidePanel中打开了
+        runHelper: async function (toolName) {
+            if (!toolName || !this.fhTools[toolName]) return;
+            
             let request = {
                 type: MSG_TYPE.OPEN_DYNAMIC_TOOL,
                 page: toolName,
@@ -93,7 +252,7 @@ new Vue({
                 request.query = `tool=${toolName}`;
             }
             chrome.runtime.sendMessage(request);
-            !!this.fhTools[toolName].noPage && setTimeout(window.close,200);
+            !!this.fhTools[toolName].noPage && setTimeout(window.close, 200);
         },
 
         openOptionsPage: () => {
@@ -105,6 +264,29 @@ new Vue({
             // 获取后台页面,返回window对象
             chrome.tabs.create({url: event.currentTarget.href});
             return false;
+        },
+
+        updateLayoutClasses() {
+            const container = document.getElementById('pageContainer');
+            if (!container) return;
+            
+            const installedCount = this.installedToolsCount;
+            
+            // 移除所有布局相关的类
+            container.classList.remove('few-tools', 'very-few-tools');
+            
+            // 根据工具数量添加相应的类
+            if (installedCount <= 1) {
+                container.classList.add('very-few-tools');
+                console.log('Popup布局:应用very-few-tools类 (工具数量:', installedCount, ')');
+            } else if (installedCount <= 3) {
+                container.classList.add('few-tools');
+                console.log('Popup布局:应用few-tools类 (工具数量:', installedCount, ')');
+            } else {
+                console.log('Popup布局:使用默认布局 (工具数量:', installedCount, ')');
+            }
         }
     }
 });
+
+

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 5 - 0
apps/poster-maker/css/all.min.css


+ 891 - 0
apps/poster-maker/css/main.css

@@ -0,0 +1,891 @@
+@import url("all.min.css");
+
+/* 全局样式 */
+:root {
+  --primary-color: #4a6bff;
+  --secondary-color: #6c757d;
+  --background-color: #f8f9fa;
+  --card-bg: #ffffff;
+  --text-color: #333333;
+  --border-color: #e0e0e0;
+  --success-color: #28a745;
+  --danger-color: #dc3545;
+  --shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+  --radius: 8px;
+  --transition: all 0.3s ease;
+}
+
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+}
+
+body {
+  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+  background-color: var(--background-color);
+  color: var(--text-color);
+  line-height: 1.6;
+}
+
+.app-container {
+  max-width: 1200px;
+  margin: 0 auto;
+  padding: 0 20px 0px;
+}
+
+/* 头部样式 */
+.app-header {
+  text-align: center;
+  margin-bottom: 20px;
+  padding: 15px 0;
+  border-bottom: 1px solid var(--border-color);
+}
+
+.app-header h1 {
+  font-size: 2.2rem;
+  color: var(--primary-color);
+  margin-bottom: 5px;
+}
+
+.app-description {
+  font-size: 1rem;
+  color: var(--secondary-color);
+}
+
+/* 主内容区域 */
+.app-main {
+  display: flex;
+  gap: 20px;
+  max-height: calc(100vh - 80px);
+  height: calc(100vh - 80px); /* 添加固定高度 */
+}
+
+.tool-panel {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  background-color: var(--card-bg);
+  border-radius: var(--radius);
+  box-shadow: var(--shadow);
+  overflow: hidden;
+  min-height: 0; /* 确保 flex 子元素可以正确滚动 */
+}
+
+.preview-panel {
+  flex: 1;
+  background-color: var(--card-bg);
+  border-radius: var(--radius);
+  box-shadow: var(--shadow);
+  display: flex;
+  flex-direction: column;
+}
+
+/* 面板选项卡 */
+.panel-tabs {
+  display: flex;
+  border-bottom: 1px solid var(--border-color);
+  height: 46px;  /* 统一高度 */
+  flex-shrink: 0; /* 防止压缩 */
+}
+
+.panel-tab {
+  flex: 1;
+  padding: 12px 15px;
+  background: none;
+  border: none;
+  font-size: 0.95rem;
+  font-weight: 500;
+  color: var(--secondary-color);
+  cursor: pointer;
+  transition: var(--transition);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 8px;
+}
+
+.panel-tab i {
+  font-size: 1rem;
+}
+
+.panel-tab span {
+  margin-top: 1px;
+}
+
+.panel-tab:hover {
+  background-color: rgba(0, 0, 0, 0.03);
+}
+
+.panel-tab.active {
+  color: var(--primary-color);
+  border-bottom: 2px solid var(--primary-color);
+}
+
+.panel-tab.active i {
+  color: var(--primary-color);
+}
+
+.panel-content {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  min-height: 0; /* 确保内容可以滚动 */
+  height: 100%;
+}
+
+.tab-content {
+  display: none;
+  width: 100%;
+  height: 100%;
+  padding: 15px;
+  overflow-y: auto;
+}
+
+.tab-content.active {
+  display: block;
+}
+
+/* 模板选择区域 */
+.template-selection {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+
+.template-categories {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+  margin-bottom: 12px;
+  flex-shrink: 0; /* 防止压缩 */
+}
+
+.category-btn {
+  padding: 5px 10px;
+  border-radius: 16px;
+  border: 1px solid var(--border-color);
+  background-color: white;
+  font-size: 0.85rem;
+  cursor: pointer;
+  transition: var(--transition);
+}
+
+.category-btn:hover {
+  background-color: var(--background-color);
+}
+
+.category-btn.active {
+  background-color: var(--primary-color);
+  color: white;
+  border-color: var(--primary-color);
+}
+
+.templates-container {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
+  gap: 12px;
+  flex: 1;
+  overflow-y: auto;
+  padding: 5px;
+  align-items: start;
+  min-height: 0; /* 确保可以滚动 */
+}
+
+.template-item {
+  border: 2px solid transparent;
+  border-radius: var(--radius);
+  overflow: hidden;
+  cursor: pointer;
+  transition: var(--transition);
+  position: relative;
+  display: flex;
+  flex-direction: column;
+  height: 130px; /* 固定高度 */
+}
+
+.template-item:hover {
+  transform: translateY(-3px);
+  box-shadow: var(--shadow);
+}
+
+.template-item.selected {
+  border-color: var(--primary-color);
+}
+
+.template-thumbnail {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+  flex: 1;
+}
+
+.template-name {
+  font-size: 0.8rem;
+  text-align: center;
+  padding: 4px;
+  background-color: rgba(0, 0, 0, 0.7);
+  color: white;
+  position: absolute;
+  bottom: 0;
+  width: 100%;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+/* 编辑区域 */
+.editor-panel {
+  height: 100%;
+  overflow-y: auto;
+}
+
+.edit-form {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  padding-right: 5px;
+}
+
+.form-group {
+  display: flex;
+  flex-direction: column;
+}
+
+.form-group label {
+  margin-bottom: 4px;
+  font-weight: 500;
+  font-size: 0.9rem;
+}
+
+.form-control {
+  padding: 8px 10px;
+  border: 1px solid var(--border-color);
+  border-radius: var(--radius);
+  font-size: 0.95rem;
+  transition: var(--transition);
+}
+
+.form-control:focus {
+  outline: none;
+  border-color: var(--primary-color);
+  box-shadow: 0 0 0 2px rgba(74, 107, 255, 0.2);
+}
+
+.color-picker {
+  height: 36px;
+  padding: 4px;
+  border: 1px solid var(--border-color);
+  border-radius: var(--radius);
+}
+
+/* 高级选项区域 */
+.advanced-options {
+  height: 100%;
+  overflow-y: auto;
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+  padding-right: 5px;
+}
+
+.option-group {
+  background-color: #f8f9fa;
+  border-radius: var(--radius);
+  padding: 15px;
+}
+
+.option-group h3 {
+  font-size: 1rem;
+  margin-bottom: 12px;
+  color: var(--primary-color);
+  font-weight: 600;
+}
+
+.range-slider-container {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+
+.range-slider {
+  flex: 1;
+  -webkit-appearance: none;
+  height: 6px;
+  border-radius: 3px;
+  background: #ddd;
+  outline: none;
+}
+
+.range-slider::-webkit-slider-thumb {
+  -webkit-appearance: none;
+  appearance: none;
+  width: 16px;
+  height: 16px;
+  border-radius: 50%;
+  background: var(--primary-color);
+  cursor: pointer;
+}
+
+.range-value {
+  width: 40px;
+  font-size: 0.85rem;
+  text-align: right;
+}
+
+.watermark-options {
+  margin-top: 10px;
+  padding-top: 10px;
+  border-top: 1px dashed var(--border-color);
+}
+
+/* 预览区域标题栏 */
+.preview-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  height: 46px;  /* 统一高度 */
+  border-bottom: 1px solid var(--border-color);
+  padding: 0 15px;
+}
+
+.preview-header h2 {
+  margin: 0;
+  color: var(--primary-color);
+  font-size: 0.95rem;  /* 统一字体大小 */
+  font-weight: 500;    /* 统一字重 */
+}
+
+.zoom-controls {
+  display: flex;
+  gap: 8px;
+}
+
+.zoom-btn {
+  padding: 4px 12px;
+  border: 1px solid var(--border-color);
+  border-radius: 4px;
+  background-color: white;
+  color: var(--text-color);
+  font-size: 13px;
+  cursor: pointer;
+  transition: var(--transition);
+  height: 28px;       /* 固定按钮高度 */
+  line-height: 20px;  /* 文字垂直居中 */
+}
+
+.zoom-btn:hover {
+  background-color: var(--background-color);
+  border-color: var(--primary-color);
+  color: var(--primary-color);
+}
+
+/* 预览区域 */
+.preview-container {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  flex: 1;
+  margin-bottom: 15px;
+  background-color: #f0f2f5;
+  padding: 15px;
+  overflow: hidden;
+  position: relative;
+}
+
+.poster-preview {
+  width: 375px;
+  min-height: 667px;
+  background-color: white;
+  box-shadow: var(--shadow);
+  border-radius: var(--radius);
+  position: relative;
+  transform-origin: center;
+  transition: transform 0.3s ease;
+}
+
+.poster-preview > div {
+  min-height: 667px;
+  display: flex;
+  flex-direction: column;
+}
+
+.poster-preview img {
+  max-width: 100%;
+  height: auto;
+  object-fit: contain;
+}
+
+.poster-preview.with-filter {
+  filter: brightness(var(--brightness, 100%)) contrast(var(--contrast, 100%)) saturate(var(--saturation, 100%));
+}
+
+.poster-preview-container {
+  position: relative;
+}
+
+.poster-watermark {
+  position: absolute;
+  color: white;
+  font-size: 16px;
+  padding: 5px 10px;
+  font-weight: 500;
+  text-shadow: 0 1px 2px rgba(0,0,0,0.5);
+  pointer-events: none;
+}
+
+/* 图片上传 */
+.image-upload {
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+}
+
+.upload-preview {
+  max-width: 100%;
+  height: 120px;
+  border: 1px dashed var(--border-color);
+  border-radius: var(--radius);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  overflow: hidden;
+}
+
+.upload-preview img {
+  max-width: 100%;
+  max-height: 100%;
+  object-fit: contain;
+}
+
+.upload-btn {
+  display: inline-block;
+  padding: 6px 12px;
+  background-color: var(--secondary-color);
+  color: white;
+  border: none;
+  border-radius: var(--radius);
+  cursor: pointer;
+  transition: var(--transition);
+  font-size: 0.9rem;
+}
+
+.upload-btn:hover {
+  background-color: #5a6268;
+}
+
+.placeholder-message {
+  text-align: center;
+  color: var(--secondary-color);
+  padding: 20px;
+}
+
+.detection-notice {
+  margin-bottom: 15px;
+}
+
+.info-message {
+  padding: 10px;
+  background-color: rgba(74, 107, 255, 0.1);
+  border-radius: var(--radius);
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  font-size: 0.9rem;
+  color: var(--primary-color);
+}
+
+.info-message i {
+  font-size: 1rem;
+}
+
+/* 按钮样式 */
+.action-buttons {
+  display: flex;
+  justify-content: center;
+  gap: 15px;
+  margin-bottom: 10px;
+}
+
+.btn {
+  padding: 10px 20px;
+  border: none;
+  border-radius: var(--radius);
+  font-size: 1rem;
+  font-weight: 500;
+  cursor: pointer;
+  transition: var(--transition);
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.btn i {
+  font-size: 1.1rem;
+}
+
+.primary-btn {
+  background-color: var(--primary-color);
+  color: white;
+}
+
+.primary-btn:hover {
+  background-color: #3a5bd9;
+}
+
+.secondary-btn {
+  background-color: var(--secondary-color);
+  color: white;
+}
+
+.secondary-btn:hover {
+  background-color: #5a6268;
+}
+
+.btn:disabled {
+  background-color: #cccccc;
+  cursor: not-allowed;
+}
+
+/* 模态框样式 */
+.overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background-color: rgba(0, 0, 0, 0.5);
+  z-index: 999;
+  display: none;
+}
+
+.modal {
+  position: fixed;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  background-color: white;
+  padding: 30px;
+  border-radius: var(--radius);
+  box-shadow: var(--shadow);
+  z-index: 1000;
+  width: 80%;
+  max-width: 500px;
+  display: none;
+}
+
+.close-modal {
+  position: absolute;
+  right: 20px;
+  top: 15px;
+  font-size: 1.5rem;
+  cursor: pointer;
+  color: var(--secondary-color);
+}
+
+.close-modal:hover {
+  color: var(--danger-color);
+}
+
+.modal h2 {
+  margin-bottom: 20px;
+  color: var(--primary-color);
+}
+
+.share-options {
+  display: flex;
+  justify-content: space-around;
+  margin-bottom: 30px;
+}
+
+.share-option {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 10px;
+  cursor: pointer;
+  transition: var(--transition);
+}
+
+.share-option i {
+  font-size: 2rem;
+  color: var(--primary-color);
+}
+
+.share-option:hover {
+  transform: translateY(-5px);
+}
+
+.share-instructions {
+  background-color: var(--background-color);
+  padding: 15px;
+  border-radius: var(--radius);
+}
+
+.share-instructions p {
+  margin-bottom: 8px;
+}
+
+/* 响应式设计 */
+@media (max-width: 992px) {
+  .app-main {
+    flex-direction: column;
+  }
+  
+  .preview-panel {
+    margin-top: 20px;
+  }
+}
+
+@media (max-width: 576px) {
+  .app-header h1 {
+    font-size: 2rem;
+  }
+  
+  .templates-container {
+    grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
+  }
+  
+  .action-buttons {
+    flex-direction: column;
+  }
+  
+  .btn {
+    width: 100%;
+    justify-content: center;
+  }
+}
+
+/* 动画效果 */
+@keyframes fadeIn {
+  from { opacity: 0; }
+  to { opacity: 1; }
+}
+
+.fade-in {
+  animation: fadeIn 0.5s ease forwards;
+}
+
+/* 导航栏样式 */
+.main-navbar {
+  background-color: #fff;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+  padding: 10px 20px;
+  margin-bottom: 20px;
+}
+
+.navbar-brand {
+  display: flex;
+  align-items: center;
+  position: relative;
+}
+.mod-head-actions{
+  position: absolute;
+  right: 0;
+} 
+.mod-head-actions a:hover{
+  color: #f00;
+}
+
+.brand-link {
+  display: flex;
+  align-items: center;
+  text-decoration: none;
+  color: var(--text-color);
+}
+
+.brand-link img {
+  width: 24px;
+  height: 24px;
+  margin-right: 8px;
+}
+
+.brand-text {
+  margin-right: 8px;
+}
+
+.brand-subtitle {
+  font-size: 14px;
+  color: var(--secondary-color);
+  padding-left: 8px;
+  border-left: 1px solid var(--border-color);
+}
+
+/* 移除旧的预览面板标题样式 */
+.preview-panel h2 {
+  margin-bottom: 0;
+  padding-bottom: 0;
+  border-bottom: none;
+}
+
+/* 特殊布局样式 */
+.special-layout {
+  background-color: var(--background-color);
+  padding: 15px;
+  border-radius: var(--radius);
+  position: relative;
+}
+
+.special-layout .image-upload {
+  height: 100%;
+}
+
+.special-layout .upload-preview {
+  height: 150px;
+}
+
+.special-layout .color-picker {
+  width: 100%;
+  height: 40px;
+}
+
+.special-layout label {
+  margin-bottom: 8px;
+  color: var(--text-color);
+  font-weight: 500;
+}
+.special-layout .left-column{
+  flex: 1;
+  width: 240px;
+}
+.special-layout .right-column{
+  flex: 1;
+  width: 120px;
+  position: absolute;
+  top: 15px;
+  right: 20px;
+}
+
+.special-layout .upload-btn{
+  position: absolute;
+  bottom: 0;
+  background: #0009;
+  color: #fff;
+  width: 240px;
+  text-align: center;
+}
+
+/* 加载提示 */
+.loading-tip {
+  position: fixed;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  background: rgba(0, 0, 0, 0.8);
+  color: white;
+  padding: 15px 30px;
+  border-radius: 25px;
+  font-size: 14px;
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  z-index: 9999;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+}
+
+.loading-tip i {
+  font-size: 16px;
+}
+
+@keyframes spin {
+  0% { transform: rotate(0deg); }
+  100% { transform: rotate(360deg); }
+}
+
+.loading-tip .fa-spin {
+  animation: spin 1s linear infinite;
+}
+
+
+/* 工具市场按钮样式(保持不变) */
+.panel-title>a.x-other-tools {
+  margin: 1px 0 0;
+  font-size: 13px;
+  cursor: pointer;
+  text-decoration: none;
+  -webkit-user-select: none;
+  user-select: none;
+  color: #333;
+  float: right;
+  background-color: #f5f8ff;
+  padding: 5px 10px;
+  border-radius: 15px;
+  border: 1px solid #d0d9ff;
+  transition: all 0.3s ease;
+  display: flex;
+  align-items: center;
+  position: relative;
+  top: 0px;
+}
+
+.panel-title>a.x-other-tools .icon-plus-circle {
+  display: inline-block;
+  width: 16px;
+  height: 16px;
+  background: url(../../static/img/plus-circle.svg) no-repeat center center;
+  background-size: contain;
+  margin-right: 5px;
+}
+
+.panel-title>a.x-other-tools .tool-market-badge {
+  display: inline-block;
+  background-color: #4d89fe;
+  color: white;
+  padding: 2px 6px;
+  border-radius: 10px;
+  margin-left: 5px;
+  font-size: 12px;
+  font-weight: bold;
+}
+
+.panel-title>a.x-other-tools:hover {
+  color: #333;
+  background-color: #e6edff;
+  box-shadow: 0 2px 5px rgba(0,0,0,0.15);
+  transform: translateY(-1px);
+}
+
+
+/* 保持原有的顶部导航样式 */
+.x-donate-link {
+  float: right;
+  line-height: 18px;
+  color: #2563eb;
+  cursor: pointer;
+  text-decoration: none;
+  border: none;
+  white-space: nowrap;
+  margin-right: auto;
+  border-radius: 20px;
+  background-color: #eff6ff;
+  transition: all 0.2s ease;
+  position: relative;
+  display: inline-flex;
+  align-items: center;
+  box-shadow: 0 1px 2px rgba(37, 99, 235, 0.1);
+  position: absolute;
+  right: 230px;
+  top: 4px;
+  padding: 4px 12px;
+  font-size: 12px;
+  font-weight: normal;
+}
+
+.x-donate-link:hover {
+  background-color: #dbeafe;
+  color: #1d4ed8;
+  text-decoration: none;
+  box-shadow: 0 2px 4px rgba(37, 99, 235, 0.15);
+  transform: translateY(-1px);
+}
+
+.x-donate-link>a {
+  font-size: 12px;
+  color: blue;
+  cursor: pointer;
+  text-decoration: underline;
+}
+.x-donate-link>a:hover {
+  text-decoration: underline;
+  color: #f00;
+}

+ 194 - 0
apps/poster-maker/index.html

@@ -0,0 +1,194 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>海报快速生成</title>
+  
+  <!-- 本地CSS文件 -->
+  <link rel="stylesheet" href="css/main.css">
+  
+</head>
+<body>
+  <div class="main-navbar">
+    <div class="navbar-brand">
+      <a href="#" class="brand-link">
+        <img src="../static/img/fe-48.png" alt="fehelper"/> 
+        <span class="brand-text">FeHelper</span>
+        <span class="brand-subtitle">海报快速生成</span>
+      </a>
+      <div class="mod-head-actions panel-title">
+          <a href="#" class="x-donate-link" id="donate-link"><i class="nav-icon">❤&nbsp;</i>打赏鼓励</a>
+          <a class="x-other-tools" id="other-tools"><i class="icon-plus-circle"></i> 探索更多实用工具 <span class="tool-market-badge">工具市场</span></a>
+      </div>
+    </div>
+  </div>
+
+  <div class="app-container">
+    <main class="app-main">
+      <div class="tool-panel">
+        <div class="panel-tabs">
+          <button class="panel-tab active" data-tab="templates">
+            <i class="fas fa-images"></i>
+            <span>模板</span>
+          </button>
+          <button class="panel-tab" data-tab="editor">
+            <i class="fas fa-edit"></i>
+            <span>编辑</span>
+          </button>
+          <button class="panel-tab" data-tab="advanced">
+            <i class="fas fa-sliders-h"></i>
+            <span>高级</span>
+          </button>
+        </div>
+        
+        <div class="panel-content">
+          <div class="tab-content active" id="templates-tab">
+            <div class="template-selection">
+              <div class="template-categories">
+                <button class="category-btn active" data-category="all">全部</button>
+                <button class="category-btn" data-category="tech">技术</button>
+                <button class="category-btn" data-category="product">产品</button>
+                <button class="category-btn" data-category="operation">运营</button>
+                <button class="category-btn" data-category="promotion">促销</button>
+                <button class="category-btn" data-category="social">社交</button>
+                <button class="category-btn" data-category="work">工作</button>
+              </div>
+              <div class="templates-container" id="templates-container"></div>
+            </div>
+          </div>
+          
+          <div class="tab-content" id="editor-tab">
+            <div class="editor-panel">
+              <div class="edit-form" id="edit-form">
+                <p class="placeholder-message">请先选择一个模板</p>
+              </div>
+            </div>
+          </div>
+          
+          <div class="tab-content" id="advanced-tab">
+            <div class="advanced-options">
+              <div class="option-group">
+                <h3>布局调整</h3>
+                <div class="form-group">
+                  <label for="poster-width">宽度比例</label>
+                  <div class="range-slider-container">
+                    <input type="range" id="poster-width" min="80" max="120" value="100" class="range-slider">
+                    <span class="range-value">100%</span>
+                  </div>
+                </div>
+                <div class="form-group">
+                  <label for="poster-height">高度比例</label>
+                  <div class="range-slider-container">
+                    <input type="range" id="poster-height" min="80" max="120" value="100" class="range-slider">
+                    <span class="range-value">100%</span>
+                  </div>
+                </div>
+              </div>
+              
+              <div class="option-group">
+                <h3>滤镜效果</h3>
+                <div class="form-group">
+                  <label for="brightness">亮度</label>
+                  <div class="range-slider-container">
+                    <input type="range" id="brightness" min="50" max="150" value="100" class="range-slider">
+                    <span class="range-value">100%</span>
+                  </div>
+                </div>
+                <div class="form-group">
+                  <label for="contrast">对比度</label>
+                  <div class="range-slider-container">
+                    <input type="range" id="contrast" min="50" max="150" value="100" class="range-slider">
+                    <span class="range-value">100%</span>
+                  </div>
+                </div>
+                <div class="form-group">
+                  <label for="saturation">饱和度</label>
+                  <div class="range-slider-container">
+                    <input type="range" id="saturation" min="0" max="200" value="100" class="range-slider">
+                    <span class="range-value">100%</span>
+                  </div>
+                </div>
+              </div>
+              
+              <div class="option-group">
+                <h3>导出选项</h3>
+                <div class="form-group">
+                  <label for="export-quality">导出质量</label>
+                  <div class="range-slider-container">
+                    <input type="range" id="export-quality" min="70" max="100" value="90" class="range-slider">
+                    <span class="range-value">90%</span>
+                  </div>
+                </div>
+                <div class="form-group">
+                  <label for="export-format">导出格式</label>
+                  <select id="export-format" class="form-control">
+                    <option value="png">PNG (透明背景)</option>
+                    <option value="jpeg">JPEG (高兼容性)</option>
+                  </select>
+                </div>
+              </div>
+              
+              <div class="option-group">
+                <h3>水印设置</h3>
+                <div class="form-group">
+                  <label>
+                    <input type="checkbox" id="enable-watermark"> 添加水印
+                  </label>
+                </div>
+                <div class="watermark-options" style="display: none;">
+                  <div class="form-group">
+                    <label for="watermark-text">水印文字</label>
+                    <input type="text" id="watermark-text" class="form-control" value="我的品牌">
+                  </div>
+                  <div class="form-group">
+                    <label for="watermark-position">位置</label>
+                    <select id="watermark-position" class="form-control">
+                      <option value="bottom-right">右下角</option>
+                      <option value="bottom-left">左下角</option>
+                      <option value="top-right">右上角</option>
+                      <option value="top-left">左上角</option>
+                      <option value="center">中央</option>
+                    </select>
+                  </div>
+                  <div class="form-group">
+                    <label for="watermark-opacity">透明度</label>
+                    <div class="range-slider-container">
+                      <input type="range" id="watermark-opacity" min="10" max="100" value="30" class="range-slider">
+                      <span class="range-value">30%</span>
+                    </div>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+      
+      <div class="preview-panel">
+        <div class="preview-header">
+          <h2>预览</h2>
+          <div class="zoom-controls">
+            <button class="zoom-btn" id="zoom-out">缩小</button>
+            <button class="zoom-btn" id="zoom-reset">重置</button>
+            <button class="zoom-btn" id="zoom-in">放大</button>
+          </div>
+        </div>
+        <div class="preview-container">
+          <div id="poster-preview" class="poster-preview"></div>
+        </div>
+        <div class="action-buttons">
+          <button id="download-btn" class="btn primary-btn" disabled>
+            <i class="fas fa-download"></i> 下载海报
+          </button>
+        </div>
+      </div>
+    </main>
+  </div>
+
+  <!-- 引入JavaScript文件 -->
+  <script src="js/FileSaver.min.js"></script>
+  <script src="../chart-maker/lib/html2canvas.min.js"></script>
+  <script type="module" src="js/index.js"></script>
+</body>
+</html>

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
apps/poster-maker/js/FileSaver.min.js


+ 231 - 0
apps/poster-maker/js/eventHandlers.js

@@ -0,0 +1,231 @@
+// 事件处理器
+
+// 设置所有事件监听器
+export function setupEventListeners() {
+  // 面板选项卡切换
+  const panelTabs = document.querySelectorAll('.panel-tab');
+  if (panelTabs) {
+    panelTabs.forEach(tab => {
+      tab.addEventListener('click', () => {
+        // 移除所有选项卡的active类
+        panelTabs.forEach(t => t.classList.remove('active'));
+        
+        // 隐藏所有内容面板
+        document.querySelectorAll('.tab-content').forEach(content => {
+          content.style.display = 'none';
+        });
+        
+        // 添加当前选项卡的active类
+        tab.classList.add('active');
+        
+        // 显示对应的内容面板
+        const tabId = tab.dataset.tab;
+        if (tabId === 'templates') {
+          document.getElementById('templates-tab').style.display = 'block';
+        } else if (tabId === 'editor') {
+          document.getElementById('editor-tab').style.display = 'block';
+        } else if (tabId === 'advanced') {
+          document.getElementById('advanced-tab').style.display = 'block';
+        }
+      });
+    });
+  }
+  
+  // 分类筛选按钮
+  const categoryBtns = document.querySelectorAll('.category-btn');
+  if (categoryBtns) {
+    categoryBtns.forEach(btn => {
+      btn.addEventListener('click', () => {
+        // 移除所有按钮的active类
+        categoryBtns.forEach(b => b.classList.remove('active'));
+        
+        // 添加当前按钮的active类
+        btn.classList.add('active');
+        
+        // 获取分类值并渲染模板
+        const category = btn.dataset.category;
+        window.renderTemplates(window.templates, category);
+      });
+    });
+  }
+  
+  // 下载按钮
+  const downloadBtn = document.getElementById('download-btn');
+  if (downloadBtn) {
+    downloadBtn.addEventListener('click', () => {
+      // 获取导出设置
+      const exportFormat = document.getElementById('export-format')?.value || 'png';
+      const exportQuality = document.getElementById('export-quality')?.value / 100 || 0.9;
+      
+      // 下载海报
+      window.downloadPoster(exportFormat, exportQuality);
+    });
+  }
+
+  // 打赏鼓励
+  const donateLink = document.getElementById('donate-link');
+  if (donateLink) {
+    donateLink.addEventListener('click', (event) => {
+      event.preventDefault();
+      event.stopPropagation();
+      chrome.runtime.sendMessage({
+        type: 'fh-dynamic-any-thing', 
+        thing: 'open-donate-modal',
+        params: { toolName: 'poster-maker' }
+      });
+    });
+  }
+
+  // 探索更多实用工具
+  const otherTools = document.getElementById('other-tools');
+  if (otherTools) {
+    otherTools.addEventListener('click', (event) => {
+      event.preventDefault();
+      event.stopPropagation();
+      chrome.runtime.openOptionsPage();
+    });
+  }
+
+  // 设置高级选项事件监听器
+  setupAdvancedOptions();
+}
+
+// 设置高级选项事件监听器
+function setupAdvancedOptions() {
+  // 布局调整
+  const posterWidth = document.getElementById('poster-width');
+  const posterHeight = document.getElementById('poster-height');
+  const posterPreview = document.getElementById('poster-preview');
+  
+  if (posterWidth && posterHeight && posterPreview) {
+    posterWidth.addEventListener('input', (e) => {
+      const value = e.target.value;
+      posterWidth.nextElementSibling.textContent = `${value}%`;
+      posterPreview.style.width = `${375 * value / 100}px`;
+    });
+    
+    posterHeight.addEventListener('input', (e) => {
+      const value = e.target.value;
+      posterHeight.nextElementSibling.textContent = `${value}%`;
+      posterPreview.style.height = `${667 * value / 100}px`;
+    });
+  }
+  
+  // 滤镜效果
+  const brightness = document.getElementById('brightness');
+  const contrast = document.getElementById('contrast');
+  const saturation = document.getElementById('saturation');
+  
+  if (brightness && contrast && saturation && posterPreview) {
+    const updateFilter = () => {
+      const brightnessValue = brightness.value;
+      const contrastValue = contrast.value;
+      const saturationValue = saturation.value;
+      
+      posterPreview.style.setProperty('--brightness', `${brightnessValue}%`);
+      posterPreview.style.setProperty('--contrast', `${contrastValue}%`);
+      posterPreview.style.setProperty('--saturation', `${saturationValue}%`);
+      posterPreview.classList.add('with-filter');
+      
+      brightness.nextElementSibling.textContent = `${brightnessValue}%`;
+      contrast.nextElementSibling.textContent = `${contrastValue}%`;
+      saturation.nextElementSibling.textContent = `${saturationValue}%`;
+    };
+    
+    brightness.addEventListener('input', updateFilter);
+    contrast.addEventListener('input', updateFilter);
+    saturation.addEventListener('input', updateFilter);
+  }
+  
+  // 导出质量
+  const exportQuality = document.getElementById('export-quality');
+  if (exportQuality) {
+    exportQuality.addEventListener('input', (e) => {
+      const value = e.target.value;
+      exportQuality.nextElementSibling.textContent = `${value}%`;
+    });
+  }
+  
+  // 水印设置
+  const enableWatermark = document.getElementById('enable-watermark');
+  const watermarkOptions = document.querySelector('.watermark-options');
+  const watermarkText = document.getElementById('watermark-text');
+  const watermarkPosition = document.getElementById('watermark-position');
+  const watermarkOpacity = document.getElementById('watermark-opacity');
+  
+  if (enableWatermark && watermarkOptions) {
+    enableWatermark.addEventListener('change', (e) => {
+      if (e.target.checked) {
+        watermarkOptions.style.display = 'block';
+        updateWatermark();
+      } else {
+        watermarkOptions.style.display = 'none';
+        removeWatermark();
+      }
+    });
+    
+    if (watermarkText && watermarkPosition && watermarkOpacity) {
+      watermarkText.addEventListener('input', updateWatermark);
+      watermarkPosition.addEventListener('change', updateWatermark);
+      watermarkOpacity.addEventListener('input', () => {
+        watermarkOpacity.nextElementSibling.textContent = `${watermarkOpacity.value}%`;
+        updateWatermark();
+      });
+    }
+  }
+}
+
+// 更新水印
+function updateWatermark() {
+  const posterPreview = document.getElementById('poster-preview');
+  if (!posterPreview) return;
+  
+  // 移除现有水印
+  removeWatermark();
+  
+  const watermarkText = document.getElementById('watermark-text').value;
+  const watermarkPosition = document.getElementById('watermark-position').value;
+  const watermarkOpacity = document.getElementById('watermark-opacity').value / 100;
+  
+  // 创建水印元素
+  const watermark = document.createElement('div');
+  watermark.className = 'poster-watermark';
+  watermark.textContent = watermarkText;
+  watermark.style.opacity = watermarkOpacity;
+  
+  // 设置水印位置
+  switch (watermarkPosition) {
+    case 'bottom-right':
+      watermark.style.bottom = '10px';
+      watermark.style.right = '10px';
+      break;
+    case 'bottom-left':
+      watermark.style.bottom = '10px';
+      watermark.style.left = '10px';
+      break;
+    case 'top-right':
+      watermark.style.top = '10px';
+      watermark.style.right = '10px';
+      break;
+    case 'top-left':
+      watermark.style.top = '10px';
+      watermark.style.left = '10px';
+      break;
+    case 'center':
+      watermark.style.top = '50%';
+      watermark.style.left = '50%';
+      watermark.style.transform = 'translate(-50%, -50%)';
+      break;
+  }
+  
+  posterPreview.appendChild(watermark);
+}
+
+// 移除水印
+function removeWatermark() {
+  const posterPreview = document.getElementById('poster-preview');
+  const watermark = posterPreview?.querySelector('.poster-watermark');
+  if (watermark) {
+    watermark.remove();
+  }
+}

+ 51 - 0
apps/poster-maker/js/imageUpload.js

@@ -0,0 +1,51 @@
+// 设置图片上传功能
+// 图片上传功能
+export function setupImageUpload() {
+  // 这个函数为未来可能的拖放上传功能预留
+  // 目前基本的图片上传功能已经在templateRenderer.js中实现
+  
+  // 未来可以添加:
+  // 1. 拖放上传
+  // 2. 图片裁剪
+  // 3. 图片滤镜
+  // 4. 图片调整大小
+  
+  // 示例: 如果添加拖放上传功能
+  /*
+  const uploadPreviews = document.querySelectorAll('.upload-preview');
+  
+  uploadPreviews.forEach(preview => {
+    preview.addEventListener('dragover', (e) => {
+      e.preventDefault();
+      preview.classList.add('drag-over');
+    });
+    
+    preview.addEventListener('dragleave', () => {
+      preview.classList.remove('drag-over');
+    });
+    
+    preview.addEventListener('drop', (e) => {
+      e.preventDefault();
+      preview.classList.remove('drag-over');
+      
+      const files = e.dataTransfer.files;
+      if (files.length > 0 && files[0].type.startsWith('image/')) {
+        // 找到对应的input元素
+        const inputId = preview.querySelector('img').id.replace('preview-', 'field-');
+        const input = document.getElementById(inputId);
+        
+        if (input) {
+          // 触发input的change事件
+          const dataTransfer = new DataTransfer();
+          dataTransfer.items.add(files[0]);
+          input.files = dataTransfer.files;
+          
+          // 手动触发change事件
+          const event = new Event('change', { bubbles: true });
+          input.dispatchEvent(event);
+        }
+      }
+    });
+  });
+  */
+}

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.