diff --git a/.gitignore b/.gitignore index d5ee323..e307769 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ go.work # IDE .idea/ .vscode/ +.claude/ *.swp *.swo *~ @@ -34,3 +35,5 @@ Thumbs.db # 日志文件 *.log +# 其他 +docs/ \ No newline at end of file diff --git a/app.go b/app.go index ab15182..b9f734d 100644 --- a/app.go +++ b/app.go @@ -692,6 +692,11 @@ func (a *App) GetFileServerURL() string { return "http://localhost:18765" } +// DetectFileTypeByContent 通过文件内容检测文件类型(用于小文件) +func (a *App) DetectFileTypeByContent(path string) (map[string]interface{}, error) { + return filesystem.DetectFileTypeByContentSimple(path) +} + // ========== 回收站接口 ========== // GetRecycleBinEntries 获取回收站条目 diff --git a/docs/04-功能迭代/GO-DESK-1.尝试/更新升级功能设计.md b/docs/04-功能迭代/GO-DESK-1.尝试/更新升级功能设计.md index d9f5060..537c790 100644 --- a/docs/04-功能迭代/GO-DESK-1.尝试/更新升级功能设计.md +++ b/docs/04-功能迭代/GO-DESK-1.尝试/更新升级功能设计.md @@ -1,8 +1,8 @@ # Go Desk 更新升级功能设计 -> **文档版本**:v1.0 -> **创建时间**:2025-01-XX -> **维护者**:JueChen +> **文档版本**:v0.1.0 +> **创建时间**:2026-01-20 +> **维护者**:JueChen > **状态**:设计阶段 ## 1. 功能概述 diff --git a/docs/04-功能迭代/GO-DESK-1.尝试/设备调用测试功能设计.md b/docs/04-功能迭代/GO-DESK-1.尝试/设备调用测试功能设计.md index b01360f..0f5eb3a 100644 --- a/docs/04-功能迭代/GO-DESK-1.尝试/设备调用测试功能设计.md +++ b/docs/04-功能迭代/GO-DESK-1.尝试/设备调用测试功能设计.md @@ -1,8 +1,8 @@ # Go Desk 设备调用测试功能设计 -> **文档版本**:v1.0 -> **创建时间**:2025-01-XX -> **维护者**:JueChen +> **文档版本**:v0.1.0 +> **创建时间**:2026-01-20 +> **维护者**:JueChen > **状态**:设计阶段 ## 1. 功能概述 diff --git a/docs/04-功能迭代/GO-DESK-1.尝试/需求.md b/docs/04-功能迭代/GO-DESK-1.尝试/需求.md index 705f699..5dc76e8 100644 --- a/docs/04-功能迭代/GO-DESK-1.尝试/需求.md +++ b/docs/04-功能迭代/GO-DESK-1.尝试/需求.md @@ -1,8 +1,8 @@ # Go Desk 需求文档 -> **文档版本**:v1.0 -> **创建时间**:2025-12-29 -> **维护者**:JueChen +> **文档版本**:v0.1.0 +> **创建时间**:2026-01-20 +> **维护者**:JueChen > **状态**:已确定 ## 1. 产品概述 diff --git a/docs/04-功能迭代/GO-DESK-2.数据库客户端/README.md b/docs/04-功能迭代/GO-DESK-2.数据库客户端/README.md index 28131af..9c0cded 100644 --- a/docs/04-功能迭代/GO-DESK-2.数据库客户端/README.md +++ b/docs/04-功能迭代/GO-DESK-2.数据库客户端/README.md @@ -1,7 +1,7 @@ # 数据库客户端模块 **模块状态**:开发中 -**最后更新**:2025-01-28 +**最后更新**:2026-01-28 --- @@ -19,7 +19,7 @@ ## 🚀 MVP状态 -**✅ 当前版本已达到MVP标准,可以发布MVP v1.0版本** +**🔄 当前版本处于试验阶段,正在开发中** 详细状态和检查结果请参考: - [MVP规划.md](./设计文档/MVP规划.md) - MVP功能规划 diff --git a/docs/04-功能迭代/GO-DESK-2.数据库客户端/任务规划.md b/docs/04-功能迭代/GO-DESK-2.数据库客户端/任务规划.md index 04ecf6d..81686d1 100644 --- a/docs/04-功能迭代/GO-DESK-2.数据库客户端/任务规划.md +++ b/docs/04-功能迭代/GO-DESK-2.数据库客户端/任务规划.md @@ -1,6 +1,6 @@ # 数据库客户端任务规划 -**更新日期**:2025-01-28 +**更新日期**:2026-01-28 **状态**:进行中 --- diff --git a/docs/04-功能迭代/GO-DESK-2.数据库客户端/决策记录/ADR-001-事件系统设计.md b/docs/04-功能迭代/GO-DESK-2.数据库客户端/决策记录/ADR-001-事件系统设计.md index 348af75..d8c3c5a 100644 --- a/docs/04-功能迭代/GO-DESK-2.数据库客户端/决策记录/ADR-001-事件系统设计.md +++ b/docs/04-功能迭代/GO-DESK-2.数据库客户端/决策记录/ADR-001-事件系统设计.md @@ -1,7 +1,7 @@ # ADR-001: 事件系统设计 **状态**:已采纳 -**日期**:2025-01-28 +**日期**:2026-01-28 **决策者**:开发团队 ## 上下文 diff --git a/docs/04-功能迭代/GO-DESK-2.数据库客户端/决策记录/ADR-002-表结构Tab显示策略.md b/docs/04-功能迭代/GO-DESK-2.数据库客户端/决策记录/ADR-002-表结构Tab显示策略.md index 7daa44d..9ec204c 100644 --- a/docs/04-功能迭代/GO-DESK-2.数据库客户端/决策记录/ADR-002-表结构Tab显示策略.md +++ b/docs/04-功能迭代/GO-DESK-2.数据库客户端/决策记录/ADR-002-表结构Tab显示策略.md @@ -1,7 +1,7 @@ # ADR-002: 表结构Tab显示策略 **状态**:已采纳 -**日期**:2025-01-28 +**日期**:2026-01-28 **决策者**:开发团队 ## 上下文 diff --git a/docs/04-功能迭代/GO-DESK-2.数据库客户端/决策记录/ADR-003-右键菜单实现方案.md b/docs/04-功能迭代/GO-DESK-2.数据库客户端/决策记录/ADR-003-右键菜单实现方案.md index c05492b..5e5e59e 100644 --- a/docs/04-功能迭代/GO-DESK-2.数据库客户端/决策记录/ADR-003-右键菜单实现方案.md +++ b/docs/04-功能迭代/GO-DESK-2.数据库客户端/决策记录/ADR-003-右键菜单实现方案.md @@ -1,7 +1,7 @@ # ADR-003: 右键菜单实现方案 **状态**:已采纳 -**日期**:2025-01-28 +**日期**:2026-01-28 **决策者**:开发团队 ## 上下文 diff --git a/docs/04-功能迭代/GO-DESK-2.数据库客户端/文档结构说明.md b/docs/04-功能迭代/GO-DESK-2.数据库客户端/文档结构说明.md index 800bac9..e84e1ea 100644 --- a/docs/04-功能迭代/GO-DESK-2.数据库客户端/文档结构说明.md +++ b/docs/04-功能迭代/GO-DESK-2.数据库客户端/文档结构说明.md @@ -1,6 +1,6 @@ # 文档结构说明 -**创建日期**:2025-01-28 +**创建日期**:2026-01-28 **目的**:说明文档结构如何支持现代化AI人机协同模式 --- diff --git a/docs/04-功能迭代/GO-DESK-2.数据库客户端/核对报告/BUG报告.md b/docs/04-功能迭代/GO-DESK-2.数据库客户端/核对报告/BUG报告.md index 599aaaf..6609fd5 100644 --- a/docs/04-功能迭代/GO-DESK-2.数据库客户端/核对报告/BUG报告.md +++ b/docs/04-功能迭代/GO-DESK-2.数据库客户端/核对报告/BUG报告.md @@ -1,6 +1,6 @@ # 数据库客户端 BUG 报告 -**检查日期**:2025-01-28 +**检查日期**:2026-01-28 **检查人**:JueChen --- diff --git a/docs/04-功能迭代/GO-DESK-2.数据库客户端/核对报告/MVP发布检查.md b/docs/04-功能迭代/GO-DESK-2.数据库客户端/核对报告/MVP发布检查.md index ea08f70..f2cbd52 100644 --- a/docs/04-功能迭代/GO-DESK-2.数据库客户端/核对报告/MVP发布检查.md +++ b/docs/04-功能迭代/GO-DESK-2.数据库客户端/核对报告/MVP发布检查.md @@ -1,7 +1,8 @@ # MVP发布检查报告 -**检查日期**:2025-01-28 -**目标版本**:MVP v1.0 +**检查日期**:2026-01-28 +**目标版本**:数据库客户端(试验阶段) +**状态**:🔄 开发中 **检查人**:JueChen --- @@ -64,7 +65,7 @@ ## 七、发布决策 ✅ -**✅ 建议发布MVP v1.0版本** +**⚠️ 当前处于试验阶段,暂不建议发布** **理由**: 1. 核心功能和重要功能全部完成(表结构编辑可延后) diff --git a/docs/04-功能迭代/GO-DESK-2.数据库客户端/核对报告/前端样式重构报告.md b/docs/04-功能迭代/GO-DESK-2.数据库客户端/核对报告/前端样式重构报告.md index 724f2f4..fcccc4a 100644 --- a/docs/04-功能迭代/GO-DESK-2.数据库客户端/核对报告/前端样式重构报告.md +++ b/docs/04-功能迭代/GO-DESK-2.数据库客户端/核对报告/前端样式重构报告.md @@ -1,6 +1,6 @@ # 前端样式重构报告 -**重构日期**:2025-01-28 +**重构日期**:2026-01-28 **重构范围**:数据库客户端前端布局和样式系统 **重构依据**:[前端布局样式系统设计.md](../设计文档/前端布局样式系统设计.md) diff --git a/docs/04-功能迭代/GO-DESK-2.数据库客户端/核对报告/功能实现检查报告.md b/docs/04-功能迭代/GO-DESK-2.数据库客户端/核对报告/功能实现检查报告.md index c8276a4..5e9affe 100644 --- a/docs/04-功能迭代/GO-DESK-2.数据库客户端/核对报告/功能实现检查报告.md +++ b/docs/04-功能迭代/GO-DESK-2.数据库客户端/核对报告/功能实现检查报告.md @@ -1,6 +1,6 @@ # 功能实现检查报告 -**检查日期**:2025-01-28 +**检查日期**:2026-01-28 **检查范围**:各功能模块实现情况检查 **状态**:✅ 核心功能已完成 diff --git a/docs/04-功能迭代/GO-DESK-2.数据库客户端/核对报告/完善性检查报告.md b/docs/04-功能迭代/GO-DESK-2.数据库客户端/核对报告/完善性检查报告.md index 99dbd38..daa39f8 100644 --- a/docs/04-功能迭代/GO-DESK-2.数据库客户端/核对报告/完善性检查报告.md +++ b/docs/04-功能迭代/GO-DESK-2.数据库客户端/核对报告/完善性检查报告.md @@ -1,6 +1,6 @@ # 数据库客户端完善性检查报告 -**检查日期**:2025-01-28 +**检查日期**:2026-01-28 **检查人**:JueChen > **注意**:本文档内容已合并到[综合检查报告.md](./综合检查报告.md),请优先查看综合检查报告。本文档保留作为历史记录。 diff --git a/docs/04-功能迭代/GO-DESK-2.数据库客户端/核对报告/综合检查报告.md b/docs/04-功能迭代/GO-DESK-2.数据库客户端/核对报告/综合检查报告.md index 826ddfa..1935b15 100644 --- a/docs/04-功能迭代/GO-DESK-2.数据库客户端/核对报告/综合检查报告.md +++ b/docs/04-功能迭代/GO-DESK-2.数据库客户端/核对报告/综合检查报告.md @@ -1,6 +1,6 @@ # 数据库客户端综合检查报告 -**检查日期**:2025-01-28 +**检查日期**:2026-01-28 **检查人**:JueChen **检查范围**:架构、代码、编译、完善性全面检查 diff --git a/docs/04-功能迭代/GO-DESK-2.数据库客户端/核对报告/表结构功能实现说明.md b/docs/04-功能迭代/GO-DESK-2.数据库客户端/核对报告/表结构功能实现说明.md index b9b1d2d..94379ba 100644 --- a/docs/04-功能迭代/GO-DESK-2.数据库客户端/核对报告/表结构功能实现说明.md +++ b/docs/04-功能迭代/GO-DESK-2.数据库客户端/核对报告/表结构功能实现说明.md @@ -700,7 +700,7 @@ Redis: GetKeyInfo → 命令查询 --- -**实现时间**: 2025-01-XX +**实现时间**: 2026-01-XX **状态**: ✅ 已完成 **测试状态**: ⏳ 待用户测试 diff --git a/docs/04-功能迭代/GO-DESK-2.数据库客户端/核对报告/超级工程师推进总结.md b/docs/04-功能迭代/GO-DESK-2.数据库客户端/核对报告/超级工程师推进总结.md index b88f19b..c3287e9 100644 --- a/docs/04-功能迭代/GO-DESK-2.数据库客户端/核对报告/超级工程师推进总结.md +++ b/docs/04-功能迭代/GO-DESK-2.数据库客户端/核对报告/超级工程师推进总结.md @@ -1,6 +1,6 @@ # 超级工程师推进总结 -**日期**:2025-01-28 +**日期**:2026-01-28 **推进范围**:代码质量检查、问题修复、表结构编辑功能实现 --- diff --git a/docs/04-功能迭代/GO-DESK-2.数据库客户端/测试用例/功能测试用例.md b/docs/04-功能迭代/GO-DESK-2.数据库客户端/测试用例/功能测试用例.md index 499ff9c..314ecfc 100644 --- a/docs/04-功能迭代/GO-DESK-2.数据库客户端/测试用例/功能测试用例.md +++ b/docs/04-功能迭代/GO-DESK-2.数据库客户端/测试用例/功能测试用例.md @@ -1,6 +1,6 @@ # 功能测试用例 -**创建日期**:2025-01-28 +**创建日期**:2026-01-28 **测试范围**:数据库客户端核心功能 --- diff --git a/docs/04-功能迭代/GO-DESK-2.数据库客户端/知识库/参考/技术栈.md b/docs/04-功能迭代/GO-DESK-2.数据库客户端/知识库/参考/技术栈.md index 271b9f3..5932a71 100644 --- a/docs/04-功能迭代/GO-DESK-2.数据库客户端/知识库/参考/技术栈.md +++ b/docs/04-功能迭代/GO-DESK-2.数据库客户端/知识库/参考/技术栈.md @@ -1,7 +1,7 @@ # 技术栈参考 **状态**:已确定 -**最后更新**:2025-01-28 +**最后更新**:2026-01-28 --- diff --git a/docs/04-功能迭代/GO-DESK-2.数据库客户端/知识库/规范/AI协作检查清单.md b/docs/04-功能迭代/GO-DESK-2.数据库客户端/知识库/规范/AI协作检查清单.md index 6f5767c..c0cbba9 100644 --- a/docs/04-功能迭代/GO-DESK-2.数据库客户端/知识库/规范/AI协作检查清单.md +++ b/docs/04-功能迭代/GO-DESK-2.数据库客户端/知识库/规范/AI协作检查清单.md @@ -1,7 +1,7 @@ # AI协作检查清单 **状态**:已确定 -**最后更新**:2025-01-28 +**最后更新**:2026-01-28 --- diff --git a/docs/04-功能迭代/GO-DESK-2.数据库客户端/知识库/规范/文档编写规范.md b/docs/04-功能迭代/GO-DESK-2.数据库客户端/知识库/规范/文档编写规范.md index 0229c78..ce69840 100644 --- a/docs/04-功能迭代/GO-DESK-2.数据库客户端/知识库/规范/文档编写规范.md +++ b/docs/04-功能迭代/GO-DESK-2.数据库客户端/知识库/规范/文档编写规范.md @@ -1,7 +1,7 @@ # 文档编写规范 **状态**:已确定 -**最后更新**:2025-01-28 +**最后更新**:2026-01-28 --- diff --git a/docs/04-功能迭代/GO-DESK-2.数据库客户端/知识库/规范/架构规范.md b/docs/04-功能迭代/GO-DESK-2.数据库客户端/知识库/规范/架构规范.md index 4638ea7..b4129fd 100644 --- a/docs/04-功能迭代/GO-DESK-2.数据库客户端/知识库/规范/架构规范.md +++ b/docs/04-功能迭代/GO-DESK-2.数据库客户端/知识库/规范/架构规范.md @@ -1,7 +1,7 @@ # 架构规范 **状态**:已确定 -**最后更新**:2025-01-28 +**最后更新**:2026-01-28 --- diff --git a/docs/04-功能迭代/GO-DESK-2.数据库客户端/知识库/规范/编码规范.md b/docs/04-功能迭代/GO-DESK-2.数据库客户端/知识库/规范/编码规范.md index 28e333a..fecd2f7 100644 --- a/docs/04-功能迭代/GO-DESK-2.数据库客户端/知识库/规范/编码规范.md +++ b/docs/04-功能迭代/GO-DESK-2.数据库客户端/知识库/规范/编码规范.md @@ -1,7 +1,7 @@ # 编码规范 **状态**:已确定 -**最后更新**:2025-01-28 +**最后更新**:2026-01-28 --- diff --git a/docs/04-功能迭代/GO-DESK-2.数据库客户端/行动建议.md b/docs/04-功能迭代/GO-DESK-2.数据库客户端/行动建议.md index fc07cfa..7b084c7 100644 --- a/docs/04-功能迭代/GO-DESK-2.数据库客户端/行动建议.md +++ b/docs/04-功能迭代/GO-DESK-2.数据库客户端/行动建议.md @@ -1,6 +1,6 @@ # 下一步行动建议 -**更新日期**:2025-01-28 +**更新日期**:2026-01-28 **MVP状态**:✅ 已达到发布标准 **优先级**:按P0 → P1 → P2顺序 @@ -196,7 +196,7 @@ **MVP完成度**:约90%(核心功能100%,重要功能100%) -**MVP状态**:✅ **已达到发布标准,可以发布MVP v1.0版本** +**MVP状态**:🔄 **试验阶段,功能开发中** 详细检查结果请参考:[MVP发布检查.md](./核对报告/MVP发布检查.md) diff --git a/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/MVP开发路线图.md b/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/MVP开发路线图.md index afa07cd..71fce2b 100644 --- a/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/MVP开发路线图.md +++ b/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/MVP开发路线图.md @@ -1,6 +1,6 @@ # MVP开发路线图 -**创建日期**:2025-01-28 +**创建日期**:2026-01-28 **基于**:[MVP规划.md](./MVP规划.md) **目标**:以MVP为方向指引任务推进 @@ -26,7 +26,7 @@ ## 二、MVP开发路线图 -### 阶段1:核心功能 ✅ 已完成(2025-01-28) +### 阶段1:核心功能 ✅ 已完成(2026-01-28) - ✅ 连接管理、SQL执行、表结构查看、右键菜单 ### 阶段2:重要功能 ✅ 已完成 diff --git a/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/MVP规划.md b/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/MVP规划.md index decf665..05f5068 100644 --- a/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/MVP规划.md +++ b/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/MVP规划.md @@ -1,6 +1,6 @@ # 数据库客户端 MVP(最小可用产品)规划 -**创建日期**:2025-01-28 +**创建日期**:2026-01-28 **目标**:定义最小可用产品范围,指导开发优先级 **原则**:核心功能优先,快速验证,迭代优化 @@ -145,14 +145,14 @@ - ✅ 表结构查看 - ✅ 右键菜单 -**完成时间**:2025-01-28 +**完成时间**:2026-01-28 ### 阶段2:重要功能 ⚠️ 进行中 - ✅ 书签管理(基本完成) - ✅ 模板管理(基本完成) - ⚠️ 表结构编辑(基础框架完成,待完善) -**预计完成时间**:2025-01-29 +**预计完成时间**:2026-01-29 ### 阶段3:优化功能 ⬜ 待开始 - ⬜ 性能优化 diff --git a/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/功能设计/书签模板历史功能定位设计.md b/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/功能设计/书签模板历史功能定位设计.md index cd5e1cf..4b6ebcd 100644 --- a/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/功能设计/书签模板历史功能定位设计.md +++ b/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/功能设计/书签模板历史功能定位设计.md @@ -1,6 +1,6 @@ # SQL历史功能设计 -**设计日期**:2025-01-28 +**设计日期**:2026-01-28 **设计目标**:明确SQL历史功能的设计,SQL由SQL编辑区保存得到 --- diff --git a/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/功能设计/多表结构查看方案分析.md b/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/功能设计/多表结构查看方案分析.md index 8573253..64a9b11 100644 --- a/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/功能设计/多表结构查看方案分析.md +++ b/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/功能设计/多表结构查看方案分析.md @@ -1,6 +1,6 @@ # 多表结构查看方案分析 -**分析日期**:2025-01-28 +**分析日期**:2026-01-28 **分析范围**:多表结构查看的不同实现方案 **状态**:方案分析 diff --git a/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/功能设计/左侧资源管理面板设计.md b/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/功能设计/左侧资源管理面板设计.md index 0b6227b..d77e7b0 100644 --- a/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/功能设计/左侧资源管理面板设计.md +++ b/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/功能设计/左侧资源管理面板设计.md @@ -1,6 +1,6 @@ # 左侧资源管理面板设计 -**设计日期**:2025-01-28 +**设计日期**:2026-01-28 **设计目标**:在左侧功能区下方增加资源管理面板,统一管理SQL编辑器历史、书签和SQL模板 --- diff --git a/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/功能设计/新表创建功能设计.md b/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/功能设计/新表创建功能设计.md index a8cb59f..bc55bc7 100644 --- a/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/功能设计/新表创建功能设计.md +++ b/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/功能设计/新表创建功能设计.md @@ -1,6 +1,6 @@ # 新表创建功能设计 -**设计日期**:2025-01-28 +**设计日期**:2026-01-28 **设计范围**:MySQL、MongoDB、Redis 新表/集合/Key创建功能设计 **状态**:设计阶段 diff --git a/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/功能设计/表结构查看功能设计-待讨论问题.md b/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/功能设计/表结构查看功能设计-待讨论问题.md index 2f12237..234b309 100644 --- a/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/功能设计/表结构查看功能设计-待讨论问题.md +++ b/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/功能设计/表结构查看功能设计-待讨论问题.md @@ -1,6 +1,6 @@ # 表结构查看功能 - 待讨论问题 -**创建日期**:2025-01-28 +**创建日期**:2026-01-28 **目的**:整理设计文档中需要进一步讨论和明确的问题 --- diff --git a/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/功能设计/表结构查看功能设计.md b/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/功能设计/表结构查看功能设计.md index ebac445..6251064 100644 --- a/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/功能设计/表结构查看功能设计.md +++ b/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/功能设计/表结构查看功能设计.md @@ -1,6 +1,6 @@ # 表结构查看功能设计 -**设计日期**:2025-01-28 +**设计日期**:2026-01-28 **设计范围**:MySQL、Redis、MongoDB 表结构查看界面设计 **状态**:设计阶段 @@ -152,7 +152,7 @@ │ "name": "John", │ │ "email": "john@example.com", │ │ "age": 30, │ -│ "created_at": ISODate("2025-01-01T00:00:00Z") │ +│ "created_at": ISODate("2026-01-01T00:00:00Z") │ │ } │ └─────────────────────────────────────────────────────────────┘ [显示最多 5 个文档示例,JSON 格式,可折叠展开] diff --git a/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/架构设计/事件系统设计.md b/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/架构设计/事件系统设计.md index 43805b9..d11c7a0 100644 --- a/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/架构设计/事件系统设计.md +++ b/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/架构设计/事件系统设计.md @@ -1,6 +1,6 @@ # 事件系统设计 -**设计日期**:2025-01-28 +**设计日期**:2026-01-28 **设计范围**:数据库客户端全局事件系统 **状态**:设计阶段 diff --git a/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/架构设计/前端架构设计.md b/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/架构设计/前端架构设计.md index 385a354..0a59cd2 100644 --- a/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/架构设计/前端架构设计.md +++ b/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/架构设计/前端架构设计.md @@ -3,7 +3,7 @@ **文档版本**:v2.0 **维护者**:JueChen -**更新日期**:2025-01-28 +**更新日期**:2026-01-28 **源码路径**:`go-desk/web/src/views/db-cli/` --- diff --git a/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/架构设计/右键菜单系统设计.md b/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/架构设计/右键菜单系统设计.md index b70c6fc..4f21d6b 100644 --- a/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/架构设计/右键菜单系统设计.md +++ b/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/架构设计/右键菜单系统设计.md @@ -1,6 +1,6 @@ # 右键菜单系统设计 -**设计日期**:2025-01-28 +**设计日期**:2026-01-28 **设计范围**:数据库客户端全局右键菜单系统 **状态**:设计阶段 diff --git a/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/架构设计/后端架构设计.md b/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/架构设计/后端架构设计.md index 44c3a8b..92881c5 100644 --- a/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/架构设计/后端架构设计.md +++ b/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/架构设计/后端架构设计.md @@ -2,7 +2,7 @@ **文档版本**:v2.0 **维护者**:JueChen -**更新日期**:2025-01-28 +**更新日期**:2026-01-28 **源码路径**:`go-desk/` --- diff --git a/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/需求设计/前端布局样式系统设计.md b/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/需求设计/前端布局样式系统设计.md index 3d87321..5f6d740 100644 --- a/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/需求设计/前端布局样式系统设计.md +++ b/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/需求设计/前端布局样式系统设计.md @@ -1,7 +1,7 @@ # 前端布局样式系统设计 **创建日期**:2026-01-01 -**最后更新**:2025-01-09 +**最后更新**:2026-01-09 **目标**:建立系统化的前端布局和样式规范,确保一致性和可维护性 **原则**:统一规范、可扩展、易维护、主题兼容 **状态**:✅ 已完成 Arco Design 规范优化 diff --git a/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/需求设计/数据库类型功能差异分析.md b/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/需求设计/数据库类型功能差异分析.md index 3322116..52061ea 100644 --- a/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/需求设计/数据库类型功能差异分析.md +++ b/docs/04-功能迭代/GO-DESK-2.数据库客户端/设计文档/需求设计/数据库类型功能差异分析.md @@ -1,6 +1,6 @@ # 数据库类型功能差异分析 -**分析日期**:2025-01-28 +**分析日期**:2026-01-28 **分析范围**:MySQL、Redis、MongoDB 功能支持差异 --- diff --git a/docs/04-功能迭代/GO-DESK-2.数据库客户端/问题追踪/待实现/功能-001-右键菜单系统实现.md b/docs/04-功能迭代/GO-DESK-2.数据库客户端/问题追踪/待实现/功能-001-右键菜单系统实现.md index 9d23fc7..e369b35 100644 --- a/docs/04-功能迭代/GO-DESK-2.数据库客户端/问题追踪/待实现/功能-001-右键菜单系统实现.md +++ b/docs/04-功能迭代/GO-DESK-2.数据库客户端/问题追踪/待实现/功能-001-右键菜单系统实现.md @@ -2,7 +2,7 @@ **状态**:✅ 基本实现完成(待测试验证) **优先级**:P0 -**创建日期**:2025-01-28 +**创建日期**:2026-01-28 **关联设计**:[设计文档/架构设计/右键菜单系统设计.md](../../设计文档/架构设计/右键菜单系统设计.md) ## 功能描述 diff --git a/docs/04-功能迭代/GO-DESK-2.数据库客户端/问题追踪/待讨论/问题-001-右键菜单实现方式.md b/docs/04-功能迭代/GO-DESK-2.数据库客户端/问题追踪/待讨论/问题-001-右键菜单实现方式.md index eacb25b..744f27f 100644 --- a/docs/04-功能迭代/GO-DESK-2.数据库客户端/问题追踪/待讨论/问题-001-右键菜单实现方式.md +++ b/docs/04-功能迭代/GO-DESK-2.数据库客户端/问题追踪/待讨论/问题-001-右键菜单实现方式.md @@ -2,7 +2,7 @@ **状态**:已解决 **优先级**:P0 -**提出日期**:2025-01-28 +**提出日期**:2026-01-28 **提出人**:开发团队 ## 问题描述 @@ -47,7 +47,7 @@ ## 讨论记录 -- 2025-01-28:已创建设计文档 [设计文档/架构设计/右键菜单系统设计.md](../../设计文档/架构设计/右键菜单系统设计.md) +- 2026-01-28:已创建设计文档 [设计文档/架构设计/右键菜单系统设计.md](../../设计文档/架构设计/右键菜单系统设计.md) ## 决策 @@ -55,7 +55,7 @@ **决策记录**:[ADR-003: 右键菜单实现方案](../../决策记录/ADR-003-右键菜单实现方案.md) -**决策日期**:2025-01-28 +**决策日期**:2026-01-28 **理由**: 1. 符合Arco Design设计规范 diff --git a/docs/PROJECT_STATUS.md b/docs/PROJECT_STATUS.md deleted file mode 100644 index 00f3b17..0000000 --- a/docs/PROJECT_STATUS.md +++ /dev/null @@ -1,157 +0,0 @@ -# U-Desk 项目状态 - -**更新日期**:2025-01-28 -**版本**:v0.2.0 (开发中) -**状态**:🚧 开发版本 - ---- - -## 📊 项目概览 - -U-Desk 是基于 Wails 的桌面应用程序,集成了数据库客户端、文件管理、设备测试等功能。 - -### 核心模块 - -| 模块 | 状态 | 说明 | -|------|------|------| -| 数据库客户端 | ✅ 完成 | 支持 MySQL、Redis、MongoDB | -| 文件管理 | ✅ 完成 | 模块化架构,支持预览和操作 | -| 设备测试 | ✅ 完成 | 系统设备信息查询 | -| 更新管理 | ✅ 完成 | 应用版本检查和自动更新 | - ---- - -## 🎯 最近更新 (2025-01-28) - -### 架构优化 -- ✅ **文件系统模块化重构**:将文件管理功能拆分为多个独立模块 - - `path_validator.go` - 路径验证 - - `filetype_manager.go` - 文件类型管理 - - `directory_stats.go` - 目录统计 - - `audit_log.go` - 审计日志 - - `file_lock.go` - 文件锁 - - `recycle_bin.go` - 回收站 - - `zip.go` / `zip_helper.go` - ZIP 压缩 - - `service.go` - 核心服务 - - `asset_handler.go` - 资源处理 - -- ✅ **应用启动流程优化**: - - SQLite 快速初始化 - - 核心 API 同步初始化 - - 文件服务器异步启动 - - UpdateAPI 异步初始化(涉及网络请求) - -### 前端优化 -- ✅ 新增 `CodeEditor.vue` 组件 -- ✅ 新增 Composables: - - `useFileOperations.js` - 文件操作 - - `useFavoriteFiles.js` - 收藏文件 - - `useLocalStorage.js` - 本地存储 -- ✅ 新增工具函数: - - `constants.js` - 常量定义 - - `fileUtils.js` - 文件工具 - - `debugLog.js` - 调试日志 - -### 数据库客户端 -- ✅ MVP 功能全部完成 -- ✅ 右键菜单系统实现 -- ✅ 表结构查看功能(MySQL、MongoDB、Redis) -- ✅ 测试连接功能 - ---- - -## 📚 文档 - -### 设计文档 -- `docs/04-功能迭代/GO-DESK-1.尝试/` - 应用初始化和设备测试 -- `docs/04-功能迭代/GO-DESK-2.数据库客户端/` - 数据库客户端完整文档 - -### 重构文档 -- `docs/filesystem-*.md` - 文件系统重构系列文档 -- `docs/架构改进*.md` - 架构改进文档 - ---- - -## 🚀 快速开始 - -### 开发环境 - -```bash -# 安装依赖 -go mod tidy -cd web && npm install - -# 构建前端 -cd web && npm run build - -# 开发模式 -wails dev -``` - -### 构建 - -```bash -# 构建应用 -wails build - -# 产物位置 -build/bin/go-desk.exe -``` - ---- - -## 🔧 技术栈 - -- **后端**:Go 1.25+、Wails v2 -- **前端**:Vue 3、Arco Design Vue、Vite -- **存储**:SQLite、MySQL、Redis、MongoDB - ---- - -## 📋 待办事项 - -### P0 (高优先级) -- [ ] 完善表结构编辑功能 -- [ ] 性能优化 -- [ ] 错误处理优化 - -### P1 (中优先级) -- [ ] 数据导出、导入功能 -- [ ] 查询历史管理 -- [ ] 结果集分页和筛选 - -### P2 (低优先级) -- [ ] 多数据库类型支持扩展 -- [ ] 高级功能(数据同步、备份等) - ---- - -## 📝 版本历史 - -### v0.2.0 (2025-01-28) -- ✅ 模块重命名:go-desk → u-desk -- ✅ 依赖更新:所有依赖包更新到最新版本 -- ✅ 文档更新:版本号调整为开发版本 - -### v0.1.0 (2025-01-28) -- ✅ 文件系统模块化重构 -- ✅ 应用启动流程优化 -- ✅ 数据库客户端 MVP 完成 -- ✅ 文档更新 - -### v0.9.0 (2025-01-27) -- ✅ 文件管理功能 -- ✅ 设备测试功能 -- ✅ 更新管理功能 - ---- - -## 👥 贡献 - -本项目用于学习和测试目的。 - ---- - -## 📄 许可 - -本项目仅供学习和测试使用。 diff --git a/docs/components-analysis.md b/docs/components-analysis.md deleted file mode 100644 index 86b2548..0000000 --- a/docs/components-analysis.md +++ /dev/null @@ -1,1156 +0,0 @@ -# Components 代码分析报告 - -## 一、组件概览 - -| 组件名称 | 行数 | 主要功能 | 复杂度 | -|---------|------|----------|--------| -| DeviceTest.vue | 738 | 设备测试、系统信息、基础文件操作 | 中等 | -| FileSystem.vue | 1374 | 完整文件系统管理、媒体预览 | 高 | -| ThemeToggle.vue | 50 | 主题切换 | 低 | -| UpdatePanel.vue | 428 | 版本更新管理 | 中等 | -| **总计** | **2590** | - | - | - ---- - -## 二、代码重复度分析 - -### 2.1 高度重复的代码模块 - -#### **重复点 1: localStorage 操作逻辑(100% 相同)** - -**位置**: -- DeviceTest.vue: 477-521 行(44 行) -- FileSystem.vue: 882-924 行(42 行) - -**重复代码**: -```javascript -// 完全相同的函数 -const loadFromStorage = () => { - try { - const savedPath = localStorage.getItem(STORAGE_KEYS.FILE_PATH) - const savedFileList = localStorage.getItem(STORAGE_KEYS.FILE_LIST) - const savedFileContent = localStorage.getItem(STORAGE_KEYS.FILE_CONTENT) - const savedHistory = localStorage.getItem(STORAGE_KEYS.PATH_HISTORY) - const savedHeight = localStorage.getItem(STORAGE_KEYS.FILE_CONTENT_HEIGHT) - const savedFavorites = localStorage.getItem(STORAGE_KEYS.FAVORITE_FILES) - // ... 省略加载逻辑 - } catch (error) { - console.error('从 localStorage 加载数据失败:', error) - } -} - -const saveToStorage = (key, value) => { - try { - if (typeof value === 'string') { - localStorage.setItem(key, value) - } else { - localStorage.setItem(key, JSON.stringify(value)) - } - } catch (error) { - console.error('保存到 localStorage 失败:', error) - } -} -``` - -**重复行数**: 86 行 -**占比**: 11.7% - ---- - -#### **重复点 2: 收藏夹功能(95% 相同)** - -**位置**: -- DeviceTest.vue: 544-594 行(50 行) -- FileSystem.vue: 837-879 行(42 行) - -**重复代码**: -```javascript -// 完全相同的三个函数 -const isFavorite = (path) => { - return favoriteFiles.value.some(fav => fav.path === path) -} - -const toggleFavorite = (item) => { - const index = favoriteFiles.value.findIndex(fav => fav.path === item.path) - if (index > -1) { - favoriteFiles.value.splice(index, 1) - Message.info('已取消收藏: ' + item.name) - } else { - favoriteFiles.value.push({ - path: item.path, - name: item.name, - is_dir: item.is_dir - }) - Message.success('已收藏: ' + item.name) - } - saveToStorage(STORAGE_KEYS.FAVORITE_FILES, favoriteFiles.value) -} - -const removeFavorite = (path) => { - // 相同实现 -} - -const openFavoriteFile = (path) => { - // 相同实现 -} -``` - -**重复行数**: 92 行 -**占比**: 12.5% - ---- - -#### **重复点 3: 路径历史记录(100% 相同)** - -**位置**: -- DeviceTest.vue: 523-542 行(19 行) -- FileSystem.vue: 820-834 行(14 行) - -**重复代码**: -```javascript -const addToHistory = (path) => { - if (!path || path.trim() === '') return - - const index = pathHistory.value.indexOf(path) - if (index > -1) { - pathHistory.value.splice(index, 1) - } - - pathHistory.value.unshift(path) - if (pathHistory.value.length > 20) { - pathHistory.value = pathHistory.value.slice(0, 20) - } - - saveToStorage(STORAGE_KEYS.PATH_HISTORY, pathHistory.value) -} -``` - -**重复行数**: 33 行 -**占比**: 4.5% - ---- - -#### **重复点 4: 文件大小格式化(100% 相同)** - -**位置**: -- DeviceTest.vue: 401-407 行(6 行) -- FileSystem.vue: 394-400 行(6 行) - -**重复代码**: -```javascript -const formatBytes = (bytes) => { - if (!bytes) return '0 B' - const unit = 1024 - if (bytes < unit) return bytes + ' B' - const exp = Math.floor(Math.log(bytes) / Math.log(unit)) - return (bytes / Math.pow(unit, exp)).toFixed(2) + ' ' + 'KMGTPE'[exp - 1] + 'B' -} -``` - -**重复行数**: 12 行 -**占比**: 1.6% - ---- - -#### **重复点 5: 基础文件操作(90% 相同)** - -**位置**: -- DeviceTest.vue: 275-363 行(88 行) -- FileSystem.vue: 491-783 行(292 行,包含更多预览功能) - -**重复代码**: -```javascript -// 列出目录 -const listDirectory = async () => { - if (!filePath.value) return - addToHistory(filePath.value) - fileLoading.value = true - try { - fileList.value = await listDir(filePath.value) - } catch (error) { - Message.error('列出目录失败: ' + error.message) - } finally { - fileLoading.value = false - } -} - -// 选择文件 -const selectFile = (path) => { - filePath.value = path - addToHistory(path) - const item = fileList.value.find(f => f.path === path) - if (item && item.is_dir) { - listDirectory() - } else { - readFile() - } -} - -// 读取文件(基础版) -const readFile = async () => { - // 相同的错误处理和加载逻辑 -} - -// 写入文件 -const writeFile = async () => { - // 相同实现 -} - -// 删除文件 -const deleteFile = async () => { - // 相同的 Modal 确认逻辑 -} -``` - -**重复行数**: 约 150 行 -**占比**: 20.4% - ---- - -#### **重复点 6: 拖拽调整功能(85% 相同)** - -**位置**: -- DeviceTest.vue: 409-475 行(66 行) -- FileSystem.vue: 410-437 行(27 行,简化版) - -**重复代码**: -```javascript -// 垂直拖拽 -const startResize = (e) => { - const startY = e.clientY - const startHeight = fileContentHeight.value - const onMouseMove = (moveEvent) => { - const deltaY = moveEvent.clientY - startY - const newHeight = startHeight + deltaY - if (newHeight >= 100 && newHeight <= 800) { - fileContentHeight.value = newHeight - } - } - const onMouseUp = () => { - document.removeEventListener('mousemove', onMouseMove) - document.removeEventListener('mouseup', onMouseUp) - saveToStorage(STORAGE_KEYS.FILE_CONTENT_HEIGHT, fileContentHeight.value.toString()) - } - document.addEventListener('mousemove', onMouseMove) - document.addEventListener('mouseup', onMouseUp) -} - -// 水平拖拽 -const startHorizontalResize = (e) => { - // 相同的实现模式 -} -``` - -**重复行数**: 66 行 -**占比**: 9.0% - ---- - -### 2.2 代码重复度汇总 - -| 重复模块 | DeviceTest | FileSystem | 总重复行数 | 占比 | -|---------|-----------|------------|-----------|------| -| localStorage 操作 | 44 | 42 | 86 | 11.7% | -| 收藏夹功能 | 50 | 42 | 92 | 12.5% | -| 路径历史记录 | 19 | 14 | 33 | 4.5% | -| 文件大小格式化 | 6 | 6 | 12 | 1.6% | -| 基础文件操作 | 88 | 150 | 150 | 20.4% | -| 拖拽调整功能 | 66 | 27 | 66 | 9.0% | -| **总计** | **273** | **281** | **439** | **59.7%** | - -**结论**:两个组件之间的代码重复率高达 **59.7%**,存在大量可抽取的公共逻辑。 - ---- - -## 三、抽象一致性分析 - -### 3.1 API 调用方式 ✅ 一致 - -**DeviceTest.vue**: -```javascript -import { - listDir, - readFile as readFileApi, - writeFile as writeFileApi, - deletePath -} from '@/api' -``` - -**FileSystem.vue**: -```javascript -import { - listDir, - readFile as readFileApi, - writeFile as writeFileApi, - deletePath -} from '@/api' -``` - -**结论**: API 调用方式完全一致,符合统一抽象原则。 - ---- - -### 3.2 localStorage 键名规范 ❌ 不一致 - -**DeviceTest.vue**: -```javascript -const STORAGE_KEYS = { - FILE_PATH: 'device-test-file-path', - FILE_LIST: 'device-test-file-list', - FILE_CONTENT: 'device-test-file-content', - PATH_HISTORY: 'device-test-path-history', - FILE_CONTENT_HEIGHT: 'device-test-file-content-height', - FAVORITE_FILES: 'device-test-favorite-files', - FILE_PANEL_WIDTH: 'device-test-file-panel-width' -} -``` - -**FileSystem.vue**: -```javascript -const STORAGE_KEYS = { - FILE_PATH: 'filesystem-file-path', - FILE_LIST: 'filesystem-file-list', - FILE_CONTENT: 'filesystem-file-content', - PATH_HISTORY: 'filesystem-path-history', - FILE_CONTENT_HEIGHT: 'filesystem-file-content-height', - FAVORITE_FILES: 'filesystem-favorite-files' -} -``` - -**问题**: -1. 键名前缀不统一:`device-test-` vs `filesystem-` -2. FileSystem.vue 缺少 `FILE_PANEL_WIDTH` 键 -3. 没有统一的键名管理策略 - -**建议**: 使用统一的键名前缀,如 `app-filesystem-`,或使用命名空间对象。 - ---- - -### 3.3 组件结构和命名规范 ⚠️ 部分一致 - -**相似点**: -- 两者都使用相同的 `ref` 命名:`filePath`, `fileContent`, `fileList`, `fileLoading` -- 都使用 `STORAGE_KEYS` 常量对象管理 localStorage 键名 -- 都使用 `addToHistory`, `toggleFavorite`, `removeFavorite` 等相同命名 - -**不同点**: -- DeviceTest.vue 使用 `isResizing` 和 `isResizingHorizontal` -- FileSystem.vue 使用 `showSidebar` 和 `panelWidth` -- FileSystem.vue 引入了更多状态:`isImageFile`, `isVideoFile`, `isAudioFile`, `isPdfFile` - -**建议**: -- 统一状态命名规范 -- 使用 TypeScript 接口定义状态类型 -- 抽取公共状态到 composable - ---- - -### 3.4 错误处理模式 ✅ 一致 - -两者都使用相同的错误处理模式: - -```javascript -try { - // API 调用 -} catch (error) { - Message.error('操作失败: ' + error.message) -} finally { - fileLoading.value = false -} -``` - -**结论**: 错误处理模式一致,符合最佳实践。 - ---- - -## 四、复杂度评估 - -### 4.1 FileSystem.vue 复杂度分析 - -**行数**: 1374 行(含样式) - -**函数数量**: -- 工具函数:8 个(formatBytes, getFileName, normalizeFilePath, getFileIcon 等) -- 文件操作函数:10 个(listDirectory, readFile, writeFile, deleteFile 等) -- 预览函数:6 个(previewImage, previewVideo, previewAudio, previewPdf 等) -- UI 交互函数:8 个(startResize, startResizeHorizontal, addToHistory 等) -- 生命周期函数:2 个(onMounted, watch) - -**总计**: 34 个函数 - -**复杂度问题**: -1. ❌ **过度耦合**: 预览逻辑与文件操作逻辑耦合在一个组件中 -2. ❌ **过长函数**: `readFile` 函数(56 行)包含太多分支逻辑 -3. ❌ **状态过多**: 15+ 个 ref 状态,管理复杂 -4. ❌ **重复代码**: 大量与 DeviceTest.vue 重复的逻辑 - ---- - -### 4.2 DeviceTest.vue 复杂度分析 - -**行数**: 738 行(含样式) - -**函数数量**: -- 系统信息函数:2 个(refreshSystemInfo, loadEnvVars) -- 文件操作函数:8 个(listDirectory, readFile, writeFile 等) -- UI 交互函数:6 个(startResize, startHorizontalResize 等) -- 工具函数:2 个(formatBytes, addToHistory) -- 收藏夹函数:4 个(isFavorite, toggleFavorite 等) - -**总计**: 22 个函数 - -**复杂度评估**: -- ✅ 相对简洁,职责单一 -- ⚠️ 但仍包含与 FileSystem.vue 重复的代码 - ---- - -### 4.3 过度设计评估 - -**FileSystem.vue 中的过度设计部分**: - -1. **媒体预览功能过于复杂**(171-246 行模板 + 603-707 行脚本): - ```javascript - // 多个预览函数可以合并 - const previewImage = async () => { /* 24 行 */ } - const previewVideo = () => previewMedia('video') // 可以简化 - const previewAudio = () => previewMedia('audio') // 可以简化 - const previewPdf = () => previewMedia('pdf') // 可以简化 - const previewMedia = (mediaType) => { /* 27 行 */ } - const openImageExternally = async (imagePath) => { /* 11 行 */ } - const onImageLoad = () => { /* 3 行 */ } - const onImageError = () => { /* 5 行 */ } - ``` - **建议**: 抽取为独立的 `FilePreviewer` 组件 - -2. **文件类型判断逻辑重复**(286-298 行 + 444-488 行): - ```javascript - // FILE_EXTENSIONS 常量定义 - const FILE_EXTENSIONS = { - IMAGE: ['jpg', 'jpeg', 'png', ...], - VIDEO_BROWSER: ['mp4', 'webm', ...], - VIDEO_EXTERNAL: ['avi', 'mkv', ...], - AUDIO: ['mp3', 'wav', ...], - DOCUMENT: ['doc', 'docx', ...], - ARCHIVE: ['zip', 'rar', ...], - CODE: ['js', 'ts', ...], - DATABASE: ['db', 'sqlite', ...], - EXECUTABLE: ['exe', 'msi', ...], - FONT: ['ttf', 'otf', ...] - } - - // getFileIcon 函数中再次列出这些类型 - const getFileIcon = (item) => { - if (item.is_dir) return '📁' - const ext = item.name.split('.').pop()?.toLowerCase() || '' - if (FILE_EXTENSIONS.IMAGE.includes(ext)) return '🖼️' - if ([...FILE_EXTENSIONS.VIDEO_BROWSER, ...FILE_EXTENSIONS.VIDEO_EXTERNAL].includes(ext)) return '🎬' - // ... 重复的类型判断 - } - ``` - **建议**: 统一类型判断逻辑,使用配置映射 - -3. **拖拽功能重复实现**(410-437 行): - ```javascript - const startResizeHorizontal = (e) => { - // 与 DeviceTest.vue 中的 startHorizontalResize 几乎相同 - // 仅变量名不同(panelWidth vs filePanelWidth) - } - ``` - **建议**: 抽取为 composable - ---- - -## 五、组件化建议 - -### 5.1 建议的公共 Composables - -#### **1. useLocalStorage.js** -```javascript -// hooks/useLocalStorage.js -export function useLocalStorage(key, defaultValue) { - const storedValue = ref(defaultValue) - - const load = () => { - try { - const item = localStorage.getItem(key) - if (item) storedValue.value = JSON.parse(item) - } catch (error) { - console.error('Load from localStorage failed:', error) - } - } - - const save = (value) => { - try { - localStorage.setItem(key, JSON.stringify(value)) - } catch (error) { - console.error('Save to localStorage failed:', error) - } - } - - watch(storedValue, (newValue) => save(newValue)) - - onMounted(() => load()) - - return { storedValue, load, save } -} -``` - -**减少代码**: 86 行 → 30 行(减少 56 行) - ---- - -#### **2. useFileOperations.js** -```javascript -// hooks/useFileOperations.js -export function useFileOperations() { - const filePath = ref('') - const fileContent = ref('') - const fileList = ref([]) - const fileLoading = ref(false) - - const listDirectory = async () => { - if (!filePath.value) return - fileLoading.value = true - try { - fileList.value = await listDir(filePath.value) - } catch (error) { - Message.error('列出目录失败: ' + error.message) - } finally { - fileLoading.value = false - } - } - - const readFile = async () => { - if (!filePath.value) return - fileLoading.value = true - try { - fileContent.value = await readFileApi(filePath.value) - } catch (error) { - Message.error('读取文件失败: ' + error.message) - } finally { - fileLoading.value = false - } - } - - const writeFile = async () => { - if (!filePath.value) return - fileLoading.value = true - try { - await writeFileApi(filePath.value, fileContent.value) - Message.success('文件保存成功') - } catch (error) { - Message.error('文件保存失败: ' + error.message) - } finally { - fileLoading.value = false - } - } - - const deleteFile = async () => { - if (!filePath.value) return - Modal.confirm({ - title: '确认删除', - content: `确定要删除 ${filePath.value} 吗?`, - onOk: async () => { - fileLoading.value = true - try { - await deletePath(filePath.value) - Message.success('删除成功') - filePath.value = '' - fileContent.value = '' - fileList.value = [] - } catch (error) { - Message.error('删除失败: ' + error.message) - } finally { - fileLoading.value = false - } - } - }) - } - - return { - filePath, - fileContent, - fileList, - fileLoading, - listDirectory, - readFile, - writeFile, - deleteFile - } -} -``` - -**减少代码**: 150 行 → 80 行(减少 70 行) - ---- - -#### **3. useFavoriteFiles.js** -```javascript -// hooks/useFavoriteFiles.js -export function useFavoriteFiles(storageKey) { - const favoriteFiles = ref([]) - - const isFavorite = (path) => { - return favoriteFiles.value.some(fav => fav.path === path) - } - - const toggleFavorite = (item) => { - const index = favoriteFiles.value.findIndex(fav => fav.path === item.path) - if (index > -1) { - favoriteFiles.value.splice(index, 1) - Message.info('已取消收藏: ' + item.name) - } else { - favoriteFiles.value.push({ - path: item.path, - name: item.name, - is_dir: item.is_dir - }) - Message.success('已收藏: ' + item.name) - } - saveFavorites() - } - - const removeFavorite = (path) => { - const index = favoriteFiles.value.findIndex(fav => fav.path === path) - if (index > -1) { - const name = favoriteFiles.value[index].name - favoriteFiles.value.splice(index, 1) - saveFavorites() - Message.info('已取消收藏: ' + name) - } - } - - const saveFavorites = () => { - saveToStorage(storageKey, favoriteFiles.value) - } - - const loadFavorites = () => { - try { - const saved = localStorage.getItem(storageKey) - if (saved) favoriteFiles.value = JSON.parse(saved) - } catch (error) { - console.error('Load favorites failed:', error) - } - } - - onMounted(() => loadFavorites()) - - return { - favoriteFiles, - isFavorite, - toggleFavorite, - removeFavorite - } -} -``` - -**减少代码**: 92 行 → 50 行(减少 42 行) - ---- - -#### **4. usePathHistory.js** -```javascript -// hooks/usePathHistory.js -export function usePathHistory(storageKey, maxLength = 20) { - const pathHistory = ref([]) - - const addToHistory = (path) => { - if (!path || path.trim() === '') return - - const index = pathHistory.value.indexOf(path) - if (index > -1) { - pathHistory.value.splice(index, 1) - } - - pathHistory.value.unshift(path) - if (pathHistory.value.length > maxLength) { - pathHistory.value = pathHistory.value.slice(0, maxLength) - } - - saveToStorage(storageKey, pathHistory.value) - } - - const loadHistory = () => { - try { - const saved = localStorage.getItem(storageKey) - if (saved) pathHistory.value = JSON.parse(saved) - } catch (error) { - console.error('Load history failed:', error) - } - } - - onMounted(() => loadHistory()) - - return { - pathHistory, - addToHistory - } -} -``` - -**减少代码**: 33 行 → 25 行(减少 8 行) - ---- - -#### **5. useResizable.js** -```javascript -// hooks/useResizable.js -export function useResizable(config) { - const { minHeight = 100, maxHeight = 800, storageKey } = config - const height = ref(config.defaultHeight || 200) - const isResizing = ref(false) - - const startResize = (e) => { - isResizing.value = true - const startY = e.clientY - const startHeight = height.value - - const onMouseMove = (moveEvent) => { - if (!isResizing.value) return - const deltaY = moveEvent.clientY - startY - const newHeight = startHeight + deltaY - if (newHeight >= minHeight && newHeight <= maxHeight) { - height.value = newHeight - } - } - - const onMouseUp = () => { - isResizing.value = false - document.removeEventListener('mousemove', onMouseMove) - document.removeEventListener('mouseup', onMouseUp) - if (storageKey) { - saveToStorage(storageKey, height.value.toString()) - } - } - - document.addEventListener('mousemove', onMouseMove) - document.addEventListener('mouseup', onMouseUp) - } - - return { - height, - isResizing, - startResize - } -} -``` - -**减少代码**: 66 行 → 35 行(减少 31 行) - ---- - -#### **6. useFilePreview.js** -```javascript -// hooks/useFilePreview.js -export function useFilePreview() { - const isImageFile = ref(false) - const isVideoFile = ref(false) - const isAudioFile = ref(false) - const isPdfFile = ref(false) - const previewUrl = ref('') - const loading = ref(false) - - const previewMedia = (filePath, mediaType) => { - // 重置所有状态 - isImageFile.value = false - isVideoFile.value = false - isAudioFile.value = false - isPdfFile.value = false - - const typeMap = { - image: () => { isImageFile.value = true }, - video: () => { isVideoFile.value = true }, - audio: () => { isAudioFile.value = true }, - pdf: () => { isPdfFile.value = true } - } - - typeMap[mediaType]?.() - previewUrl.value = `/localfs/${filePath.replace(/\\/g, '/')}` - } - - const clearPreview = () => { - isImageFile.value = false - isVideoFile.value = false - isAudioFile.value = false - isPdfFile.value = false - previewUrl.value = '' - } - - return { - isImageFile, - isVideoFile, - isAudioFile, - isPdfFile, - previewUrl, - loading, - previewMedia, - clearPreview - } -} -``` - -**减少代码**: 105 行 → 45 行(减少 60 行) - ---- - -### 5.2 建议的公共 UI 组件 - -#### **1. FileList.vue** -```vue - - - -``` - -**减少代码**: 80 行(模板部分) - ---- - -#### **2. FilePreviewer.vue** -```vue - - - -``` - -**减少代码**: 120 行(模板 + 脚本) - ---- - -#### **3. FavoriteSidebar.vue** -```vue - - - -``` - -**减少代码**: 60 行(模板 + 脚本) - ---- - -### 5.3 建议的组件层次结构 - -``` -src/ -├── components/ -│ ├── FileSystem/ -│ │ ├── index.vue # 主组件(简化后 ~400 行) -│ │ ├── FileList.vue # 文件列表组件 -│ │ ├── FilePreviewer.vue # 文件预览组件 -│ │ ├── FavoriteSidebar.vue # 收藏夹侧边栏 -│ │ └── EditorToolbar.vue # 编辑器工具栏 -│ │ -│ ├── DeviceTest/ -│ │ └── index.vue # 主组件(简化后 ~300 行) -│ │ -│ └── shared/ -│ ├── FileIcon.vue # 文件图标组件 -│ └── PathInput.vue # 路径输入组件 -│ -├── composables/ -│ ├── useFileOperations.js # 文件操作逻辑 -│ ├── useFilePreview.js # 文件预览逻辑 -│ ├── useFavoriteFiles.js # 收藏夹逻辑 -│ ├── usePathHistory.js # 路径历史逻辑 -│ ├── useResizable.js # 拖拽调整逻辑 -│ ├── useLocalStorage.js # localStorage 封装 -│ └── useSystemInfo.js # 系统信息获取 -│ -└── utils/ - ├── fileUtils.js # 文件工具函数 - │ ├── formatBytes() - │ ├── getFileName() - │ ├── getFileIcon() - │ └── normalizeFilePath() - │ - └── constants.js # 常量配置 - ├── FILE_EXTENSIONS - ├── STORAGE_KEYS - └── COMMON_PATHS -``` - ---- - -## 六、重构优先级 - -### 高优先级(立即执行) - -1. **抽取公共 Composables** - - useFileOperations.js(减少 150 行重复代码) - - useFavoriteFiles.js(减少 92 行重复代码) - - useLocalStorage.js(减少 86 行重复代码) - - usePathHistory.js(减少 33 行重复代码) - -2. **统一 localStorage 键名管理** - ```javascript - // utils/constants.js - export const STORAGE_KEYS = { - FILESYSTEM: { - FILE_PATH: 'app-filesystem-file-path', - FILE_LIST: 'app-filesystem-file-list', - // ... - }, - DEVICE_TEST: { - FILE_PATH: 'app-device-test-file-path', - FILE_LIST: 'app-device-test-file-list', - // ... - } - } - ``` - ---- - -### 中优先级(近期执行) - -3. **抽取公共 UI 组件** - - FileList.vue(减少 80 行) - - FilePreviewer.vue(减少 120 行) - - FavoriteSidebar.vue(减少 60 行) - -4. **简化媒体预览逻辑** - - 合并 `previewImage`, `previewVideo`, `previewAudio`, `previewPdf` 为统一的 `previewMedia` 函数 - - 使用 `useFilePreview` composable - ---- - -### 低优先级(长期优化) - -5. **优化 FileSystem.vue 结构** - - 拆分为多个子组件 - - 减少状态数量,使用状态机模式 - - 引入 TypeScript 类型定义 - -6. **性能优化** - - 虚拟滚动优化大文件列表 - - 图片懒加载 - - 防抖处理拖拽事件 - ---- - -## 七、重构后的预估效果 - -### 代码行数对比 - -| 组件/模块 | 重构前 | 重构后 | 减少 | 减少率 | -|----------|--------|--------|------|--------| -| DeviceTest.vue | 738 | 300 | 438 | 59.3% | -| FileSystem.vue | 1374 | 400 | 974 | 70.9% | -| 公共 Composables | 0 | 250 | -250 | - | -| 公共 UI 组件 | 0 | 200 | -200 | - | -| 工具函数 | 0 | 50 | -50 | - | -| **总计** | **2112** | **1200** | **912** | **43.2%** | - -### 可维护性提升 - -- ✅ **代码复用率**: 从 40% → 80% -- ✅ **单元测试覆盖**: 从 0% → 70% -- ✅ **类型安全**: 引入 TypeScript 后 100% -- ✅ **组件耦合度**: 从 高 → 低 -- ✅ **新增功能成本**: 降低 60% - ---- - -## 八、具体改进建议 - -### 8.1 立即行动项(本周) - -1. **创建 useFileOperations composable** - - 文件:`src/composables/useFileOperations.js` - - 预计时间:2 小时 - - 影响范围:DeviceTest.vue, FileSystem.vue - -2. **创建 useFavoriteFiles composable** - - 文件:`src/composables/useFavoriteFiles.js` - - 预计时间:1.5 小时 - - 影响范围:DeviceTest.vue, FileSystem.vue - -3. **统一 STORAGE_KEYS 常量** - - 文件:`src/utils/constants.js` - - 预计时间:1 小时 - - 影响范围:所有组件 - ---- - -### 8.2 短期计划(本月) - -4. **抽取 FileList 组件** - - 文件:`src/components/FileSystem/FileList.vue` - - 预计时间:3 小时 - - 影响范围:FileSystem.vue - -5. **抽取 FilePreviewer 组件** - - 文件:`src/components/FileSystem/FilePreviewer.vue` - - 预计时间:4 小时 - - 影响范围:FileSystem.vue - -6. **创建 useFilePreview composable** - - 文件:`src/composables/useFilePreview.js` - - 预计时间:2 小时 - - 影响范围:FileSystem.vue - ---- - -### 8.3 长期优化(下季度) - -7. **引入 TypeScript** - - 添加类型定义文件 - - 重构所有 composables 和组件 - - 预计时间:40 小时 - -8. **添加单元测试** - - 使用 Vitest - - 覆盖所有 composables - - 预计时间:30 小时 - -9. **性能优化** - - 虚拟滚动 - - 图片懒加载 - - 预计时间:20 小时 - ---- - -## 九、总结 - -### 核心问题 - -1. ❌ **代码重复率高达 59.7%**(439 行重复代码) -2. ❌ **localStorage 键名不统一** -3. ❌ **FileSystem.vue 过于复杂**(1374 行,34 个函数) -4. ❌ **缺乏公共抽象层**(composables 和工具函数) -5. ❌ **组件职责不清晰**(预览、操作、UI 混在一起) - -### 改进收益 - -1. ✅ **减少 43.2% 的代码**(912 行) -2. ✅ **代码复用率提升到 80%** -3. ✅ **组件复杂度降低 60%** -4. ✅ **新增功能成本降低 60%** -5. ✅ **可维护性和可测试性大幅提升** - -### 建议执行顺序 - -1. **第 1 周**:抽取公共 composables(useFileOperations, useFavoriteFiles, useLocalStorage) -2. **第 2 周**:统一常量管理,重构 DeviceTest.vue -3. **第 3-4 周**:抽取 UI 组件(FileList, FilePreviewer, FavoriteSidebar) -4. **第 5-6 周**:重构 FileSystem.vue,引入 useFilePreview -5. **长期**:TypeScript 迁移,单元测试,性能优化 - ---- - -## 附录:代码行数统计明细 - -### DeviceTest.vue(738 行) -- Template: 196 行 -- Script: 415 行 -- Style: 127 行 - -### FileSystem.vue(1374 行) -- Template: 250 行 -- Script: 693 行 -- Style: 431 行 - -### 重复代码统计 -- localStorage 操作: 86 行(11.7%) -- 收藏夹功能: 92 行(12.5%) -- 路径历史记录: 33 行(4.5%) -- 文件大小格式化: 12 行(1.6%) -- 基础文件操作: 150 行(20.4%) -- 拖拽调整功能: 66 行(9.0%) -- **总计**: 439 行(59.7%) diff --git a/docs/delete-optimization-guide.md b/docs/delete-optimization-guide.md deleted file mode 100644 index 1be31b5..0000000 --- a/docs/delete-optimization-guide.md +++ /dev/null @@ -1,292 +0,0 @@ -# 删除操作优化 - 使用指南 - -## 📋 概述 - -删除操作已优化,解决了以下问题: -1. ✅ 消除重复目录遍历(性能提升60%+) -2. ✅ 配置驱动的安全策略 -3. ✅ 支持确认机制(而非硬拒绝) -4. ✅ 默认禁用限制(避免过度防御) - ---- - -## 🚀 性能提升 - -### 修复前 -```go -// 同一个目录被遍历两次 -dirSize, _ := getDirSize(path) // 遍历1:获取大小 -fileCount, _ := countFilesInDir(path) // 遍历2:获取数量 -// 结果:大目录需要2倍时间 -``` - -### 修复后 -```go -// 一次遍历获取所有统计 -stats, _ := GetDirectoryStats(path) -// stats.Size // 大小 -// stats.FileCount // 数量 -// stats.Depth // 深度 -// 结果:性能提升60%+ -``` - ---- - -## 🔧 基本使用 - -### 1. 默认删除(推荐) -```go -err := filesystem.DeletePath(path) -if err != nil { - // 处理错误 -} -``` - -### 2. 使用自定义配置删除 -```go -config := &filesystem.Config{ - Security: filesystem.SecurityConfig{ - DeleteRestrictions: filesystem.DeleteRestrictionsConfig{ - Enabled: true, // 启用限制 - MaxFileSizeGB: 1.0, // 文件最大1GB - MaxDirSizeGB: 2.0, // 目录最大2GB - MaxDepth: 10, // 最大深度10层 - MaxFileCount: 500, // 最多500个文件 - RequireConfirm: true, // 超过限制时需要确认 - }, - }, -} - -err := filesystem.DeletePathWithConfig(path, config) -``` - ---- - -## ⚙️ 配置说明 - -### DeleteRestrictionsConfig 配置项 - -| 字段 | 类型 | 默认值 | 说明 | -|------|------|--------|------| -| `Enabled` | bool | false | 是否启用删除限制 | -| `MaxFileSizeGB` | float64 | 1.0 | 单个文件最大大小(GB)| -| `MaxDirSizeGB` | float64 | 1.0 | 目录最大大小(GB)| -| `MaxDepth` | int | 15 | 最大目录深度 | -| `MaxFileCount` | int | 1000 | 最大文件数量 | -| `RequireConfirm` | bool | true | 超过限制时确认而非拒绝 | -| `ForbiddenPaths` | []string | - | 禁止删除的路径 | - -### 默认配置 - -```go -DeleteRestrictions: DeleteRestrictionsConfig{ - Enabled: false, // 默认禁用(避免过度防御) - MaxFileSizeGB: 1.0, - MaxDirSizeGB: 1.0, - MaxDepth: 15, - MaxFileCount: 1000, - RequireConfirm: true, // 确认机制 - ForbiddenPaths: []string{ - "node_modules", ".git", ".github", - ".vscode", ".idea", "src", "dist", - "database", "db", "backup", - }, -} -``` - ---- - -## 🎯 确认机制 - -### 工作原理 - -当 `RequireConfirm = true` 时,超过限制会返回警告而非错误: - -```go -err := DeletePath(path) - -// 检查是否为限制警告 -if warning, ok := err.(*filesystem.DeleteRestrictionWarning); ok { - // 显示确认对话框 - confirmed := ShowConfirmDialog( - "删除确认", - fmt.Sprintf("该操作存在风险:\n%s\n\n是否继续?", warning.Details), - ) - - if confirmed { - // 用户确认,强制删除 - return DeletePathWithConfig(path, configWithoutRestrictions) - } - return err -} -``` - -### DeleteRestrictionWarning 结构 - -```go -type DeleteRestrictionWarning struct { - Path string // 文件路径 - Details string // 警告详情 - Info os.FileInfo // 文件信息 -} -``` - ---- - -## 📊 使用场景 - -### 场景1:开发环境(宽松) -```go -// 默认配置,禁用所有限制 -config := DefaultConfig() -err := DeletePathWithConfig(path, config) -``` - -### 场景2:生产环境(严格) -```go -config := DefaultConfig() -config.Security.DeleteRestrictions.Enabled = true -config.Security.DeleteRestrictions.RequireConfirm = false // 直接拒绝 - -err := DeletePathWithConfig(path, config) -if err != nil { - // 显示错误,不允许删除 -} -``` - -### 场景3:用户友好(推荐) -```go -config := DefaultConfig() -config.Security.DeleteRestrictions.Enabled = true -config.Security.DeleteRestrictions.RequireConfirm = true // 需要确认 - -err := DeletePathWithConfig(path, config) -if warning, ok := err.(*DeleteRestrictionWarning); ok { - // 显示确认对话框,让用户决定 - if UserConfirmed(warning.Details) { - // 继续删除 - } -} -``` - ---- - -## 🔍 安全检查 - -### 核心安全检查(始终启用) -1. ✅ 路径遍历检查(`..`) -2. ✅ 符号链接检查 -3. ✅ UNC路径检查(Windows) -4. ✅ 系统关键目录检查 -5. ✅ 敏感配置目录检查 - -### 可选限制(默认禁用) -- ⚠️ 文件大小限制 -- ⚠️ 目录大小限制 -- ⚠️ 目录深度限制 -- ⚠️ 文件数量限制 - ---- - -## 📈 性能对比 - -### 测试场景:删除包含10000个文件的目录 - -| 实现方式 | 遍历次数 | 耗时 | 性能 | -|----------|----------|------|------| -| 修复前 | 2次(大小+数量) | ~200ms | 100% | -| 修复后 | 1次(合并统计) | ~80ms | **60%↑** | - -### 内存占用 -- 修复前:2次遍历,峰值内存较高 -- 修复后:1次遍历,内存占用稳定 - ---- - -## 🛠️ API 参考 - -### DeletePath -```go -func DeletePath(path string) error -``` -使用默认配置删除文件或目录。 - -### DeletePathWithConfig -```go -func DeletePathWithConfig(path string, config *Config) error -``` -使用指定配置删除文件或目录。 - -### GetDirectoryStats -```go -func GetDirectoryStats(path string) (*DirectoryStats, error) -``` -获取目录统计信息(一次遍历)。 - -### CheckDeleteRestrictions -```go -func CheckDeleteRestrictions(path string, info os.FileInfo, config *Config) (exceeds bool, details string, err error) -``` -检查是否超过删除限制。 - ---- - -## 💡 最佳实践 - -### 1. 默认使用 `DeletePath` -```go -// 简单场景,使用默认配置 -err := filesystem.DeletePath(path) -``` - -### 2. 前端处理确认对话框 -```go -err := filesystem.DeletePath(path) -if warning, ok := err.(*filesystem.DeleteRestrictionWarning); ok { - if !frontend.ShowConfirm(warning.Details) { - return errors.New("用户取消") - } - // 用户确认,继续删除 -} -``` - -### 3. 根据环境调整配置 -```go -var config *filesystem.Config - -if IsProduction() { - // 生产环境:启用限制 - config = filesystem.DefaultConfig() - config.Security.DeleteRestrictions.Enabled = true - config.Security.DeleteRestrictions.RequireConfirm = false -} else { - // 开发环境:禁用限制 - config = filesystem.DefaultConfig() -} -``` - ---- - -## ⚠️ 注意事项 - -1. **默认禁用限制**: `Enabled = false`,避免影响正常使用 -2. **确认机制**: `RequireConfirm = true` 时会返回警告而非错误 -3. **向后兼容**: 保留 `DeletePath()` 函数,使用默认配置 -4. **性能优化**: 大目录删除前会进行统计,有一定开销 - ---- - -## 🎉 总结 - -| 优化项 | 修复前 | 修复后 | -|--------|--------|--------| -| 目录遍历 | 2次 | 1次 | -| 性能 | 基准 | 60%↑ | -| 配置化 | 硬编码 | 可配置 | -| 用户体验 | 硬拒绝 | 可确认 | -| 灵活性 | 低 | 高 | - ---- - -*文档版本: 1.0* -*最后更新: 2026-01-27* diff --git a/docs/file-security-implementation.md b/docs/file-security-implementation.md deleted file mode 100644 index cf02ccd..0000000 --- a/docs/file-security-implementation.md +++ /dev/null @@ -1,346 +0,0 @@ -# 文件管理安全功能实现总结 - -## ✅ 已完成的功能 - -### 1. 操作审计日志 (Audit Log) - -**实现位置**: `internal/filesystem/audit_log.go` - -**功能特性**: -- ✅ 记录所有文件操作(读取、写入、删除、创建等) -- ✅ 每条日志包含:时间戳、操作类型、文件路径、文件大小、操作结果 -- ✅ 使用缓冲区批量写入(每100条或每5秒刷新一次) -- ✅ 按日期自动轮转日志文件(`audit_2006-01-02.log`) -- ✅ JSON格式存储,易于解析和分析 -- ✅ 应用关闭时自动刷新缓冲区 - -**日志存储位置**: -- Windows: `%LOCALAPPDATA%\u-desk\logs\` -- macOS: `~/Library/Application Support/u-desk/logs/` -- Linux: `~/.config/u-desk/logs/` - -**集成方式**: -```go -// 在main.go中初始化 -logDir := filepath.Join(userDataDir, "logs") -filesystem.InitAudit(logDir) - -// 在文件操作中自动记录 -filesystem.ReadFile(path) // 自动记录读取操作 -filesystem.WriteFile(path, content) // 自动记录写入操作 -filesystem.DeletePath(path) // 自动记录删除操作 -``` - -**API接口**: -```go -// 获取最近的审计日志 -func (a *App) GetAuditLogs(limit int) ([]map[string]interface{}, error) -``` - ---- - -### 2. 回收站功能 (Recycle Bin) - -**实现位置**: `internal/filesystem/recycle_bin.go` - -**功能特性**: -- ✅ 删除文件时移动到回收站而非永久删除 -- ✅ 保留原始路径、删除时间、文件大小等元数据 -- ✅ 支持跨设备移动(复制+删除) -- ✅ 自动清理超过30天的文件 -- ✅ 支持恢复文件到原位置 -- ✅ 支持永久删除(清空回收站) -- ✅ JSON元数据存储(`metadata.json`) - -**回收站存储位置**: -- Windows: `%LOCALAPPDATA%\u-desk\recycle_bin\` -- macOS: `~/Library/Application Support/u-desk/recycle_bin/` -- Linux: `~/.config/u-desk/recycle_bin/` - -**文件命名规则**: -``` -20060102_150405_随机6位_原文件名.扩展名 -例如: 20250127_143022_a3b4c5_config.json -``` - -**使用示例**: -```go -// 删除到回收站 -bin := filesystem.GetRecycleBin() -bin.MoveToRecycleBin("C:\\test.txt") - -// 恢复文件 -bin.RestoreFromRecycleBin("回收站路径") - -// 永久删除 -bin.DeletePermanently("回收站路径") - -// 清空回收站 -bin.Empty() -``` - -**API接口**: -```go -// 获取回收站条目列表 -func (a *App) GetRecycleBinEntries() ([]map[string]interface{}, error) - -// 恢复文件 -func (a *App) RestoreFromRecycleBin(recyclePath string) error - -// 永久删除 -func (a *App) DeletePermanently(recyclePath string) error - -// 清空回收站 -func (a *App) EmptyRecycleBin() error -``` - ---- - -### 3. 文件锁检查 (File Lock Checker) - -**实现位置**: `internal/filesystem/file_lock.go` - -**功能特性**: -- ✅ 检测文件是否被其他程序占用 -- ✅ 尝试独占打开文件以检测锁定状态 -- ✅ 提供重试机制(可配置重试次数和间隔) -- ✅ Windows平台专用实现(使用Windows API) -- ✅ 友好的错误提示信息 - -**检查方式**: -1. 尝试以独占写模式打开文件 -2. 尝试重命名文件(更彻底的检查) -3. 检查错误类型是否为锁定相关错误 -4. 提供占用进程信息 - -**使用示例**: -```go -checker := filesystem.GetFileLockChecker() - -// 简单检查 -locked, processInfo, err := checker.IsFileLocked("C:\\test.txt") - -// 带重试的检查 -err := checker.CheckFileWithRetry("C:\\test.txt", 3, 1*time.Second) - -// 安全删除(带锁检查) -err := checker.SafeDeleteWithLockCheck("C:\\test.txt") -``` - -**错误提示示例**: -``` -无法删除文件:文件正被其他程序使用 - -提示:文件正被其他程序使用 - -请关闭相关程序后重试 -``` - ---- - -## 📂 新增文件清单 - -1. **internal/filesystem/audit_log.go** - 审计日志实现 - - `AuditLogger` 结构体 - - `AuditLogEntry` 日志条目 - - 日志记录、缓冲、轮转功能 - -2. **internal/filesystem/recycle_bin.go** - 回收站实现 - - `RecycleBin` 管理器 - - `RecycleBinEntry` 回收站条目 - - 文件移动、恢复、清理功能 - -3. **internal/filesystem/file_lock.go** - 文件锁检查实现 - - `FileLockChecker` 检查器 - - Windows API集成 - - 错误检测和重试机制 - ---- - -## 🔧 修改的文件 - -### 1. main.go -- 添加 `initFileSystemSecurity()` 初始化函数 -- 添加 `getUserDataDir()` 辅助函数 -- 配置 `OnShutdown` 回调 - -### 2. app.go -- 添加 `shutdown()` 方法 -- 添加审计日志API: `GetAuditLogs()` -- 添加回收站API: - - `GetRecycleBinEntries()` - - `RestoreFromRecycleBin()` - - `DeletePermanently()` - - `EmptyRecycleBin()` - -### 3. internal/filesystem/fs.go -- 添加全局审计日志记录器 -- 添加 `InitAudit()` 和 `CloseAudit()` 函数 -- 在 `ReadFile`、`WriteFile`、`DeletePath` 中集成审计日志 - ---- - -## 🎯 安全层级 - -系统现在具有**多层安全防护**: - -### 第1层:前端确认 -- ✅ 用户必须确认删除操作 -- ✅ 红色危险按钮提醒 -- ✅ 防止并发删除 - -### 第2层:后端验证 -- ✅ 路径安全检查 -- ✅ 敏感路径保护 -- ✅ 文件大小限制 -- ✅ 目录深度限制 - -### 第3层:文件锁检查 -- ✅ 检测文件占用 -- ✅ 防止删除正在使用的文件 -- ✅ 提供重试机制 - -### 第4层:回收站 -- ✅ 删除先移到回收站 -- ✅ 30天恢复期 -- ✅ 自动清理过期文件 - -### 第5层:审计日志 -- ✅ 记录所有操作 -- ✅ 便于追踪和审计 -- ✅ 永久保存操作历史 - ---- - -## 📊 使用流程 - -### 删除文件流程(带所有安全措施): - -``` -用户点击删除 - ↓ -前端确认对话框 - ↓ -[后端] 文件锁检查 ← 文件被占用? - ↓ ↓ - 通过 提示关闭程序 - ↓ -[后端] 移动到回收站 ← 删除失败? - ↓ ↓ - 成功 记录审计日志 - ↓ -记录审计日志(成功) - ↓ -返回前端显示成功 -``` - ---- - -## 🚀 前端集成建议 - -虽然后端API已实现,但前端仍需添加UI: - -### 1. 回收站界面 -```javascript -// 获取回收站条目 -const entries = await app.GetRecycleBinEntries() - -// 显示列表 -// - 原始路径 -// - 删除时间 -// - 文件大小 -// - 操作按钮(恢复/永久删除) - -// 清空回收站 -await app.EmptyRecycleBin() -``` - -### 2. 审计日志界面 -```javascript -// 获取审计日志 -const logs = await app.GetAuditLogs(100) // 最近100条 - -// 显示日志表格 -// - 时间戳 -// - 操作类型(read/write/delete) -// - 文件路径 -// - 成功/失败状态 -``` - -### 3. 文件锁错误处理 -```javascript -try { - await deletePathApi(path) -} catch (error) { - if (error.message.includes('文件被占用')) { - // 显示友好提示,建议用户关闭相关程序 - Message.error({ - content: error.message, - duration: 0, // 不自动关闭 - closable: true - }) - } -} -``` - ---- - -## 📝 配置项 - -所有配置都在代码中定义,可根据需要调整: - -### 审计日志配置 -```go -const bufferSize = 100 // 缓冲区大小 -const flushInterval = 5 * time.Second // 刷新间隔 -``` - -### 回收站配置 -```go -const retentionDays = 30 // 保留天数 -const autoCleanupInterval = 24 * time.Hour // 自动清理间隔 -``` - -### 文件锁配置 -```go -const defaultMaxRetries = 3 // 默认重试次数 -const defaultRetryInterval = 1 * time.Second // 默认重试间隔 -``` - ---- - -## 🧪 测试建议 - -### 1. 审计日志测试 -- 删除文件,检查日志文件是否生成 -- 检查日志格式是否正确(JSON) -- 关闭应用,检查缓冲区是否正确刷新 - -### 2. 回收站测试 -- 删除文件,检查回收站目录 -- 恢复文件,检查原位置是否有文件 -- 删除同名文件,检查是否正确处理 -- 清空回收站,检查所有文件是否删除 - -### 3. 文件锁测试 -- 用文本编辑器打开文件 -- 尝试删除,应该提示文件被占用 -- 关闭编辑器后,应该可以删除 - ---- - -## ✨ 总结 - -所有安全功能已成功实现并集成到应用中: - -1. ✅ **操作审计日志** - 完整追踪所有文件操作 -2. ✅ **回收站功能** - 30天恢复期,自动清理 -3. ✅ **文件锁检查** - 防止删除占用文件 - -系统现在具有**企业级的安全性和可靠性**,可以有效防止误删和恶意操作,同时提供完整的操作审计能力。 - ---- - -**实现日期**: 2025-01-27 -**版本**: v1.0.0 -**作者**: Claude Sonnet 4.5 diff --git a/docs/filesystem-architecture.md b/docs/filesystem-architecture.md deleted file mode 100644 index 1f2da87..0000000 --- a/docs/filesystem-architecture.md +++ /dev/null @@ -1,370 +0,0 @@ -# 文件管理模块架构升级方案 - -## 📋 目录 -- [现状分析](#现状分析) -- [架构目标](#架构目标) -- [核心设计](#核心设计) -- [模块划分](#模块划分) -- [实施路线图](#实施路线图) - ---- - -## 🔍 现状分析 - -### 当前问题 -1. **全局变量泛滥**:4个全局单例(auditLogger, recycleBin, lockChecker, fileServer) -2. **代码重复严重**:路径验证、文件类型检查、错误处理模式重复 -3. **魔法数字遍布**:至少15处硬编码常量 -4. **过度防御性**:删除操作有3层硬限制 -5. **性能隐患**:重复目录遍历、随机字符串生成低效 -6. **可测试性差**:依赖全局状态,难以编写单元测试 - -### 技术债务评估 -| 类别 | 债务量 | 优先级 | 影响范围 | -|------|--------|--------|----------| -| 重复代码 | 高 | P1 | 可维护性 | -| 性能问题 | 高 | P0 | 用户体验 | -| 架构问题 | 高 | P1 | 可扩展性 | -| 代码风格 | 中 | P2 | 可读性 | - ---- - -## 🎯 架构目标 - -### 设计原则 -1. **单一职责**:每个模块只负责一个功能领域 -2. **依赖倒置**:面向接口编程,降低耦合 -3. **开放封闭**:对扩展开放,对修改封闭 -4. **配置驱动**:安全策略可配置,不硬编码 - -### 质量目标 -- ✅ 零代码重复(DRY原则) -- ✅ 零全局变量(依赖注入) -- ✅ 零魔法数字(命名常量) -- ✅ 零性能隐患(优化热点) -- ✅ 100% 可测试(支持mock) - ---- - -## 🏗️ 核心设计 - -### 1. 分层架构 - -``` -┌─────────────────────────────────────────┐ -│ Application Layer (app.go) │ -│ - 对外接口(Bindings) │ -└────────────────┬────────────────────────┘ - │ -┌────────────────▼────────────────────────┐ -│ Service Layer (FileSystemService) │ -│ - 编排业务逻辑 │ -│ - 事务管理 │ -└────────────────┬────────────────────────┘ - │ -┌────────────────▼────────────────────────┐ -│ Component Layer │ -│ ┌────────────┬────────────┬──────────┐ │ -│ │Validator │Manager │Handler │ │ -│ │路径验证 │文件管理 │文件服务 │ │ -│ └────────────┴────────────┴──────────┘ │ -└────────────────┬────────────────────────┘ - │ -┌────────────────▼────────────────────────┐ -│ Infrastructure Layer │ -│ ┌──────────┬──────────┬──────────────┐ │ -│ │Audit │Recycle │Lock │ │ -│ │审计日志 │回收站 │文件锁 │ │ -│ └──────────┴──────────┴──────────────┘ │ -└──────────────────────────────────────────┘ -``` - -### 2. 核心接口设计 - -```go -// FileService 文件操作核心接口 -type FileService interface { - Read(path string) (string, error) - Write(path, content string) error - Delete(path string) error - List(path string) ([]FileInfo, error) - Create(path string, isDir bool) error - Move(src, dst string) error - GetInfo(path string) (*FileInfo, error) -} - -// PathValidator 路径验证接口 -type PathValidator interface { - Validate(path string) *ValidationError - IsSafe(path string) bool - IsSensitive(path string) bool -} - -// FileTypeManager 文件类型管理接口 -type FileTypeManager interface { - GetMIMEType(ext string) string - IsAllowed(ext string) bool - GetMaxSize(ext string) int64 -} - -// SecurityGuard 安全策略接口 -type SecurityGuard interface { - CheckDelete(path string) error - CheckAccess(path string) error -} -``` - -### 3. 配置驱动设计 - -```go -// Config 文件系统配置 -type Config struct { - // 安全配置 - Security SecurityConfig - // 性能配置 - Performance PerformanceConfig - // 功能开关 - Features FeatureConfig -} - -// SecurityConfig 安全策略配置 -type SecurityConfig struct { - // 路径验证 - PathValidation PathValidationConfig - // 删除限制 - DeleteRestrictions DeleteRestrictionsConfig - // 文件类型 - FileTypes FileTypeConfig -} - -// DeleteRestrictionsConfig 删除限制配置 -type DeleteRestrictionsConfig struct { - Enabled bool // 是否启用限制 - MaxSizeGB float64 // 最大文件大小(GB) - MaxDepth int // 最大目录深度 - MaxFileCount int // 最大文件数量 - RequireConfirm bool // 超过限制是否需要确认 - ForbiddenPaths []string // 禁止删除的路径 -} -``` - ---- - -## 📦 模块划分 - -### 模块1: 核心文件操作 (fs_core) -``` -fs_core/ -├── service.go # FileService 实现 -├── file_info.go # FileInfo 结构 -└── errors.go # 错误定义 -``` - -### 模块2: 路径验证 (validator) -``` -validator/ -├── path_validator.go # PathValidator 接口和实现 -├── config.go # 验证配置 -└── errors.go # 验证错误 -``` - -### 模块3: 文件类型管理 (filetype) -``` -filetype/ -├── manager.go # FileTypeManager 实现 -├── types.go # 文件类型配置 -└── mime.go # MIME 类型映射 -``` - -### 模块4: 基础设施 (infra) -``` -infra/ -├── audit/ -│ └── logger.go # 审计日志 -├── recycle/ -│ └── bin.go # 回收站 -├── lock/ -│ └── checker.go # 文件锁检查 -└── server/ - └── handler.go # HTTP 文件服务 -``` - -### 模块5: ZIP 操作 (zip) -``` -zip/ -├── reader.go # ZIP 读取 -├── writer.go # ZIP 写入 -├── security.go # ZIP 安全检查 -└── temp.go # 临时文件管理 -``` - -### 模块6: 配置管理 (config) -``` -config/ -├── constants.go # 常量定义 -├── config.go # 配置结构 -└── defaults.go # 默认配置 -``` - ---- - -## 🗺️ 实施路线图 - -### 阶段1: 紧急修复(P0)- 1天 -**目标**: 修复严重性能和稳定性问题 - -- [x] 任务1: 修复 `generateRandomString` 的 `time.Sleep` -- [x] 任务2: 修复文件锁检查的破坏性 rename - -**影响**: 立即提升性能和稳定性 - ---- - -### 阶段2: 基础建设(P1)- 2天 -**目标**: 统一配置和常量,消除技术债务 - -- [x] 任务3: 创建 constants.go,定义所有命名常量 -- [x] 任务4: 创建 config.go,统一配置管理 -- [x] 任务5: 定义核心接口(FileService, PathValidator, FileTypeManager) - -**影响**: 提升代码质量,为重构打基础 - ---- - -### 阶段3: DRY重构(P1)- 3天 -**目标**: 消除代码重复,提升可维护性 - -- [x] 任务6: 重构路径验证逻辑(PathValidator) -- [x] 任务7: 重构文件类型管理(FileTypeManager) -- [x] 任务8: 重构 ZIP 操作(withZipReader) - -**影响**: 减少30%+代码量,提升可维护性 - ---- - -### 阶段4: 安全优化(P1)- 2天 -**目标**: 优化过度防御,改善用户体验 - -- [x] 任务9: 重构 DeletePath 安全检查 -- [x] 任务10: 配置化安全策略 - -**影响**: 提升用户体验,保留安全性 - ---- - -### 阶段5: 架构升级(P1)- 3天 -**目标**: 引入依赖注入,消除全局变量 - -- [x] 任务11: 创建 FileSystemService -- [x] 任务12: 重构各组件为独立模块 -- [x] 任务13: 消除全局变量 - -**影响**: 提升可测试性和可扩展性 - ---- - -### 阶段6: 代码质量(P2)- 2天 -**目标**: 统一代码风格,完善文档 - -- [x] 任务14: 统一错误处理 -- [x] 任务15: 添加结构化日志 -- [x] 任务16: 统一注释风格 -- [x] 任务17: 编写单元测试 - -**影响**: 提升代码可读性和可维护性 - ---- - -### 阶段7: 测试验证(P2)- 2天 -**目标**: 确保重构质量,回归测试 - -- [x] 任务18: 编写集成测试 -- [x] 任务19: 性能基准测试 -- [x] 任务20: 安全测试 - -**影响**: 确保重构质量,无回归问题 - ---- - -## 📊 预期收益 - -### 代码质量 -- **代码量**: 预计减少 30-40% -- **重复率**: 从 25% 降至 < 5% -- **圈复杂度**: 平均降低 40% - -### 性能提升 -- **删除操作**: 性能提升 60%(消除重复遍历) -- **回收站**: 性能提升 99%(修复 time.Sleep) -- **文件锁**: 安全性提升 100%(消除破坏性操作) - -### 可维护性 -- **测试覆盖率**: 从 0% 提升至 80%+ -- **可测试性**: 从困难变为简单(依赖注入) -- **扩展性**: 新增功能无需修改核心代码 - ---- - -## 🔧 技术选型 - -### 依赖注入 -- 考虑 Uber Fx 或 Google Wire -- 或者手动 DI(更简单,适合当前规模) - -### 配置管理 -- 使用结构体配置 -- 支持 JSON/YAML 导入导出 -- 环境变量覆盖 - -### 日志 -- 结构化日志(logrus 或 zap) -- 可配置日志级别 -- 支持日志轮转 - -### 测试 -- 单元测试:testify/assert -- Mock:gomock -- 基准测试:内置 testing/benchmark - ---- - -## 📝 注意事项 - -### 兼容性 -- 保持对外接口(app.go 的方法)不变 -- 内部重构对前端透明 - -### 渐进式重构 -- 不重写,只重构 -- 一次只改一个模块 -- 每次重构后运行测试 - -### 回滚计划 -- 使用 Git 分支管理 -- 每个阶段完成后打 tag -- 出现问题可快速回滚 - ---- - -## 🎯 成功标准 - -### 功能完整性 -- ✅ 所有现有功能正常工作 -- ✅ 无新增 bug -- ✅ 性能不下降 - -### 代码质量 -- ✅ 代码重复率 < 5% -- ✅ 测试覆盖率 > 80% -- ✅ 代码审查通过 - -### 文档完整性 -- ✅ 架构文档完整 -- ✅ API 文档完整 -- ✅ 配置文档完整 - ---- - -*文档版本: 1.0* -*创建日期: 2026-01-27* -*作者: Claude Code* diff --git a/docs/filesystem-code-style-guide.md b/docs/filesystem-code-style-guide.md deleted file mode 100644 index 4519396..0000000 --- a/docs/filesystem-code-style-guide.md +++ /dev/null @@ -1,429 +0,0 @@ -# 文件管理模块代码风格规范 - -## 概述 - -本文档定义了文件管理模块的代码风格规范,确保代码一致性、可读性和可维护性。 - ---- - -## 1. 注释规范 - -### 1.1 包注释 -每个包应该有一个简短的包注释,说明包的用途。 - -```go -// Package filesystem 提供文件系统操作功能 -// -// 核心功能: -// - 文件读写、删除、列表 -// - 路径验证和安全检查 -// - ZIP文件操作 -// - 审计日志和回收站 -package filesystem -``` - -### 1.2 函数注释 -使用标准Go文档注释风格: - -```go -// DeletePath 删除文件或目录 -// -// 参数: -// path - 文件或目录路径 -// -// 返回: -// error - 错误信息,nil表示成功 -// -// 示例: -// err := fs.DeletePath("/path/to/file") -func (s *FileSystemService) DeletePath(path string) error { - // 实现... -} -``` - -### 1.3 禁止的注释风格 -```go -// 禁止使用emoji -// 🔒 安全检查 -// ✅ 优化 -// ⚠️ 警告 - -// 应使用纯文本 -// 安全检查 -// 性能优化 -// 警告 -``` - ---- - -## 2. 错误处理规范 - -### 2.1 错误包装 -使用 WrapError 添加上下文: - -```go -// 推荐做法 -data, err := os.ReadFile(path) -if err != nil { - return "", WrapError("读取文件", path, err) -} - -// 避免裸错误 -return "", err // ❌ 不推荐 -return "", fmt.Errorf("失败: %w", err) // ✅ 推荐 -``` - -### 2.2 错误消息 -使用中文描述(面向中文用户): - -```go -// 推荐 -return fmt.Errorf("文件不存在: %s", path) - -// 避免使用英文 -return fmt.Errorf("file not found: %s", path) // ❌ -``` - -### 2.3 错误忽略 -必须注释说明原因: - -```go -// 推荐:注释说明原因 -if err := logger.Close(); err != nil { - // 日志关闭失败,程序即将退出,忽略错误 -} - -// 禁止:无注释忽略 -_ = logger.Close() // ❌ -``` - ---- - -## 3. 命名规范 - -### 3.1 常量命名 -使用大驼峰命名法: - -```go -const ( - MaxZipSize = 100 * 1024 * 1024 - DefaultDirPermissions = 0755 - AuditFlushInterval = 5 * time.Second -) -``` - -### 3.2 变量命名 -使用小驼峰命名法: - -```go -var ( - globalService *FileSystemService - defaultConfig *Config - defaultPermissions os.FileMode = 0644 -) -``` - -### 3.3 接口命名 -接口名应该是动作或能力的描述,通常以 -er 结尾: - -```go -type Reader interface { - Read(p []byte) (n int, err error) -} - -type Validator interface { - Validate(path string) error -} -``` - ---- - -## 4. 函数设计规范 - -### 4.1 函数长度 -推荐单个函数不超过50行。如果超过,考虑拆分子函数: - -```go -// 推荐:拆分子函数 -func DeletePath(path string) error { - if err := validatePath(path); err != nil { - return err - } - - if err := checkPermissions(path); err != nil { - return err - } - - return performDelete(path) -} - -// 避免:长函数 -func DeletePath(path string) error { - // 100行代码... -} -``` - -### 4.2 参数数量 -函数参数不超过5个。如果超过,使用结构体: - -```go -// 推荐:使用结构体 -type DeleteOptions struct { - Path string - Force bool - SkipRecycle bool - IgnoreLock bool - Reason string -} - -func DeleteWithOptions(opts DeleteOptions) error { - // 实现... -} - -// 避免:过多参数 -func DeleteWithOptions(path string, force bool, skipRecycle bool, ignoreLock bool, reason string, timeout int) error { - // 参数过多 -} -``` - -### 4.3 返回值 -函数返回值遵循以下顺序: -1. 结果 -2. 错误 - -```go -// 推荐 -func ReadFile(path string) ([]byte, error) - -// 避免多个返回值 -func ReadFile(path string) ([]byte, bool, error, int) -``` - ---- - -## 5. 代码组织 - -### 5.1 文件组织 -每个文件应该有单一的职责: - -``` -filesystem/ -├── fs.go # 核心文件操作 -├── service.go # 文件系统服务 -├── path_validator.go # 路径验证 -├── filetype_manager.go # 文件类型管理 -├── zip.go # ZIP操作 -├── errors.go # 错误定义 -├── logger.go # 日志记录 -└── constants.go # 常量定义 -``` - -### 5.2 导入顺序 -标准库 → 第三方库 → 项目内部: - -```go -import ( - // 标准库 - "context" - "fmt" - "os" - - // 第三方库 - "github.com/google/uuid" - - // 项目内部 - "go-desk/internal/common" -) -``` - ---- - -## 6. 性能规范 - -### 6.1 避免重复计算 -使用缓存或预计算: - -```go -// 推荐:缓存结果 -type statsCache struct { - mu sync.RWMutex - cache map[string]*DirectoryStats -} - -func (c *statsCache) Get(path string) (*DirectoryStats, error) { - c.mu.RLock() - if stats, ok := c.cache[path]; ok { - c.mu.RUnlock() - return stats, nil - } - c.mu.RUnlock() - - // 计算并缓存 - stats, err := GetDirectoryStats(path) - if err != nil { - return nil, err - } - - c.mu.Lock() - c.cache[path] = stats - c.mu.Unlock() - - return stats, nil -} - -// 避免:重复计算 -func processData(path string) { - stats1, _ := GetDirectoryStats(path) - stats2, _ := GetDirectoryStats(path) // 重复计算 -} -``` - -### 6.2 资源释放 -使用 defer 确保资源释放: - -```go -// 推荐 -func ReadFile(path string) ([]byte, error) { - file, err := os.Open(path) - if err != nil { - return nil, err - } - defer file.Close() // 确保关闭 - - return io.ReadAll(file) -} -``` - ---- - -## 7. 并发安全 - -### 7.1 共享状态 -使用互斥锁保护共享状态: - -```go -type SafeCounter struct { - mu sync.RWMutex - count int -} - -func (c *SafeCounter) Increment() { - c.mu.Lock() - defer c.mu.Unlock() - c.count++ -} - -func (c *SafeCounter) Get() int { - c.mu.RLock() - defer c.mu.RUnlock() - return c.count -} -``` - -### 7.2 避免数据竞争 -不要在goroutine中直接共享变量: - -```go -// 推荐:传递参数 -for i := 0; i < 10; i++ { - go func(n int) { - fmt.Println(n) - }(i) -} - -// 避免:闭包捕获 -for i := 0; i < 10; i++ { - go func() { - fmt.Println(i) // 数据竞争 - }() -} -``` - ---- - -## 8. 测试规范 - -### 8.1 测试文件命名 -测试文件命名为 `xxx_test.go`: - -```go -// fs_test.go -package filesystem - -import "testing" - -func TestDeletePath(t *testing.T) { - // 测试代码 -} -``` - -### 8.2 表格驱动测试 -使用表格驱动测试多种场景: - -```go -func TestValidatePath(t *testing.T) { - tests := []struct { - name string - path string - wantErr bool - }{ - {"正常路径", "/tmp/test.txt", false}, - {"路径遍历", "/tmp/../etc/passwd", true}, - {"空路径", "", true}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := ValidatePath(tt.path) - if (err != nil) != tt.wantErr { - t.Errorf("ValidatePath() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} -``` - ---- - -## 9. 文档规范 - -### 9.1 README -每个模块应该有README说明: - -```markdown -# 文件系统模块 - -## 功能 -- 文件读写 -- 路径验证 -- ZIP操作 - -## 使用示例 -... - -## 配置 -... -``` - -### 9.2 API文档 -导出的函数和类型必须有文档注释。 - ---- - -## 10. 代码审查清单 - -提交代码前,确保: - -- [ ] 移除所有emoji注释 -- [ ] 函数有文档注释 -- [ ] 错误处理完善(无忽略错误) -- [ ] 命名符合规范 -- [ ] 无魔法数字(使用常量) -- [ ] 无重复代码(遵循DRY) -- [ ] 导入顺序正确 -- [ ] 资源正确释放(defer) - ---- - -*版本: 1.0* -*最后更新: 2026-01-27* diff --git a/docs/filesystem-complete-summary.md b/docs/filesystem-complete-summary.md deleted file mode 100644 index 9645513..0000000 --- a/docs/filesystem-complete-summary.md +++ /dev/null @@ -1,468 +0,0 @@ -# 文件管理模块升级 - 完整总结报告 - -**项目**: go-desk 文件管理模块 -**升级周期**: 2026-01-27 -**状态**: ✅ 全部完成 - ---- - -## 📊 执行摘要 - -### 完成情况 -``` -✅ P0 任务 (严重问题) [████████████████████] 100% (2/2) -✅ P1 任务 (核心功能) [████████████████████] 100% (7/7) -✅ P2 任务 (代码质量) [████████████████████] 100% (2/2) - -总体完成度: 100% (11/11 任务) -``` - -### 关键指标 -- **代码重复减少**: 60% (从 ~25% 降至 <10%) -- **魔法数字消除**: 100% (15+ → 0) -- **性能提升**: 60%+ (删除操作优化) -- **全局变量消除**: 100% (4个 → 可DI) -- **新增文件**: 10个 -- **新增代码**: ~1,700行 -- **删除重复**: 330+行 - ---- - -## 📋 任务清单 - -### ✅ P0 任务 (2个) - -#### 任务2: 修复严重性能问题 -**状态**: ✅ 完成 -**耗时**: 约30分钟 - -**成果**: -1. 修复 `generateRandomString` 性能灾难 - - 问题: 使用 `time.Sleep(time.Nanosecond)` - - 解决: 使用 `crypto/rand` - - 提升: 99%+ - -2. 修复文件锁检查的破坏性操作 - - 问题: 使用 `os.Rename` 测试 - - 解决: 使用 `os.OpenFile` - - 提升: 消除文件损坏风险 - ---- - -### ✅ P1 任务 (7个) - -#### 任务3: 重构路径验证逻辑 (DRY) -**状态**: ✅ 完成 -**文件**: `path_validator.go` (~210行) - -**成果**: -- 统一 `PathValidator` 接口 -- 消除4处重复验证逻辑 -- 配置驱动安全策略 - -**代码减少**: 107行 - -#### 任务4: 重构文件类型管理 (DRY) -**状态**: ✅ 完成 -**文件**: `filetype_manager.go` (~180行) - -**成果**: -- 统一 `FileTypeManager` 接口 -- 消除2处MIME类型映射 -- 统一白名单/黑名单管理 - -**代码减少**: 104行 - -#### 任务5: 优化删除操作安全检查 -**状态**: ✅ 完成 -**文件**: `directory_stats.go` (~115行) - -**成果**: -- 合并目录遍历(性能60%↑) -- 配置驱动删除限制 -- 确认机制替代硬拒绝 - -**代码减少**: 28行 - -#### 任务6: 重构ZIP操作 (DRY + 性能) -**状态**: ✅ 完成 -**文件**: `zip_helper.go` (~130行) - -**成果**: -- `withZipReader` 通用包装器 -- 消除4处 `zip.OpenReader` 重复 -- 简化操作函数 - -**代码减少**: 85行 - -#### 任务7: 引入依赖注入架构 -**状态**: ✅ 完成 -**文件**: `service.go` (~480行) - -**成果**: -- `FileSystemService` 统一服务 -- 消除4个全局变量依赖 -- 提升可测试性 - -**架构升级**: 依赖注入 - -#### 任务8: 统一常量和配置管理 -**状态**: ✅ 完成 -**文件**: -- `constants.go` (~90行) -- `config.go` (~350行) - -**成果**: -- 40+个命名常量 -- 配置驱动架构 -- 功能开关支持 - -**魔法数字**: 100%消除 - ---- - -### ✅ P2 任务 (2个) - -#### 任务9: 改进错误处理和日志 -**状态**: ✅ 完成 -**文件**: -- `errors.go` (~100行) -- `logger.go` (~160行) - -**成果**: -- 统一错误类型定义 -- 结构化日志记录器 -- 错误包装和上下文 - -#### 任务10: 统一代码风格和注释 -**状态**: ✅ 完成 -**文件**: `code-style-guide.md` - -**成果**: -- 代码风格规范文档 -- 移除emoji注释 -- 统一注释风格 -- 函数长度限制 - ---- - -## 📁 文件清单 - -### 新增文件 (10个) - -| 文件 | 行数 | 说明 | -|------|------|------| -| `constants.go` | 90 | 统一常量定义 | -| `config.go` | 350 | 配置管理架构 | -| `path_validator.go` | 210 | 路径验证器 | -| `filetype_manager.go` | 180 | 文件类型管理器 | -| `directory_stats.go` | 115 | 目录统计优化 | -| `zip_helper.go` | 130 | ZIP操作辅助 | -| `service.go` | 480 | 文件系统服务 | -| `service_interfaces.go` | 30 | 核心接口定义 | -| `errors.go` | 100 | 错误类型定义 | -| `logger.go` | 160 | 日志记录器 | - -**总计**: ~1,845行新代码 - -### 文档文件 (5个) - -| 文件 | 说明 | -|------|------| -| `filesystem-architecture.md` | 架构设计方案 | -| `filesystem-progress.md` | 进度跟踪报告 | -| `filesystem-phase2-report.md` | 任务3&4报告 | -| `delete-optimization-guide.md` | 删除优化指南 | -| `filesystem-code-style-guide.md` | 代码风格规范 | - ---- - -## 🏆 核心改进 - -### 1. 架构设计 - -#### 设计模式应用 -- ✅ **依赖注入**: FileSystemService -- ✅ **策略模式**: PathValidator, FileTypeManager -- ✅ **门面模式**: 统一服务入口 -- ✅ **单例模式**: 全局服务(兼容) -- ✅ **模板方法**: withZipReader - -#### 分层架构 -``` -应用层 (app.go) - ↓ -服务层 (FileSystemService) - ↓ -组件层 (Validator, Manager, Handler) - ↓ -基础设施层 (Audit, RecycleBin, Lock) -``` - -### 2. 代码质量 - -#### DRY原则 -| 模块 | 重复次数 | 统一后 | 改善 | -|------|---------|--------|------| -| 路径验证 | 4处 | 1处 | 75%↓ | -| 文件类型 | 2处 | 1处 | 50%↓ | -| ZIP打开 | 4处 | 1处 | 75%↓ | -| 目录遍历 | 2次 | 1次 | 50%↓ | - -**总体**: 代码重复率从 ~25% 降至 <10% - -#### 可测试性 -- ✅ 接口可mock -- ✅ 依赖可注入 -- ✅ 无全局状态 -- ✅ 纯函数设计 - -**可测试性**: 从 困难 → 简单 - -### 3. 性能优化 - -| 操作 | 优化前 | 优化后 | 提升 | -|------|--------|--------|------| -| 删除大目录 | 2次遍历 | 1次遍历 | **60%↑** | -| 随机字符串 | 慢 | 快 | **99%↑** | -| 文件锁检查 | 破坏性 | 非破坏性 | **100%↑** | - -### 4. 配置化 - -#### 可配置项 -- ✅ 安全策略(路径验证、删除限制) -- ✅ 性能参数(缓冲区、超时) -- ✅ 功能开关(审计、回收站、文件锁) -- ✅ 文件类型(MIME、权限、大小) - -**配置化程度**: 0% → 90% - ---- - -## 📈 对比分析 - -### 修复前的问题 - -#### 1. 代码重复 -```go -// fs.go -func isSafePath(path string) bool { - // 67行验证逻辑 -} - -// asset_handler.go -if strings.Contains(path, "..") { - http.Error(w, "Path traversal detected", http.StatusForbidden) -} - -// zip.go -func validateZipPath(zipPath string) error { - // 10行验证逻辑 -} -``` - -#### 2. 全局变量 -```go -var globalAuditLogger *AuditLogger -var globalRecycleBin *RecycleBin -var globalLockChecker *FileLockChecker -var defaultFileTypeManager = ... -``` - -#### 3. 魔法数字 -```go -if size > 1024*1024*1024 { // ❌ -if depth > 15 { // ❌ -if fileCount > 1000 { // ❌ -``` - -#### 4. 性能问题 -```go -// generateRandomString -time.Sleep(time.Nanosecond) // ❌ 性能灾难 - -// 文件锁检查 -os.Rename(path, testPath) // ❌ 破坏性操作 -``` - ---- - -### 修复后的改进 - -#### 1. 统一验证 -```go -// 使用统一验证器 -validator := NewPathValidator(config) -if err := validator.Validate(path); err != nil { - return err -} -``` - -#### 2. 依赖注入 -```go -// 注入所有依赖 -service, err := NewFileSystemService(config) -service.ReadFile(path) -service.Close(context.Background()) -``` - -#### 3. 命名常量 -```go -if size > MaxDeleteSizeGB { // ✅ -if depth > MaxDirectoryDepth { // ✅ -if fileCount > MaxFileCount { // ✅ -``` - -#### 4. 性能优化 -```go -// 使用加密随机数 -n, _ := rand.Int(rand.Reader, big.NewInt(100)) - -// 非破坏性检查 -file, _ := os.OpenFile(path, os.O_RDWR, 0666) -``` - ---- - -## 💡 技术亮点 - -### 1. 向后兼容性 -```go -// 旧代码继续工作 -func DeletePath(path string) error { - return DeletePathWithConfig(path, DefaultConfig()) -} - -// 新代码使用依赖注入 -service.DeletePath(path) -``` - -### 2. 渐进式升级 -- 阶段1: 修复严重问题 ✅ -- 阶段2: 基础建设 ✅ -- 阶段3: DRY重构 ✅ -- 阶段4: 代码质量 ✅ - -### 3. 配置驱动 -```go -// 开发环境 -config := DefaultConfig() - -// 生产环境 -config := DefaultConfig() -config.Security.DeleteRestrictions.Enabled = true -``` - ---- - -## 🎯 最终收益 - -### 代码质量指标 - -| 指标 | 初始 | 最终 | 改善 | -|------|------|------|------| -| **代码重复率** | ~25% | <10% | **60%↓** | -| **魔法数字** | 15+ | 0 | **100%↓** | -| **全局变量** | 4个 | 可DI | **100%↓** | -| **性能问题** | 2个P0 | 0 | **100%↓** | -| **可测试性** | 困难 | 简单 | **∞** | -| **配置化** | 0% | 90% | **∞** | - -### 代码统计 - -#### 新增代码 -- **文件**: 10个 -- **代码**: ~1,845行 -- **接口**: 3个 -- **辅助函数**: 25+个 - -#### 删除重复 -- **路径验证**: 107行 -- **文件类型**: 104行 -- **删除操作**: 28行 -- **ZIP操作**: 85行 -- **总计**: **330+行** - -#### 文档 -- **架构文档**: 1份 -- **进度报告**: 4份 -- **指南文档**: 2份 - ---- - -## 🚀 后续建议 - -### 1. 立即可用 -- ✅ 代码已经可以使用 -- ✅ 向后兼容 -- ✅ 性能提升明显 - -### 2. 短期优化(1-2周) -- 编写单元测试 -- 性能基准测试 -- 集成测试 - -### 3. 中期规划(1个月) -- 将架构应用到其他模块(dbclient, system) -- 完善API文档 -- 用户手册 - -### 4. 长期优化(3个月) -- 监控和指标收集 -- A/B测试新特性 -- 性能调优 - ---- - -## 📝 经验总结 - -### ✅ 成功经验 - -1. **渐进式重构**: 保持兼容,降低风险 -2. **优先级明确**: P0 → P1 → P2 -3. **文档先行**: 先设计后实施 -4. **测试驱动**: 代码质量保证 - -### ⚠️ 注意事项 - -1. **全局变量**: 虽然可用DI,但仍有全局服务(向后兼容) -2. **测试覆盖**: 新代码缺少单元测试 -3. **性能监控**: 需要实际环境验证 - -### 💡 最佳实践 - -1. **依赖注入优于全局变量** -2. **配置化优于硬编码** -3. **接口优于具体类型** -4. **组合优于继承** - ---- - -## 🎉 总结 - -**文件管理模块升级圆满完成!** - -### 核心成就 -- ✅ 消除代码重复 (60%↓) -- ✅ 消除魔法数字 (100%↓) -- ✅ 消除全局变量 (100%↓) -- ✅ 消除性能问题 (100%↓) -- ✅ 提升可测试性 (简单) -- ✅ 配置化架构 (90%) - -### 质量保证 -- **可维护性**: 代码清晰,易于理解 -- **可扩展性**: 接口设计,易于扩展 -- **可测试性**: 依赖注入,易于测试 -- **性能**: 优化热点,响应迅速 - -### 技术债务 -- **技术债务**: 从 高 → 低 -- **代码质量**: 从 中 → 高 -- **架构**: 从 混乱 → 清晰 - ---- - -*报告生成工具: Claude Code* -*版本: 最终版* -*完成日期: 2026-01-27* diff --git a/docs/filesystem-final-report.md b/docs/filesystem-final-report.md deleted file mode 100644 index 400776f..0000000 --- a/docs/filesystem-final-report.md +++ /dev/null @@ -1,342 +0,0 @@ -# 文件管理模块升级进度报告 - 任务7 - -**完成时间**: 2026-01-27 -**任务**: 引入依赖注入架构 - ---- - -## ✅ 任务7完成总结 - -### 🎯 核心成果 - -#### 1. 创建统一的文件系统服务 -**新文件**: `internal/filesystem/service.go` (~480行) - -**架构**: -```go -type FileSystemService struct { - // 核心组件 - config *Config - pathValidator PathValidator - fileTypeManager FileTypeManager - - // 基础设施组件 - auditLogger *AuditLogger - recycleBin *RecycleBin - lockChecker *FileLockChecker - - // 状态管理 - mu sync.RWMutex - initialized bool -} -``` - -**价值**: -- ✅ 消除全局变量依赖 -- ✅ 统一初始化流程 -- ✅ 便于测试(可mock所有组件) -- ✅ 资源生命周期管理 - -#### 2. 定义核心接口 -**新文件**: `internal/filesystem/service_interfaces.go` - -```go -type FileService interface { - // 基本操作 - Read(path string) (string, error) - Write(path, content string) error - Delete(path string) error - List(path string) ([]map[string]interface{}, error) - CreateDir(path string) error - CreateFile(path string) error - GetInfo(path string) (map[string]interface{}, error) - Open(path string) error - - // 配置 - GetConfig() *Config - Close(ctx context.Context) error -} -``` - -**好处**: -- ✅ 面向接口编程 -- ✅ 便于单元测试(可创建mock实现) -- ✅ 降低耦合度 - -#### 3. 保持向后兼容 -**新增全局服务**: -```go -// 全局服务实例(单例) -var globalService *FileSystemService - -// 获取全局服务(保持向后兼容) -func GetGlobalService() (*FileSystemService, error) - -// 初始化全局文件系统(兼容旧代码) -func InitGlobalFileSystem() error -``` - -**价值**: -- ✅ 现有代码无需大改 -- ✅ 渐进式迁移 -- ✅ 新代码可以使用依赖注入 - ---- - -## 📊 架构改进 - -### 修复前:全局变量满天飞 -```go -// 分散在各个文件中 -var globalAuditLogger *AuditLogger // audit_log.go -var globalRecycleBin *RecycleBin // recycle_bin.go -var globalLockChecker *FileLockChecker // file_lock.go -var defaultFileTypeManager = ... // filetype_manager.go - -// 问题: -// 1. 难以测试(无法mock) -// 2. 生命周期管理混乱 -// 3. 初始化顺序依赖 -// 4. 无法同时运行多个实例 -``` - -### 修复后:依赖注入 -```go -// 创建服务(可注入所有依赖) -service, err := NewFileSystemService(config) -if err != nil { - log.Fatal(err) -} - -// 使用服务 -err := service.DeletePath(path) -service.Close(context.Background()) - -// 测试时可以注入mock组件 -mockService := &FileSystemService{ - config: testConfig, - pathValidator: mockValidator, - auditLogger: mockLogger, -} -``` - ---- - -## 🔍 技术亮点 - -### 1. 依赖注入模式 -```go -// 构造函数注入 -func NewFileSystemService(config *Config) (*FileSystemService, error) { - service := &FileSystemService{ - config: config, - pathValidator: NewPathValidator(config), // 注入 - fileTypeManager: NewFileTypeManager(config), // 注入 - } - - // 初始化基础设施 - if err := service.initializeComponents(); err != nil { - return nil, err - } - - return service, nil -} -``` - -**好处**: -- ✅ 依赖显式化 -- ✅ 便于替换实现 -- ✅ 支持依赖反转 - -### 2. 生命周期管理 -```go -// 初始化 -service, err := NewFileSystemService(config) - -// 使用 -service.ReadFile(path) - -// 清理 -service.Close(context.Background()) -``` - -**好处**: -- ✅ 明确的初始化流程 -- ✅ 优雅的资源释放 -- ✅ 避免资源泄漏 - -### 3. 可测试性 -```go -// 创建mock实现 -type MockValidator struct {} -func (m *MockValidator) Validate(path string) *ValidationError { - return nil // 总是通过 -} - -// 注入mock -service := &FileSystemService{ - pathValidator: &MockValidator{}, -} - -// 测试代码 -func TestDeletePath(t *testing.T) { - service := createTestService() - err := service.DeletePath("/test/path") - assert.NoError(t, err) -} -``` - ---- - -## 📈 整体进度 - -``` -✅ P0 严重性能问题 [████████████████████] 100% (2/2) -✅ P1 基础建设 [████████████████████] 100% (4/4) -✅ P1 安全优化 [████████████████████] 100% (1/1) -✅ P1 DRY重构 [████████████████████] 100% (4/4) -✅ P1 ZIP重构 [████████████████████] 100% (1/1) -✅ P1 架构升级 [████████████████████] 100% (1/1) -⏳ P2 代码质量 [--------------------] 0% (0/2) - -总体进度: 65% (7/11 任务完成) -架构升级: 完成 -代码减少: 330+ 行重复代码 -``` - ---- - -## 💡 设计模式 - -### 1. 依赖注入(DI) -```go -// 所有依赖通过构造函数传入 -func NewFileSystemService(config *Config) (*FileSystemService, error) { - // 注入所有依赖 - service := &FileSystemService{ - config: config, - pathValidator: NewPathValidator(config), - fileTypeManager: NewFileTypeManager(config), - } - return service, nil -} -``` - -### 2. 单例模式(兼容) -```go -var globalService *FileSystemService -var globalServiceOnce sync.Once - -func GetGlobalService() (*FileSystemService, error) { - var err error - globalServiceOnce.Do(func() { - globalService, err = NewFileSystemService(DefaultConfig()) - }) - return globalService, err -} -``` - -### 3. 门面模式(Facade) -```go -// FileSystemService 作为统一入口 -// 屏蔽了内部复杂的子系统 -type FileSystemService struct { - pathValidator PathValidator - fileTypeManager FileTypeManager - auditLogger *AuditLogger - recycleBin *RecycleBin - // ... -} -``` - ---- - -## 🎯 剩余任务 - -### 低优先级(可选) -1. **任务9**: 改进错误处理和日志 📝 -2. **任务10**: 统一代码风格和注释 🎨 -3. **任务1**: 完成架构规划文档 📄 - -**说明**: 这些是P2任务,不是必需的。核心架构已经完成! - ---- - -## 📊 累计收益总结 - -### 代码质量 -| 指标 | 初始 | 最终 | 改善 | -|------|------|------|------| -| 代码重复率 | ~25% | <10% | 60%↓ | -| 魔法数字 | 15+ | 0 | 100%↓ | -| 全局变量 | 4个 | 0(可用DI) | 100%↓ | -| 性能问题 | 2个严重 | 0 | 100%↓ | -| 可测试性 | 困难 | 简单 | ∞ | - -### 代码统计 -- **新增文件**: 9个 -- **删除重复**: 330+ 行 -- **新增接口**: 3个 -- **辅助函数**: 20+ 个 - -### 架构改进 -- ✅ 路径验证统一(PathValidator) -- ✅ 文件类型管理统一(FileTypeManager) -- ✅ 删除操作优化(DirectoryStats + 配置驱动) -- ✅ ZIP操作统一(withZipReader) -- ✅ 依赖注入架构(FileSystemService) -- ✅ 配置驱动(Config) - ---- - -## 🎉 总结 - -**任务7圆满完成!** 主要成就: - -1. ✅ **消除全局变量**: 4个全局单例 → 可注入组件 -2. ✅ **提升可测试性**: 难以mock → 可mock所有依赖 -3. ✅ **生命周期管理**: 混乱 → 清晰的初始化/清理 -4. ✅ **向后兼容**: 保留全局服务单例 - -**累计完成**: 7/11任务 (65%) -**核心架构**: ✅ 全部完成 -**P1任务**: ✅ 全部完成 - -**可以停止了!** 核心架构升级已经完成,剩余任务是P2(可选的代码质量改进)。 - ---- - -## 🚀 使用建议 - -### 推荐方式(依赖注入) -```go -// main.go 或 app.go -func main() { - // 创建服务 - service, err := filesystem.NewFileSystemService( - filesystem.DefaultConfig(), - ) - if err != nil { - log.Fatal(err) - } - defer service.Close(context.Background()) - - // 使用服务 - app := &App{ - fs: service, - } - // ... -} -``` - -### 兼容方式(全局服务) -```go -// 现有代码继续工作 -filesystem.InitGlobalFileSystem() -err := filesystem.DeletePath(path) -``` - ---- - -*报告生成工具: Claude Code* -*版本: 5.0(最终版)* diff --git a/docs/filesystem-phase2-report.md b/docs/filesystem-phase2-report.md deleted file mode 100644 index 8659f08..0000000 --- a/docs/filesystem-phase2-report.md +++ /dev/null @@ -1,363 +0,0 @@ -# 文件管理模块升级进度报告 - 任务3&4 - -**完成时间**: 2026-01-27 -**阶段**: 阶段2-3 DRY重构 - ---- - -## ✅ 已完成任务 - -### 🎯 任务3:重构路径验证逻辑(DRY) -**状态**: ✅ 完成 -**文件**: `internal/filesystem/path_validator.go` - -#### 解决的问题 -- ❌ **修复前**: 路径验证逻辑分散在4个地方 - - `fs.go`: `isSafePath()` (67行) - - `fs.go`: `isSensitivePath()` (40行) - - `asset_handler.go`: HTTP路径检查 (20行) - - `zip.go`: `validateZipPath()` (10行) - -- ✅ **修复后**: 统一的路径验证器接口 - -#### 创建的架构 - -```go -// 路径验证器接口 -type PathValidator interface { - Validate(path string) *ValidationError - IsSafe(path string) bool - IsSensitive(path string) bool -} - -// 默认实现 -type DefaultPathValidator struct { - config *Config -} -``` - -#### 代码对比 - -**修复前(重复代码)**: -```go -// fs.go -func isSafePath(path string) bool { - cleanPath := filepath.Clean(path) - if strings.Contains(cleanPath, "..") { - return false - } - if fi, err := os.Lstat(path); err == nil && fi.Mode()&os.ModeSymlink != 0 { - return false - } - // ... 60+ 行代码 -} - -// asset_handler.go -if strings.Contains(decodedPath, "..") { - http.Error(w, "Path traversal detected", http.StatusForbidden) - return -} -// ... 重复的检查逻辑 -``` - -**修复后(统一验证)**: -```go -// 使用统一验证器 -validator := NewPathValidator(config) -if !validator.IsSafe(path) { - return fmt.Errorf("路径不安全") -} - -// 详细验证 -if err := validator.Validate(path); err != nil { - if err.IsError { - return err // 禁止访问 - } - // 敏感路径,可以警告但允许访问 -} -``` - -#### 收益 -- ✅ **消除重复**: 4处重复 → 1处实现 -- ✅ **代码减少**: ~140行重复代码 → 单一实现 -- ✅ **配置驱动**: 安全策略可配置 -- ✅ **易于测试**: 可mock接口 -- ✅ **向后兼容**: 保留 `isSafePath()` 兼容函数 - ---- - -### 🎯 任务4:重构文件类型管理(DRY) -**状态**: ✅ 完成 -**文件**: `internal/filesystem/filetype_manager.go` - -#### 解决的问题 -- ❌ **修复前**: 文件类型检查重复定义 - - `asset_handler.go`: `getContentType()` (29行) - - `asset_handler.go`: `isAllowedFileType()` (80行) - - 两个函数都有自己的MIME类型映射 - -- ✅ **修复后**: 统一的文件类型管理器 - -#### 创建的架构 - -```go -// 文件类型管理器接口 -type FileTypeManager interface { - GetMIMEType(ext string) string - IsAllowed(ext string) bool - GetMaxSize(ext string) int64 - GetFileInfo(ext string) *FileInfo -} - -// 文件类型信息 -type FileInfo struct { - Extension string - MIMEType string - Allowed bool - MaxSize int64 - Category string -} -``` - -#### 代码对比 - -**修复前(重复定义)**: -```go -// asset_handler.go - getContentType -func getContentType(ext string) string { - mimeTypes := map[string]string{ - ".jpg": "image/jpeg", - ".png": "image/png", - // ... 20+ 条目 - } - // ... -} - -// asset_handler.go - isAllowedFileType -func isAllowedFileType(ext string) bool { - allowedExtensions := map[string]bool{ - ".jpg": true, - ".png": true, - // ... 30+ 条目 - } - - forbiddenExtensions := map[string]bool{ - ".env": true, - ".key": true, - // ... 35+ 条目 - } - // ... -} -``` - -**修复后(统一管理)**: -```go -// 使用统一管理器 -info := defaultFileTypeManager.GetFileInfo(ext) -fmt.Printf("类型: %s, MIME: %s, 允许: %v\n", - info.Category, info.MIMEType, info.Allowed) - -// 简单检查 -if !defaultFileTypeManager.IsAllowed(ext) { - return fmt.Errorf("文件类型不允许") -} -``` - -#### 收益 -- ✅ **消除重复**: 2处MIME映射 → 1处配置 -- ✅ **代码减少**: ~110行重复代码 → 配置驱动 -- ✅ **易于扩展**: 新增文件类型只需修改配置 -- ✅ **统一逻辑**: 白名单/黑名单优先级统一 -- ✅ **向后兼容**: 保留兼容函数 - ---- - -## 📊 整体进度 - -``` -阶段1: 紧急修复 (P0) [████████████████████] 100% ✅ -阶段2: 基础建设 (P1) [████████████████████] 100% ✅ - ├─ 常量管理 [████████████████████] 100% ✅ - ├─ 配置管理 [████████████████████] 100% ✅ - ├─ 接口定义 [████████████████████] 100% ✅ - └─ 文档 [████████████████████] 100% ✅ -阶段3: DRY重构 (P1) [███████████──────────] 33% 🔄 - ├─ 路径验证统一 [████████████████████] 100% ✅ - ├─ 文件类型管理 [████████████████████] 100% ✅ - ├─ ZIP操作重构 [--------------------] 0% ⏳ - └─ 错误处理统一 [--------------------] 0% ⏳ -阶段4: 安全优化 (P1) [--------------------] 0% ⏳ -阶段5: 架构升级 (P1) [--------------------] 0% ⏳ -阶段6: 代码质量 (P2) [--------------------] 0% ⏳ -阶段7: 测试验证 (P2) [--------------------] 0% ⏳ - -总体进度: 35% (4/11 任务完成) -``` - ---- - -## 📈 代码质量提升 - -| 指标 | 修复前 | 当前 | 目标 | 进度 | -|------|--------|------|------|------| -| 魔法数字 | 15+ | 0 | 0 | ✅ 100% | -| 代码重复率 | ~25% | ~18% | <5% | 🔄 28% | -| 路径验证重复 | 4处 | 0 | 0 | ✅ 100% | -| 文件类型重复 | 2处 | 0 | 0 | ✅ 100% | -| 配置化程度 | 0% | 60% | 90% | 🔄 67% | - ---- - -## 📁 新增/修改的文件 - -| 文件 | 类型 | 说明 | -|------|------|------| -| `path_validator.go` | ✨ 新增 | 统一路径验证器 | -| `filetype_manager.go` | ✨ 新增 | 统一文件类型管理器 | -| `fs.go` | 🔧 修改 | 删除重复的验证函数(-107行) | -| `asset_handler.go` | 🔧 修改 | 使用新的管理器(-104行) | -| `constants.go` | ✨ 已有 | 常量定义 | -| `config.go` | ✨ 已有 | 配置管理 | - -**代码减少**: -211 行重复代码 - ---- - -## 🏗️ 架构改进 - -### 设计模式应用 - -#### 1. 策略模式(Strategy Pattern) -```go -// 不同场景使用不同的验证策略 -type PathValidator interface { ... } - -type StrictValidator struct { ... } // 严格验证 -type PermissiveValidator struct { ... } // 宽松验证 -``` - -#### 2. 单一职责原则(SRP) -- `PathValidator`: 只负责路径验证 -- `FileTypeManager`: 只负责文件类型管理 -- `Config`: 只负责配置管理 - -#### 3. 开闭原则(OCP) -```go -// 对扩展开放,对修改封闭 -type CustomValidator struct { - DefaultPathValidator - // 可以添加自定义验证逻辑 -} -``` - ---- - -## 🔍 技术亮点 - -### 1. 向后兼容性 -```go -// 保留旧函数作为兼容层 -func isSafePath(path string) bool { - validator := NewPathValidator(DefaultConfig()) - return validator.IsSafe(path) -} - -func getContentType(ext string) string { - return defaultFileTypeManager.GetMIMEType(ext) -} -``` -**好处**: 现有代码无需修改,渐进式升级 - -### 2. 配置驱动 -```go -// 安全策略完全可配置 -config := &Config{ - Security: SecurityConfig{ - PathValidation: PathValidationConfig{ - AllowSymlinks: false, - AllowUNCPaths: false, - CheckWindowsSystemPaths: true, - // ... 更多配置 - }, - }, -} -``` -**好处**: 不同环境可以有不同的安全策略 - -### 3. 错误分类 -```go -type ValidationError struct { - Path string - Reason string - IsError bool // true=禁止, false=警告 -} -``` -**好处**: 区分硬错误和软警告,改善用户体验 - ---- - -## 🎯 下一步计划 - -剩余7个任务: - -### 🔴 高优先级(建议继续) -1. **任务5**: 优化删除操作安全检查 - - 移除硬限制 - - 合并目录遍历 - - 添加确认机制 - -2. **任务6**: 重构ZIP操作 - - 创建 `withZipReader` 通用函数 - - 消除重复的打开/关闭逻辑 - -### 🟡 中优先级 -3. **任务7**: 引入依赖注入架构 -4. **任务9**: 改进错误处理和日志 - -### 🟢 低优先级 -5. **任务10**: 统一代码风格和注释 -6. **任务1**: 完成架构规划文档 - ---- - -## 💡 经验总结 - -### ✅ 做得好的地方 -1. **渐进式重构**: 保持向后兼容,降低风险 -2. **配置驱动**: 避免硬编码,提升灵活性 -3. **接口抽象**: 便于测试和扩展 -4. **文档完善**: 每个重构都有详细说明 - -### ⚠️ 注意事项 -1. **全局变量**: `defaultFileTypeManager` 仍然使用全局变量 - - **待解决**: 任务7(依赖注入) - -2. **测试覆盖**: 新代码缺少单元测试 - - **待解决**: 阶段7(测试验证) - -3. **性能**: `os.Lstat` 在每次验证时都会调用 - - **可优化**: 添加缓存层 - ---- - -## 📊 量化收益 - -### 代码质量 -- **删除重复代码**: 211行 -- **新增接口**: 2个 -- **新增实现**: 2个 -- **配置化项**: 40+ - -### 可维护性 -- **DRY原则**: 路径验证和文件类型完全符合DRY -- **单一职责**: 每个模块职责清晰 -- **易于测试**: 接口可mock -- **易于扩展**: 配置驱动 - -### 性能 -- **无明显变化**: 重构主要是代码组织,不影响性能 - ---- - -*报告生成工具: Claude Code* -*版本: 2.0* diff --git a/docs/filesystem-phase3-report.md b/docs/filesystem-phase3-report.md deleted file mode 100644 index 51816ac..0000000 --- a/docs/filesystem-phase3-report.md +++ /dev/null @@ -1,334 +0,0 @@ -# 文件管理模块升级进度报告 - 任务5 - -**完成时间**: 2026-01-27 -**任务**: 优化删除操作安全检查 - ---- - -## ✅ 任务5完成总结 - -### 🎯 核心成果 - -#### 1. 性能优化:消除重复目录遍历 -**文件**: `internal/filesystem/directory_stats.go` - -**问题**: -```go -// 修复前:同一个目录被遍历2次 -dirSize, _ := getDirSize(path) // 遍历1:获取大小 -fileCount, _ := countFilesInDir(path) // 遍历2:获取数量 -``` - -**解决**: -```go -// 修复后:一次遍历获取所有统计 -stats, _ := GetDirectoryStats(path) -// stats.Size // 大小 -// stats.FileCount // 数量 -// stats.Depth // 深度 -``` - -**收益**: -- ✅ 性能提升 **60%+** -- ✅ 减少磁盘I/O -- ✅ 降低内存占用 - ---- - -#### 2. 配置驱动的安全策略 -**文件**: `internal/filesystem/fs.go` - -**问题**: -```go -// 修复前:硬编码的3层限制 -if dirSize > 1024*1024*1024 { // 1GB - return fmt.Errorf("目录过大") -} -if depth > 15 { - return fmt.Errorf("目录层级过深") -} -if fileCount > 1000 { - return fmt.Errorf("文件过多") -} -``` - -**解决**: -```go -// 修复后:配置驱动 -config := DefaultConfig() -config.Security.DeleteRestrictions.Enabled = true -config.Security.DeleteRestrictions.MaxDirSizeGB = 2.0 -config.Security.DeleteRestrictions.RequireConfirm = true - -err := DeletePathWithConfig(path, config) -``` - -**收益**: -- ✅ 灵活可配置 -- ✅ 适应不同场景 -- ✅ 无需修改代码 - ---- - -#### 3. 确认机制替代硬拒绝 - -**问题**: -- 修复前:超过限制直接拒绝,阻止合法操作 - -**解决**: -```go -type DeleteRestrictionWarning struct { - Path string - Details string - Info os.FileInfo -} - -// 前端可以捕获警告并显示确认对话框 -if warning, ok := err.(*DeleteRestrictionWarning); ok { - confirmed := ShowConfirmDialog(warning.Details) - if confirmed { - // 用户确认,继续删除 - } -} -``` - -**收益**: -- ✅ 改善用户体验 -- ✅ 保留安全性 -- ✅ 用户自主决策 - ---- - -#### 4. 默认禁用过度限制 - -**配置策略**: -```go -DeleteRestrictions: DeleteRestrictionsConfig{ - Enabled: false, // 默认禁用(避免过度防御) - RequireConfirm: true, // 启用时使用确认机制 -} -``` - -**收益**: -- ✅ 不影响正常使用 -- ✅ 按需启用保护 -- ✅ 向后兼容 - ---- - -## 📊 代码改进 - -### 新增文件 - -| 文件 | 行数 | 说明 | -|------|------|------| -| `directory_stats.go` | ~115 | 目录统计和限制检查 | -| `delete-optimization-guide.md` | - | 使用指南 | - -### 修改文件 - -| 文件 | 改动 | 说明 | -|------|------|------| -| `fs.go` | 重构 | 使用新的统计和检查逻辑 | - -### 删除代码 - -```go -// 删除重复遍历函数(-28行) --func getDirSize(path string) (int64, error) --func countFilesInDir(path string) (int, error) - -// 重构DeletePath(-55行,+72行净增17行,但功能更强) -``` - ---- - -## 🔍 技术细节 - -### DirectoryStats 结构 - -```go -type DirectoryStats struct { - Size int64 // 总大小(字节) - FileCount int // 文件数量 - DirCount int // 目录数量 - Depth int // 最大深度 -} -``` - -### 优化算法 - -```go -// 一次遍历,多维度统计 -func GetDirectoryStats(path string) (*DirectoryStats, error) { - stats := &DirectoryStats{} - baseDepth := strings.Count(filepath.Clean(path), string(filepath.Separator)) - - err := filepath.Walk(path, func(p string, info os.FileInfo, err error) error { - // 计算深度 - currentDepth := strings.Count(filepath.Clean(p), string(filepath.Separator)) - baseDepth - if currentDepth > stats.Depth { - stats.Depth = currentDepth - } - - if info.IsDir() { - stats.DirCount++ - return nil - } - - stats.FileCount++ - stats.Size += info.Size() - return nil - }) - - return stats, err -} -``` - ---- - -## 📈 性能基准 - -### 测试场景 - -**测试环境**: -- 目录:10000个文件 -- 总大小:~500MB -- 目录深度:5层 - -**测试结果**: - -| 实现方式 | 遍历次数 | 耗时 | CPU | 内存 | -|----------|----------|------|-----|------| -| 修复前 | 2次 | ~200ms | 高 | ~2MB | -| 修复后 | 1次 | ~80ms | 低 | ~1MB | -| **提升** | **-50%** | **+60%** | **↓** | **-50%** | - ---- - -## 🎯 整体进度更新 - -``` -✅ P0 严重性能问题 [████████████████████] 100% (2/2) -✅ P1 基础建设 [████████████████████] 100% (4/4) -🔄 P1 DRY重构 [███████████████--------] 50% (3/6) -✅ P1 安全优化 [████████████████████] 100% (1/1) -⏳ P1 ZIP重构 [--------------------] 0% (0/1) -⏳ P1 架构升级 [--------------------] 0% (0/1) -⏳ P2 代码质量 [--------------------] 0% (0/2) - -总体进度: 45% (5/11 任务完成) -性能提升: 60%+ (删除操作) -代码减少: 240+ 行重复代码 -``` - ---- - -## 💡 设计亮点 - -### 1. 单一职责 -- `GetDirectoryStats`: 只负责统计 -- `CheckDeleteRestrictions`: 只负责检查 -- `DeletePathWithConfig`: 只负责删除逻辑 - -### 2. 开闭原则 -```go -// 对扩展开放 -type CustomStats struct { - DirectoryStats - CustomField string -} - -// 对修改封闭 -func DeletePath(path string) error { - return DeletePathWithConfig(path, DefaultConfig()) -} -``` - -### 3. 向后兼容 -```go -// 旧代码继续工作 -err := filesystem.DeletePath(path) - -// 新代码可以使用配置 -err := filesystem.DeletePathWithConfig(path, customConfig) -``` - ---- - -## 🚀 下一步建议 - -剩余6个任务,优先级排序: - -### 🔴 高优先级 -1. **任务6**: 重构ZIP操作 - - 创建 `withZipReader` 通用函数 - - 消除重复的打开/关闭逻辑 - - 预计代码减少50+行 - -2. **任务7**: 引入依赖注入架构 - - 消除全局变量 - - 创建 FileSystemService - - 提升可测试性 - -### 🟡 中优先级 -3. **任务9**: 改进错误处理和日志 -4. **任务10**: 统一代码风格和注释 - ---- - -## 📊 累计收益 - -### 代码质量 -| 指标 | 修复前 | 当前 | 提升 | -|------|--------|------|------| -| 重复代码 | ~25% | ~15% | 40%↓ | -| 魔法数字 | 15+ | 0 | 100%↓ | -| 性能问题 | 2个严重 | 0 | 100%↓ | -| 配置化程度 | 0% | 80% | ∞ | - -### 架构改进 -- ✅ 路径验证统一 -- ✅ 文件类型管理统一 -- ✅ 删除操作优化 -- ✅ 配置驱动架构 - -### 文档完善 -- ✅ 架构设计文档 -- ✅ 进度跟踪报告 -- ✅ 使用指南文档 -- ✅ API参考文档 - ---- - -## 📝 经验总结 - -### ✅ 成功经验 -1. **渐进式优化**: 保持兼容,降低风险 -2. **性能优先**: 消除热点,提升体验 -3. **配置驱动**: 灵活适配不同场景 -4. **用户友好**: 确认机制改善UX - -### ⚠️ 待改进 -1. **全局变量**: 仍有4个全局单例 -2. **测试覆盖**: 新代码缺少单元测试 -3. **错误处理**: 部分错误被忽略 - ---- - -## 🎉 总结 - -任务5已圆满完成!主要成就: - -1. ✅ **性能提升60%+** - 消除重复目录遍历 -2. ✅ **配置化策略** - 灵活的安全检查 -3. ✅ **确认机制** - 改善用户体验 -4. ✅ **代码质量** - 删除240+行重复代码 - -**累计完成**: 5/11任务 (45%) -**下一里程碑**: 完成DRY重构(还需1个任务) - ---- - -*报告生成工具: Claude Code* -*版本: 3.0* diff --git a/docs/filesystem-phase4-report.md b/docs/filesystem-phase4-report.md deleted file mode 100644 index 9da1e60..0000000 --- a/docs/filesystem-phase4-report.md +++ /dev/null @@ -1,290 +0,0 @@ -# 文件管理模块升级进度报告 - 任务6 - -**完成时间**: 2026-01-27 -**任务**: 重构ZIP操作(DRY + 性能) - ---- - -## ✅ 任务6完成总结 - -### 🎯 核心成果 - -#### 1. 创建通用ZIP操作包装器 -**新文件**: `internal/filesystem/zip_helper.go` (~130行) - -**功能**: -- ✅ `withZipReader`: 通用的ZIP文件打开/关闭包装器 -- ✅ `withZipFile`: 在ZIP中查找文件并执行操作 -- ✅ 辅助函数:文件匹配、读取、格式化等 - -**代码对比**: -```go -// 修复前:每个函数都重复这些代码 -func ExtractFileFromZip(zipPath, filePath string) (string, error) { - if err := validateZipPath(zipPath); err != nil { - return "", err - } - - reader, err := zip.OpenReader(zipPath) - if err != nil { - return "", fmt.Errorf("打开 zip 文件失败: %v", err) - } - defer reader.Close() - - for _, file := range reader.File { - if filepath.Clean(file.Name) == filepath.Clean(filePath) { - // ... 操作逻辑 - } - } -} - -// 修复后:简洁清晰 -func ExtractFileFromZip(zipPath, filePath string) (string, error) { - result, err := withZipFile(zipPath, filePath, func(file *zip.File) (interface{}, error) { - // 只需关注业务逻辑 - rc, err := file.Open() - // ... - return string(data), nil - }) - return result.(string), err -} -``` - -#### 2. 重构所有ZIP操作函数 -**文件**: `internal/filesystem/zip.go` - -**重构的函数**: -1. ✅ `ExtractFileFromZip`: 45行 → 22行(-51%) -2. ✅ `ExtractFileFromZipToTemp`: 80行 → 60行(-25%) -3. ✅ `GetZipFileInfo`: 30行 → 10行(-67%) - -**代码减少**: ~85行重复代码 - -#### 3. 新增辅助函数 -**文件**: `zip_helper.go` + `zip.go` - -```go -// 文件匹配 -func isMatchFile(file *zip.File, targetPath string) bool - -// 读取文件内容 -func readAllFromFile(rc io.ReadCloser) ([]byte, error) - -// 压缩方法描述 -func getCompressionMethodString(method uint16) string - -// 创建文件信息map -func createFileInfoMap(file *zip.File, includeExtra ...bool) map[string]interface{} - -// ZIP文件基本验证 -func validateZipFileBasic(zipPath string) error - -// ZIP文件头检查 -func checkZipFileHeader(zipPath string) error -``` - ---- - -## 📊 代码质量提升 - -### DRY原则 -| 指标 | 修复前 | 修复后 | 改善 | -|------|--------|--------|------| -| zip.OpenReader 重复 | 4处 | 0 | 100%↓ | -| 打开/关闭逻辑重复 | ~40行 | 1处 | 100%↓ | -| 文件查找逻辑重复 | ~30行 | 1处 | 100%↓ | -| 文件信息格式化 | 3处 | 1处 | 67%↓ | - -### 代码简化 -| 函数 | 修复前行数 | 修复后行数 | 减少 | -|------|-----------|-----------|------| -| ExtractFileFromZip | 45 | 22 | -51% | -| ExtractFileFromZipToTemp | 80 | 60 | -25% | -| GetZipFileInfo | 30 | 10 | -67% | -| **合计** | **155** | **92** | **-41%** | - -### 辅助函数 -- `zip_helper.go`: 7个新函数 -- `zip.go`: 2个新函数 -- **总计**: 9个可复用函数 - ---- - -## 🔍 技术亮点 - -### 1. 高阶函数模式 -```go -// ZipOperation 操作回调类型 -type ZipOperation func(*zip.ReadCloser) (interface{}, error) - -// 通用包装器 -func withZipReader(zipPath string, operation ZipOperation) (interface{}, error) { - // 统一的验证、打开、关闭逻辑 - reader, err := zip.OpenReader(zipPath) - defer reader.Close() - return operation(reader) -} -``` - -**好处**: -- ✅ 关注点分离:包装器处理资源,回调处理业务 -- ✅ 错误处理统一 -- ✅ 代码可读性提升 - -### 2. 进一步封装 -```go -// for single file operations -type ZipFileOperation func(*zip.File) (interface{}, error) - -func withZipFile(zipPath, filePath string, operation ZipFileOperation) (interface{}, error) { - return withZipReader(zipPath, func(reader *zip.ReadCloser) (interface{}, error) { - for _, file := range reader.File { - if isMatchFile(file, filePath) { - return operation(file) - } - } - return nil, fmt.Errorf("文件不存在") - }) -} -``` - -**好处**: -- ✅ 单文件操作更简洁 -- ✅ 自动文件查找 -- ✅ 统一错误处理 - -### 3. 辅助函数提取 -```go -// 消除重复的格式化逻辑 -func getCompressionMethodString(method uint16) string { - if method == 8 { - return "Deflate" - } - return "Store" -} - -// 统一的文件信息创建 -func createFileInfoMap(file *zip.File, includeExtra ...bool) map[string]interface{} { - // 统一格式 -} -``` - ---- - -## 📈 整体进度更新 - -``` -✅ P0 严重性能问题 [████████████████████] 100% (2/2) -✅ P1 基础建设 [████████████████████] 100% (4/4) -✅ P1 安全优化 [████████████████████] 100% (1/1) -✅ P1 DRY重构 [████████████████████] 100% (4/4) -🔄 P1 ZIP重构 [████████████████████] 100% (1/1) -⏳ P1 架构升级 [--------------------] 0% (0/1) -⏳ P2 代码质量 [--------------------] 0% (0/2) - -总体进度: 55% (6/11 任务完成) -代码减少: 330+ 行重复代码 -性能提升: 60%+ (删除操作) -``` - ---- - -## 💡 设计模式应用 - -### 1. 模板方法模式 -```go -// withZipReader 定义了ZIP操作的标准流程 -func withZipReader(zipPath string, operation ZipOperation) (interface{}, error) { - // 1. 验证路径 - if err := validateZipPath(zipPath); err != nil { - return nil, err - } - - // 2. 打开文件 - reader, err := zip.OpenReader(zipPath) - defer reader.Close() - - // 3. 执行操作(由调用者实现) - return operation(reader) -} -``` - -### 2. 回调函数模式 -```go -// 调用者只需关注业务逻辑 -result, err := withZipFile(zipPath, filePath, func(file *zip.File) (interface{}, error) { - // 业务逻辑:读取、提取、获取信息等 - return data, nil -}) -``` - -### 3. 单一职责原则 -- `zip_helper.go`: ZIP操作的通用逻辑 -- `zip.go`: 具体业务函数 -- 每个辅助函数只做一件事 - ---- - -## 🎯 剩余任务 - -### 高优先级(建议继续) -1. **任务7**: 引入依赖注入架构 🏗️ 重要 - - 消除全局变量(4个) - - 创建 FileSystemService - - 提升可测试性到80%+ - -2. **任务9**: 改进错误处理和日志 📝 质量提升 - - 修复被忽略的错误 - - 统一错误消息 - - 添加结构化日志 - -### 低优先级 -3. **任务10**: 统一代码风格和注释 -4. **任务1**: 完成架构规划文档 - ---- - -## 📊 累计收益 - -### 代码质量 -| 指标 | 初始 | 当前 | 目标 | 进度 | -|------|------|------|------|------| -| 代码重复率 | ~25% | ~10% | <5% | 60% | -| 魔法数字 | 15+ | 0 | 0 | 100% | -| 全局变量 | 4个 | 4个 | 0 | 0% | -| 性能问题 | 2个 | 0 | 0 | 100% | - -### 代码减少 -- **任务2**: 0行(性能修复) -- **任务3**: 107行(路径验证) -- **任务4**: 104行(文件类型) -- **任务5**: 28行(删除优化) -- **任务6**: 85行(ZIP重构) -- **总计**: **328行重复代码** - -### 架构改进 -- ✅ 路径验证统一 -- ✅ 文件类型管理统一 -- ✅ 删除操作优化 -- ✅ ZIP操作统一 -- ✅ 配置驱动架构 -- ⏳ 依赖注入(待完成) - ---- - -## 🎉 总结 - -任务6已圆满完成!主要成就: - -1. ✅ **消除重复**: 4处 `zip.OpenReader` → 1处通用包装器 -2. ✅ **代码简化**: 3个函数共减少41%代码量 -3. ✅ **辅助函数**: 9个可复用工具函数 -4. ✅ **更易维护**: 清晰的关注点分离 - -**累计完成**: 6/11任务 (55%) -**下一里程碑**: 完成架构升级(依赖注入) - ---- - -*报告生成工具: Claude Code* -*版本: 4.0* diff --git a/docs/filesystem-progress.md b/docs/filesystem-progress.md deleted file mode 100644 index a23c7ce..0000000 --- a/docs/filesystem-progress.md +++ /dev/null @@ -1,244 +0,0 @@ -# 文件管理模块升级进度报告 - -**生成时间**: 2026-01-27 -**当前阶段**: 阶段1-2 进行中 - ---- - -## ✅ 已完成任务 - -### 🔴 P0: 修复严重性能问题 (任务2) -**状态**: ✅ 完成 -**耗时**: 约15分钟 - -#### 修复内容 - -##### 1. `generateRandomString` 性能灾难 -**问题**: -- 使用 `time.Sleep(time.Nanosecond)` 导致每次生成6个字符耗时极长 -- 使用时间戳作为随机源不安全 - -**修复**: -```go -// 修复前 -for i := range b { - b[i] = charset[time.Now().UnixNano()%int64(len(charset))] - time.Sleep(time.Nanosecond) // ⚠️ 性能灾难 -} - -// 修复后 -for i := range b { - n, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset)))) - if err != nil { - b[i] = charset[time.Now().UnixNano()%int64(len(charset))] // 回退 - continue - } - b[i] = charset[n.Int64()] -} -``` - -**收益**: -- ✅ 性能提升 99%+ (消除 nanosecond sleep) -- ✅ 随机性提升 (使用加密安全的随机数) - -##### 2. 文件锁检查的破坏性操作 -**问题**: -- 使用 `os.Rename` 测试文件锁,会短暂改变文件名 -- 如果第一次 rename 失败,第二次会报错(testPath 不存在) - -**修复**: -```go -// 修复前:破坏性测试 -testPath := path + ".locktest" -if err := os.Rename(path, testPath); err != nil { - _ = os.Rename(testPath, path) // ⚠️ testPath 不存在,会报错 - // ... -} -_ = os.Rename(testPath, path) // ⚠️ 再次 rename - -// 修复后:非破坏性测试 -file, err := os.OpenFile(path, os.O_RDWR|syscall.O_CREAT, 0666) -if err != nil { - if isLockError(err) { - return true, processInfo, nil - } - return false, "", err -} -defer file.Close() -return false, "", nil -``` - -**收益**: -- ✅ 消除文件损坏风险 -- ✅ 消除错误处理 bug -- ✅ 简化代码逻辑 - ---- - -### 🟢 P1: 统一常量和配置管理 (任务8) -**状态**: ✅ 完成 -**耗时**: 约20分钟 - -#### 创建的文件 - -##### 1. `constants.go` -**内容**: 统一管理所有命名常量 - -```go -// 文件大小限制 -const ( - MaxZipSize = 100 * 1024 * 1024 - MaxExtractSize = 500 * 1024 * 1024 - MaxSingleFileSize = 50 * 1024 * 1024 - MaxHTTPFileSize = 500 * 1024 * 1024 - // ... -) - -// 时间相关 -const ( - AuditFlushInterval = 5 * time.Second - RecycleBinRetentionPeriod = 30 * 24 * time.Hour - TempFileCleanupAge = 24 * time.Hour - // ... -) - -// 数量限制 -const ( - MaxDirectoryDepth = 15 - MaxFileCount = 1000 - // ... -) -``` - -**收益**: -- ✅ 消除15+处魔法数字 -- ✅ 提升代码可读性 -- ✅ 便于统一调整参数 - -##### 2. `config.go` -**内容**: 配置驱动的安全策略和功能开关 - -```go -type Config struct { - Security SecurityConfig - Performance PerformanceConfig - Features FeatureConfig -} - -type DeleteRestrictionsConfig struct { - Enabled bool - MaxFileSizeGB float64 - MaxDirSizeGB float64 - RequireConfirm bool // 关键改进:确认而非拒绝 - // ... -} -``` - -**收益**: -- ✅ 安全策略可配置 -- ✅ 功能开关集中管理 -- ✅ 为依赖注入打基础 - ---- - -## 🔄 进行中任务 - -### 下一步:重构路径验证逻辑 (任务3) -**优先级**: P1 -**预计耗时**: 1-2小时 - -**计划**: -1. 创建 `PathValidator` 接口 -2. 实现 `DefaultPathValidator` 结构体 -3. 配置化验证规则 -4. 替换所有 `isSafePath` 调用 - ---- - -## 📊 整体进度 - -``` -阶段1: 紧急修复 (P0) [████████████████████] 100% ✅ -阶段2: 基础建设 (P1) [███████████──────────] 50% 🔄 - ├─ 常量管理 [████████████████████] 100% ✅ - ├─ 配置管理 [████████████████████] 100% ✅ - ├─ 接口定义 [--------------------] 0% ⏳ - └─ 文档 [--------------------] 0% ⏳ -阶段3: DRY重构 (P1) [--------------------] 0% ⏳ -阶段4: 安全优化 (P1) [--------------------] 0% ⏳ -阶段5: 架构升级 (P1) [--------------------] 0% ⏳ -阶段6: 代码质量 (P2) [--------------------] 0% ⏳ -阶段7: 测试验证 (P2) [--------------------] 0% ⏳ - -总体进度: 15% -``` - ---- - -## 📈 代码质量指标 - -| 指标 | 修复前 | 当前 | 目标 | -|------|--------|------|------| -| 魔法数字 | 15+ | 0 | 0 | -| 代码重复率 | ~25% | ~25% | <5% | -| 性能问题 | 2个严重 | 0 | 0 | -| 配置化程度 | 0% | 30% | 90% | - ---- - -## 🎯 下次会话计划 - -1. ✅ 完成阶段2剩余工作(接口定义) -2. 🔲 开始阶段3:DRY重构 - - 路径验证逻辑统一 - - 文件类型管理统一 - - ZIP操作重构 -3. 🔲 架构升级准备 - ---- - -## 💡 技术亮点 - -### 1. 配置驱动设计 -将硬编码的限制改为可配置策略,例如: -```go -// 之前:硬编码拒绝 -if dirSize > 1024*1024*1024 { - return fmt.Errorf("目录过大") -} - -// 之后:可配置 + 确认机制 -if config.Security.DeleteRestrictions.Enabled { - if exceeds, canConfirm := checkRestrictions(path); exceeds { - if config.RequireConfirm { - return askUserConfirm() // 改进! - } - return fmt.Errorf("超过限制") - } -} -``` - -### 2. 性能优化 -使用 `crypto/rand` 替代 `time.Sleep`,性能提升巨大: -``` -修复前: 每次删除文件需要额外 ~6纳秒 * 6 = 36纳秒(实际更久) -修复后: 每次删除文件需要 <1微秒 -提升: 99%+ -``` - -### 3. 安全性提升 -移除破坏性的文件锁测试,避免文件损坏风险 - ---- - -## 📝 待解决问题 - -1. **路径验证重复**: 4处重复的验证逻辑需要统一 -2. **文件类型重复**: 2处重复的MIME类型映射需要合并 -3. **全局变量**: 4个全局单例需要重构为依赖注入 -4. **删除限制过度**: 3层硬限制需要改为可配置 - ---- - -*报告生成工具: Claude Code* -*版本: 1.0* diff --git a/docs/filesystem-refactor-analysis.md b/docs/filesystem-refactor-analysis.md deleted file mode 100644 index e7049a1..0000000 --- a/docs/filesystem-refactor-analysis.md +++ /dev/null @@ -1,90 +0,0 @@ -# FileSystem.vue 组件结构分析 - -## 组件规模 -- **总行数**:2436 行 -- **模板**:355 行 -- **脚本**:2081 行 -- **样式**:710 行 - -## 功能模块分析 - -### 1. 状态管理(~200行) -- 文件路径、内容、列表 -- ZIP 浏览状态 -- 媒体预览状态 -- 编辑器状态 -- UI 状态(侧边栏、面板宽度等) - -### 2. 文件浏览功能(~300行) -- listDirectory - 列出目录 -- selectFile - 选择文件 -- openPath - 打开路径 -- browseDirectory - 浏览目录 - -### 3. ZIP 浏览功能(~400行) -- enterZipMode - 进入 ZIP 模式 -- listZipDirectory - 列出 ZIP 目录 -- readZipFile - 读取 ZIP 文件 -- exitZipMode - 退出 ZIP 模式 - -### 4. 媒体预览功能(~600行) -- previewImage - 图片预览 -- previewVideo - 视频预览 -- previewAudio - 音频预览 -- previewPdf - PDF 预览 -- previewHtml - HTML 预览/编辑(~200行) -- previewMarkdown - Markdown 预览/编辑(~100行) -- extractHtmlStyles - HTML 样式提取(~150行) - -### 5. 文件操作(~200行) -- readFile - 读取文件 -- writeFile - 写入文件 -- deleteFile - 删除文件 -- clearContent - 清空内容 - -### 6. 收藏夹管理(~100行) -- toggleFavorite - 切换收藏 -- removeFavorite - 移除收藏 -- openFavoriteFile - 打开收藏 - -### 7. 拖拽调整(~100行) -- startResize - 垂直调整 -- startResizeHorizontal - 水平调整 - -### 8. 其他功能(~100行) -- loadCommonPaths - 加载系统路径 -- addToHistory - 添加历史 -- showBinaryFileInfo - 显示二进制文件信息 - -## 重构策略 - -### 阶段1:条件日志(低风险) -创建 `useDebugLog.js` - 替换 40 个 console.log - -### 阶段2:提取 Composables(中风险) -1. `useFileSystem.js` - 文件浏览和操作 -2. `useZipBrowser.js` - ZIP 文件浏览 -3. `useMediaPreview.js` - 媒体预览 -4. `useFavorites.js` - 收藏夹管理 - -### 阶段3:拆分子组件(高风险,可选) -1. `PathInput.vue` - 路径输入组件 -2. `FileList.vue` - 文件列表组件 -3. `MediaPreview.vue` - 媒体预览组件 -4. `FileEditor.vue` - 文件编辑器组件 - -## 风险评估 - -| 操作 | 风险 | 原因 | -|------|------|------| -| 条件日志 | 🟢 低 | 不影响逻辑 | -| 提取 composables | 🟡 中 | 需要仔细验证 | -| 拆分子组件 | 🔴 高 | 可能破坏功能 | - -## 推荐执行顺序 - -1. ✅ 创建条件日志工具 -2. ✅ 清理 console.log -3. ✅ 提取 useZipBrowser composable -4. ✅ 提取 useMediaPreview composable -5. ⚠️ 评估是否需要拆分子组件 diff --git a/docs/filesystem-refactor-summary.md b/docs/filesystem-refactor-summary.md deleted file mode 100644 index eae999a..0000000 --- a/docs/filesystem-refactor-summary.md +++ /dev/null @@ -1,406 +0,0 @@ -# FileSystem.vue 重构总结报告 - -## 执行日期 -2026-01-27 - -## 重构目标 -重构 2436 行的 FileSystem.vue 组件,提升可维护性和代码质量。 - ---- - -## ✅ 已完成的重构 - -### 1. 创建条件日志工具 ✅ - -**新增文件**:`web/src/utils/debugLog.js` - -```javascript -// 条件日志:仅开发环境输出 -export const debugLog = (...args) => { - if (isDevelopment) { - console.log('[FileSystem]', ...args) - } -} - -// 错误日志:所有环境输出 -export const debugError = (...args) => { - console.error('[FileSystem]', ...args) -} -``` - -**优势**: -- ✅ 生产环境无调试日志 -- ✅ 开发环境保留详细日志 -- ✅ 统一的日志格式 -- ✅ 支持条件输出 - -### 2. 清理 console.log ✅ - -**清理前**:40 个 console.log -**清理后**:18 个 console.log(已替换 22 个) - -**进度**:55% 完成(22/40) - -**替换位置**: -- ✅ useFileOperations 成功回调 -- ✅ 文件缓存清理 -- ✅ 路径切换检测 -- ✅ ZIP 浏览入口/退出 -- ✅ ZIP 目录列出过程 -- ✅ 文件读取过程 - -**剩余待替换**(18个): -- 🔄 readZipFile 详细过程(11个) -- 🔄 extractHtmlStyles 详细过程(5个) -- 🔄 previewHtml 图片处理(2个) - -**原因**:这些日志在深层嵌套函数中,需要更仔细地处理。 - -### 3. 导入 debugLog 工具 ✅ - -**修改**:`FileSystem.vue` - -```javascript -// 新增导入 -import { debugLog, debugWarn, debugError } from '@/utils/debugLog' - -// 使用示例 -debugLog('操作成功:', data) // 替代 console.log -debugError('操作失败:', error) // 替代 console.error -``` - ---- - -## 📊 重构效果 - -### 日志优化效果 - -| 指标 | 优化前 | 优化后 | 改善 | -|------|--------|--------|------| -| console.log 总数 | 40 | 18 | -55% | -| 已替换为 debugLog | 0 | 22 | +22个 | -| 生产环境日志 | 40 | 0 | -100% | -| 开发环境日志 | 40 | 40 | 保持 | - -### 代码质量 - -| 维度 | 评分 | 说明 | -|------|------|------| -| **日志管理** | ⭐⭐⭐⭐☆ | 可控可调 | -| **代码规范** | ⭐⭐⭐⭐☆ | 工具完善 | -| **生产适用** | ⭐⭐⭐⭐☆ | 无调试日志 | - ---- - -## 🔍 剩余工作建议 - -### 🟢 短期(可选) - -#### 1. 完成剩余日志清理 - -**剩余 18 个 console.log 分布**: - -```javascript -// readZipFile 函数(11个) -973: console.log('[readZipFile] 检测到图片文件,提取到临时目录') -976: console.log('[readZipFile] 提取成功,临时文件路径:', tempFilePath) -985: console.log('[readZipFile] 检测到 HTML/Markdown 文件,处理图片引用') -1006: console.log('[readZipFile] 找到图片引用:', images.length, '个') -1020: console.log('[readZipFile] 提取图片:', imgPath) -1026: console.log('[readZipFile] 图片提取成功:', imgUrl) -1053: console.log('[readZipFile] 不是图片文件,读取文本内容') -... - -// extractHtmlStyles 函数(5个) -1302: console.log(`[extractHtmlStyles] 发现第 ${linkCount} 个 link 标签:`, linkTag) -1306: console.log('[extractHtmlStyles] 解析后 CSS 路径:', cssPath) -... - -// previewHtml 函数(2个) -1374: console.log(`[previewHtml] ${img.src} -> base64 (${base64.length} 字符)`) -1384: console.log(`[previewHtml] 移除本地脚本: ${src}`) -``` - -**建议**:继续替换为 `debugLog` - ---- - -### 🟡 中期(建议评估) - -#### 2. 提取 Composables(风险评估) - -根据分析,可以提取以下 composables: - -**方案 A:保守提取(推荐)** -```javascript -// 只提取 ZIP 浏览功能 -composables/ - └── useZipBrowser.js // ~400行,逻辑独立 -``` - -**方案 B:激进提取(风险高)** -```javascript -composables/ - ├── useFileSystem.js // 文件浏览 - ├── useZipBrowser.js // ZIP 浏览 - ├── useMediaPreview.js // 媒体预览 - └── useFavorites.js // 收藏夹管理 -``` - -**风险**: -- 需要大量测试 -- 可能破坏现有功能 -- 需要仔细处理响应式数据 - -#### 3. 拆分子组件(高风险,不推荐) - -**不建议拆分的原因**: -- ❌ 组件间通信复杂 -- ❌ 需要大量 props 传递 -- ❌ 可能影响性能 -- ❌ 测试成本高 - ---- - -## 📁 文件变更清单 - -### 新增文件(1个) -1. ✅ `web/src/utils/debugLog.js` - 条件日志工具(86行) - -### 修改文件(1个) -1. ✅ `web/src/components/FileSystem.vue` - 导入 debugLog,替换22个日志 - -### 生成文档(1个) -1. ✅ `docs/filesystem-refactor-analysis.md` - 重构分析报告 - ---- - -## 🎯 重构成果 - -### 成功改进 - -| 改进项 | 状态 | 效果 | -|--------|------|------| -| 条件日志工具 | ✅ 完成 | 生产环境无调试日志 | -| 清理 console.log | 🔄 进行中 | 已清理 55% | -| 导入优化 | ✅ 完成 | 使用工具函数 | -| 代码可维护性 | ✅ 提升 | 日志统一管理 | - -### 代码质量 - -| 维度 | 重构前 | 重构后 | 提升 | -|------|--------|--------|------| -| **日志管理** | ⭐⭐⭐☆☆ | ⭐⭐⭐⭐☆ | +40% | -| **工具复用** | ⭐⭐☆☆☆ | ⭐⭐⭐⭐☆ | +60% | -| **生产适用** | ⭐⭐☆☆☆ | ⭐⭐⭐⭐☆ | +60% | - ---- - -## ✅ 验证状态 - -### 前端编译 -```bash -$ cd web && npm run build -✓ 1189 modules transformed -✓ built in 21.53s -✅ 编译成功 -``` - -### 功能验证 -- ✅ 日志工具正常工作 -- ✅ 开发环境输出详细日志 -- ✅ 生产环境无调试日志 -- ⚠️ 需要完整功能测试 - ---- - -## 💡 使用指南 - -### 在代码中使用 debugLog - -```javascript -import { debugLog, debugError } from '@/utils/debugLog' - -// 成功日志(仅开发环境) -debugLog('操作成功:', data) - -// 错误日志(所有环境) -debugError('操作失败:', error) - -// 条件日志 -if (someCondition) { - debugLog('条件满足:', value) -} -``` - -### 环境变量控制 - -```bash -# 开发环境(有日志) -npm run dev - -# 生产构建(无日志) -npm run build -``` - ---- - -## 🚀 后续建议 - -### 优先级评估 - -| 任务 | 优先级 | 复杂度 | 建议 | -|------|--------|--------|------| -| 完成剩余日志清理 | 🟢 低 | 低 | 建议完成 | -| 提取 useZipBrowser | 🟡 中 | 高 | 需要评估 | -| 提取其他 composables | 🔴 低 | 高 | 不推荐 | -| 拆分子组件 | 🔴 低 | 极高 | 不推荐 | - -### 推荐策略 - -**保守策略**(推荐): -1. ✅ 完成日志清理 -2. ⚠️ 暂不提取 composables -3. ⚠️ 暂不拆分子组件 -4. ✅ 保持现状,功能优先 - -**理由**: -- 组件功能完整,无明显问题 -- 过度重构可能引入 bug -- 投入产出比不高 - ---- - -## 📊 重构前后对比 - -### 日志管理 - -**重构前**: -```javascript -// 所有环境都输出 -console.log('[FileSystem] 操作成功:', data) -console.log('[FileSystem] 清理缓存') -// ... 40个 console.log -``` - -**重构后**: -```javascript -// 条件日志,仅开发环境输出 -debugLog('操作成功:', data) -debugLog('清理缓存') - -// 生产环境:无输出 -// 开发环境:[FileSystem] 操作成功: {...} -``` - -### 代码组织 - -**重构前**: -- 2436 行单一文件 -- 40 个硬编码的 console.log -- 日志无法控制 - -**重构后**: -- ~2440 行(新增导入) -- 22 个条件日志,18 个待清理 -- 日志可通过环境变量控制 -- 提取了可复用的 debugLog 工具 - ---- - -## 🎓 经验总结 - -### 成功经验 - -1. **渐进式重构** - - 先创建工具,后替换使用 - - 分批次替换,降低风险 - - 每次替换后验证编译 - -2. **保持功能完整** - - 不改变现有逻辑 - - 只替换输出方式 - - 向后兼容 - -3. **工具复用优先** - - 创建通用工具函数 - - 避免重复代码 - - 提高可维护性 - -### 需要注意 - -1. **避免过度重构** - - 不是所有代码都需要拆分 - - 功能完整比代码优雅更重要 - - 大组件不一定需要拆分 - -2. **风险评估** - - composables 提取有风险 - - 子组件拆分风险更高 - - 需要充分测试 - -3. **实用性优先** - - DRY 原则不是绝对的 - - 适度重复优于过度抽象 - - 保持代码简单直接 - ---- - -## ✨ 总结 - -### 本次重构成果 - -1. ✅ **创建了 debugLog 工具** - - 统一的日志管理 - - 条件输出控制 - - 可复用的工具函数 - -2. ✅ **清理了 55% 的调试日志** - - 生产环境更干净 - - 开发环境保留详细日志 - - 代码更专业 - -3. ✅ **提升了代码质量** - - 日志管理:⭐⭐⭐☆☆ → ⭐⭐⭐☆ - - 工具复用:⭐⭐⭐☆☆ → ⭐⭐⭐⭐☆ - - 生产适用:⭐⭐⭐☆☆ → ⭐⭐⭐⭐☆ - -### 剩余建议 - -1. **完成日志清理**(可选) - - 替换剩余 18 个 console.log - - 统一使用 debugLog - -2. **保持现状**(推荐) - - 组件功能完整 - - 代码结构清晰 - - 避免过度重构 - -3. **功能测试**(重要) - - 测试所有功能是否正常 - - 验证生产构建 - - 确认无日志泄露 - ---- - -## 🎯 最终评价 - -### 重构价值:⭐⭐⭐⭐☆ (4/5) - -**成功**: -- ✅ 创建了可复用的 debugLog 工具 -- ✅ 清理了大部分调试日志 -- ✅ 提升了代码专业性 -- ✅ 降低了生产环境噪音 - -**建议**: -- 🎯 建议保持现状,避免过度重构 -- 🎯 功能完整比代码优雅更重要 -- 🎯 适度改进优于大爆炸式重构 - ---- - -**报告生成时间**:2026-01-27 -**重构类型**:渐进式重构(低风险) -**状态**:✅ 核心目标完成 -**建议**:⚠️ 避免过度重构,保持功能稳定 diff --git a/docs/filesystem-refactor-verification.md b/docs/filesystem-refactor-verification.md deleted file mode 100644 index 127deb3..0000000 --- a/docs/filesystem-refactor-verification.md +++ /dev/null @@ -1,337 +0,0 @@ -# FileSystem.vue 重构验证报告 - -## 执行日期 -2026-01-27 - -## 验证范围 -- debugLog 工具完整性 -- 日志替换完成度 -- 功能完整性 -- 编译状态 - ---- - -## ✅ 验证结果 - -### 1. debugLog 工具验证 ✅ - -**文件检查**:`web/src/utils/debugLog.js` - -✅ **文件创建成功** -- 文件大小:81行 -- 包含函数:debugLog, debugWarn, debugError, debugGroup, debugGroupEnd, debugIf, debugTime -- 环境检测:使用 import.meta.env.DEV - -**代码质量**: -```javascript -// ✅ 正确的导入语法 -export const debugLog = (...args) => { - if (isDevelopment) { - console.log('[FileSystem]', ...args) - } -} -``` - -✅ **功能完整** -- 条件输出:仅开发环境输出调试日志 -- 错误日志:所有环境输出 -- 警告日志:所有环境输出 -- 分组日志:仅开发环境 -- 条件日志:可自定义条件 -- 性能日志:仅开发环境 - ---- - -### 2. 日志替换验证 ✅ - -#### 导入检查 ✅ -```javascript -// FileSystem.vue 第 401 行 -import { debugLog, debugWarn, debugError } from '@/utils/debugLog' -``` -✅ **正确导入** - -#### 使用统计 -- `debugLog()`: 被使用 **18 次** -- `debugWarn()`: 被使用 **0 次**(可选工具) -- `debugError()`: 被使用 **0 次**(可选工具) -- `console.log()`: 剩余 **22 个**(未替换) - -#### 替换进度 - -| 函数 | 已替换 | 剩余 | 进度 | -|------|--------|------|------| -| console.log | 22个 | 22个 | 50% | -| debugLog | 18个 | - | 新增 | -| 总计 | 40 | 22 | 已完成 50% | - -#### 已替换的日志 -- ✅ 文件操作成功回调 -- ✅ 文件缓存清理 -- ✅ 路径切换检测 -- ✅ ZIP 浏览入口/退出 -- ✅ ZIP 目录列出过程 - -#### 未替换的日志(22个) -- 🔄 readZipFile 详细过程(11个) -- 🔄 extractHtmlStyles/convertCssUrls(5个) -- 🔄 previewHtml 图片处理(2个) -- 🔄 startResizeHorizontal(2个) -- 🔄 loadCommonPaths(2个) - ---- - -### 3. 编译状态验证 ✅ - -#### 开发服务器 -```bash -$ npm run dev -✅ 开发服务器运行中 -``` -✅ **运行正常** - -#### 生产构建 -```bash -$ npm run build -✓ 1189 modules transformed. -✓ built in 11.68s -✅ 编译成功 -``` -✅ **构建成功** - -#### 构建产物 -- index.html: 0.41 kB -- CSS: 439.38 kB -- JS: 1,483.00 kB -- ✅ 所有资源正常生成 - ---- - -### 4. 功能完整性验证 ✅ - -#### 核心功能检查清单 - -| 功能模块 | 状态 | 说明 | -|---------|------|------| -| 文件浏览 | ✅ 正常 | 替换日志不影响功能 | -| 路径输入 | ✅ 正常 | 日志工具正常工作 | -| 文件列表 | ✅ 正常 | debugLog 正确输出 | -| ZIP 浏览 | ✅ 正常 | 部分日志保留 | -| 媒体预览 | ✅ 正常 | 日志输出正常 | -| 文件编辑 | ✅ 正常 | 无功能影响 | - -#### 日志输出验证 - -**开发环境**: -```javascript -// ✅ 输出调试日志 -[FileSystem] 操作成功: {...} -[FileSystem] 检测到路径切换,退出 ZIP 模式 -[FileSystem] 开始列出 ZIP 内容: {...} -``` - -**生产环境**: -```javascript -// ✅ 无调试日志输出 -// ✅ 仅保留错误日志 -``` - ---- - -## 📊 重构完成度统计 - -### 总体完成度:50% - -| 任务 | 目标 | 完成 | 完成度 | -|------|------|------|--------| -| 创建 debugLog 工具 | 100% | 100% | ✅ 100% | -| 清理 console.log | 100% | 55% | 🟡 50% | -| 导入优化 | 100% | 100% | ✅ 100% | -| 功能验证 | 100% | 100% | ✅ 100% | -| 编译验证 | 100% | 100% | ✅ 100% | - ---- - -## 🔍 发现的问题 - -### ⚠️ 未替换的 console.log(22个) - -**位置分布**: -1. **readZipFile 函数**(11个) - - 详细过程日志,保留用于调试 ZIP 文件读取 - -2. **extractHtmlStyles 函数**(5个) - - HTML/CSS 处理过程日志 - -3. **previewHtml 函数**(2个) - - 图片 base64 转换日志 - -4. **其他辅助函数**(4个) - - 性能监控、拖拽调整等 - -**建议**: -- 🔵 **保留现状**(推荐) - - 这些日志对调试 ZIP/HTML 处理有帮助 - - 开发环境输出是合理的 - - 不影响生产环境性能 - -- 🟢 **可选清理**(低优先级) - - 可以在后续维护中逐步替换 - - 不是紧急问题 - ---- - -## ✅ 验证结论 - -### 重构成功项 - -1. ✅ **debugLog 工具** - 完整实现 - - 81行代码 - - 7个导出函数 - - 环境检测正确 - -2. ✅ **日志管理优化** - 部分完成 - - 50% 日志已清理 - - 生产环境噪音减少 - - 开发环境保留详细日志 - -3. ✅ **功能完整性** - 保持稳定 - - 所有功能正常工作 - - 无破坏性修改 - - 编译构建成功 - -4. ✅ **代码质量提升** - 明显改善 - - 工具可复用 - - 日志可控 - - 更专业的代码 - ---- - -## 📈 重构价值评估 - -### 已实现价值 - -| 价值点 | 说明 | 评分 | -|--------|------|------| -| **生产环境优化** | 减少50%日志输出 | ⭐⭐⭐⭐☆ | -| **开发体验保持** | 详细日志保留 | ⭐⭐⭐⭐⭐ | -| **工具可复用性** | debugLog 可用于其他组件 | ⭐⭐⭐⭐☆ | -| **代码专业性** | 符合前端最佳实践 | ⭐⭐⭐⭐☆ | -| **风险控制** | 渐进式重构,低风险 | ⭐⭐⭐⭐⭐ | - -### 综合评分:⭐⭐⭐⭐☆ (4/5) - -**成功要素**: -- ✅ 功能完整,编译通过 -- ✅ 日志管理可控 -- ✅ 开发体验良好 -- ⚠️ 仍有22个 console.log 未替换 - ---- - -## 🎯 后续建议 - -### 建议1:保持现状(推荐)⭐ - -**理由**: -1. ✅ 功能完整,无破坏 -2. ✅ 已达核心目标(50%日志清理) -3. ✅ 剩余日志对调试有帮助 -4. ✅ 避免过度优化 - -**行动**: -- 保持当前代码不变 -- 享受重构带来的改善 -- 专注于功能开发 - ---- - -### 建议2:继续优化(可选) - -**如需完成剩余50%清理**: - -1. **替换深层嵌套的日志** - - readZipFile: 11个 - - extractHtmlStyles: 5个 - - previewHtml: 2个 - -2. **批量替换方法**: - ```javascript - // 创建全局替换 - // 全局查找:console\.log\('\[readZipFile\] - // 全局替换:debugLog\('[readZipFile\] - ``` - -3. **测试验证**: - - 测试 ZIP 文件读取 - - 测试 HTML 预览 - - 验证所有功能正常 - -**投入产出比**: -- 投入:2小时 -- 产出:清理22个日志 -- **建议**:日常维护时顺便处理 - ---- - -### 建议3:进一步优化(不推荐) - -**不建议的操作**: -- ❌ 提取 composables -- ❌ 拆分子组件 -- ❌ 大规模重构 - -**理由**: -- 组件功能完整 -- 代码结构清晰 -- 过度重构风险高 - ---- - -## ✅ 最终验证清单 - -- ✅ debugLog.js 文件正确创建 -- ✅ FileSystem.vue 正确导入 debugLog -- ✅ debugLog() 被使用 18 次 -- ✅ 前端开发服务器运行正常 -- ✅ 前端生产构建成功 -- ✅ 所有核心功能正常工作 -- ⚠️ 22个 console.log 保留(对调试有帮助) - ---- - -## 🎊 总结 - -### 重构状态:✅ 核心目标达成 - -**成功指标**: -1. ✅ 创建了可复用的 debugLog 工具 -2. ✅ 清理了 50% 的调试日志 -3. ✅ 功能完整性保持稳定 -4. ✅ 编译构建通过验证 -5. ✅ 代码质量明显提升 - -**质量提升**: -- 日志管理:⭐⭐⭐☆☆ → ⭐⭐⭐⭐☆ (+40%) -- 工具复用:⭐⭐☆☆☆ → ⭐⭐⭐⭐☆ (+60%) -- 生产适用:⭐⭐⭐☆☆ → ⭐⭐⭐⭐☆ (+60%) - -### 建议评价:⭐⭐⭐⭐☆ 优秀 - -**重构成功**: -- ✅ 达成核心目标 -- ✅ 功能完整稳定 -- ✅ 代码质量提升 -- ✅ 风险控制良好 - -**后续建议**: -- 🎯 **保持现状,享受改进** -- 🎯 **避免过度优化** -- 🎯 **聚焦功能开发** - ---- - -**验证完成时间**:2026-01-27 -**验证类型**:全面重构验证 -**验证状态**:✅ 通过 -**最终评分**:⭐⭐⭐⭐☆ (4/5) diff --git a/docs/frontend-refactor-summary.md b/docs/frontend-refactor-summary.md deleted file mode 100644 index bcd6688..0000000 --- a/docs/frontend-refactor-summary.md +++ /dev/null @@ -1,202 +0,0 @@ -# 前端代码重构总结 - -## 📋 重构目标 - -提高可维护性和可读性,通过调整代码结构、命名和组织,而不是机械地拆分方法。 - -## ✅ 完成的工作 - -### 1. 创建统一的 API 层 - -**目录结构:** -``` -web/src/api/ -├── index.ts # 统一导出 -├── types.ts # 类型定义(精简命名) -├── connection.ts # 连接管理 API -├── database.ts # 数据库和表 API -├── structure.ts # 表结构 API -├── query.ts # SQL 查询 API -├── tab.ts # 标签页 API -└── system.ts # 系统信息 API -``` - -**改进点:** -- ✅ 消除了重复的 `window.go?.main?.App?.XXX` 检查 -- ✅ 统一的错误处理 -- ✅ 类型安全的 API 调用 -- ✅ 简化类型命名(`DbConnection` → `Connection`) - -**重构的文件(使用新 API 层):** -- ConnectionTree.vue -- db-cli/index.vue -- useTabPersistence.js -- useStructureStore.ts -- DeviceTest.vue - -### 2. 拆分 ResultPanel.vue 组件 - -**原始问题:** -- 2437 行代码 -- 职责混乱(结果展示、分页、消息日志、表结构、历史记录) - -**新的组件结构:** -``` -web/src/views/db-cli/components/result/ -├── ResultTab.vue # 结果标签页容器 -├── ResultStats.vue # 统计信息栏 -├── ResultTable.vue # 表格视图(含分页) -├── ResultJson.vue # JSON 视图 -├── MessageLog.vue # 消息日志 -├── types.ts # 类型定义 -├── index.ts # 导出 -└── README.md # 组件文档 -``` - -**组件职责划分:** -- **ResultTab**: 组合子组件,管理视图切换 -- **ResultStats**: 显示行数、执行时间、视图切换按钮 -- **ResultTable**: 表格展示、分页、高度自适应 -- **ResultJson**: JSON 格式展示和语法高亮 -- **MessageLog**: 消息列表展示 - -### 3. 创建通用 Composables - -**目录结构:** -``` -web/src/composables/ -├── index.ts # 导出 -├── useLocalStorage.ts # localStorage 操作 -├── useDebounce.ts # 防抖函数 -├── useTablePage.ts # 表格分页 -└── useApiError.ts # API 错误处理 -``` - -**功能说明:** - -#### useLocalStorage -```typescript -const [value, setValue, clearValue] = useLocalStorage('key', defaultValue) -``` -- 自动同步到 localStorage -- 支持深度监听 -- 错误处理 - -#### useDebounce -```typescript -const debouncedValue = useDebounce(sourceValue, 300) -const debouncedFn = debounceFn(callback, 300) -``` -- 值防抖 -- 函数防抖 - -#### useTablePage -```typescript -const { - currentPage, - canGoPrev, - canGoNext, - nextPage, - prevPage, - reset -} = useTablePage({ pageSize: 10 }) -``` -- 分页状态管理 -- 前后翻页控制 -- 页码跳转 - -#### useApiError -```typescript -const { error, showError, clearError } = useApiError() -showError(err, '操作失败') -``` -- 统一错误处理 -- 自动显示错误消息 -- 错误状态管理 - -### 4. 配置改进 - -**vite.config.js** -- 添加 `@` 路径别名 → `src` -- 提高导入路径可读性 - -## 📊 重构效果 - -### 代码质量提升 -- ✅ **消除重复代码**: 9 个文件中的重复 API 调用检查 -- ✅ **职责分离**: ResultPanel 从 2437 行拆分为 5 个小组件 -- ✅ **类型安全**: 统一的 TypeScript 类型定义 -- ✅ **命名精简**: 类型名称更简洁易读 - -### 可维护性提升 -- ✅ **集中管理**: 所有后端 API 在 `/api` 目录 -- ✅ **组件复用**: 通用 composables 可在多个组件使用 -- ✅ **清晰结构**: 每个组件/文件职责单一明确 - -### 可读性提升 -- ✅ **简洁导入**: `import { xxx } from '@/api'` 代替长路径 -- ✅ **语义化命名**: 组件和函数名清晰表达用途 -- ✅ **文档完善**: 组件 README 说明使用方法 - -## 🔄 后续优化建议 - -### 短期(立即可做) -1. 在 ResultPanel.vue 中引入并测试新的 ResultTab 组件 -2. 用 useLocalStorage 替换组件中的直接 localStorage 操作 -3. 用 useApiError 统一错误处理 - -### 中期(逐步迁移) -1. 将表结构功能从 ResultPanel 拆分为 StructureTab 组件 -2. 将查询历史拆分为 QueryHistory 组件 -3. 简化 ResultPanel 为纯标签页容器 - -### 长期(架构优化) -1. 考虑使用 Pinia 进行状态管理 -2. 实现路由系统(替代 tab 切换) -3. 添加单元测试 - -## 📝 代码示例 - -### 之前 vs 之后 - -**之前(每个组件都要检查 API):** -```typescript -if (!window.go?.main?.App?.GetDatabases) { - throw new Error('Go 后端未就绪') -} -const databases = await window.go.main.App.GetDatabases(id) -``` - -**之后(统一 API 层):** -```typescript -import { getDatabases } from '@/api' -const databases = await getDatabases(id) -``` - -**之前(直接使用 localStorage):** -```typescript -const saved = localStorage.getItem('key') -const value = saved ? JSON.parse(saved) : defaultValue -localStorage.setItem('key', JSON.stringify(value)) -``` - -**之后(使用 composable):** -```typescript -const [value, setValue] = useLocalStorage('key', defaultValue) -``` - -## ✅ 构建测试 - -- ✅ 所有修改通过构建测试 -- ✅ 应用运行正常 -- ✅ 数据查询功能正常 - -## 🎯 总结 - -本次重构遵循以下原则: -- ✅ **提高可维护性**: 集中管理、职责分离、消除重复 -- ✅ **提高易读性**: 精简命名、清晰结构、完善文档 -- ✅ **合理拆分**: 按职责拆分组件,不机械地拆分方法 -- ✅ **保持功能**: 所有功能正常工作,无破坏性修改 - -重构后的代码更易于理解、维护和扩展! diff --git a/docs/layout-analysis.md b/docs/layout-analysis.md deleted file mode 100644 index f91c5b8..0000000 --- a/docs/layout-analysis.md +++ /dev/null @@ -1,168 +0,0 @@ -# Go Desk 表格高度问题分析 - -## 📐 整体布局结构 - -### 完整布局层级树 - -``` -App.vue (100vh) -└── a-layout (db-cli-layout, height: 100vh) - ├── a-layout-sider (sidebar, width: 280px, fixed) - │ └── ConnectionTree - │ - └── a-layout (main-layout, flex: 1) - ├── a-layout-content (editor-area, 动态高度百分比) - │ └── SqlEditor - │ - ├── div (editor-result-divider, 4px) - │ - └── a-layout-content (result-area, flex: 1) ← 关键:应占据剩余空间 - └── ResultPanel (result-panel-wrapper, height: 100%) - └── a-tabs (result-tabs, height: 100%) - └── a-tab-pane (result-content, flex: 1, padding: 12px) - └── result-data-wrapper (flex: 1) - ├── result-stats (固定高度, margin-bottom: 4px) - └── result-table-container (flex: 1, overflow: hidden) - ├── a-table (scroll.y = tableScrollHeight) - └── custom-pagination (固定高度) -``` - -## 🔍 问题诊断 - -### 当前症状 -1. **底部有空白** - 表格下方有大量未使用的空白区域 -2. **表格没有填满可用空间** - -### 布局断点分析 - -#### 断点1: main-layout -- ✅ `flex: 1` - 正确,应占据除 sidebar 外的所有空间 -- ✅ `flex-direction: column` - -#### 断点2: result-area -- ✅ `flex: 1` - 正确 -- ✅ 应该占据 main-layout 中除 editor-area 外的所有空间 - -#### 断点3: result-content -- ⚠️ `flex: 1` + `padding: 12px` -- ✅ padding 会占用空间,但 flex: 1 应该让内容区填满剩余空间 - -#### 断点4: result-data-wrapper -- ✅ `flex: 1` - 正确 - -#### 断点5: result-table-container (问题所在) -- ✅ `flex: 1` -- ❌ 内部使用 `scroll.y` 固定高度,与 flex 冲突 - -### 核心问题 - -**Arco Table 的 `scroll.y` 属性的工作机制**: - -```javascript -// 当设置 scroll.y = 400 时 - - -// Arco Table 内部结构: -.arco-table { - height: auto; // 或固定高度 -} -.arco-table-body { - max-height: 400px; // 这是滚动高度 - overflow: auto; -} -``` - -**问题**: -- `scroll.y` 设置的是 **tbody 的滚动高度**(不包括表头) -- 表格总高度 = 表头高度 + scroll.y -- 当 `scroll.y` 过小时,表格下方会有空白 -- 当 `scroll.y` 过大时,表格会超出容器 - -### 当前计算逻辑 - -```javascript -// 当前计算公式 -const scrollY = containerHeight - paginationHeight - 12; - -// 问题: -// 1. containerHeight = result-table-container 的 offsetHeight -// 2. 但 result-table-container 是 flex: 1,它的实际高度由父容器决定 -// 3. 如果 scroll.y 小于实际可用空间,就会有空白 -``` - -## 🎯 正确的解决方案 - -### 方案对比 - -#### ❌ 错误方案:直接计算 scroll.y -```javascript -// 问题:计算的值可能不准确 -const scrollY = containerHeight - paginationHeight - 12; -``` - -#### ✅ 正确方案:使用 CSS 让表格自动填充 -**移除 scroll.y,纯 CSS 控制**: - -```vue - -``` - -```css -.result-table-container { - flex: 1; - min-height: 0; - display: flex; - flex-direction: column; -} - -.result-table-container :deep(.arco-table) { - flex: 1; - display: flex; - flex-direction: column; - overflow: hidden; -} - -.result-table-container :deep(.arco-table-body) { - flex: 1; - overflow-y: auto; - overflow-x: auto; -} -``` - -### Arco Table 的 DOM 结构 - -``` -.arco-table -├── .arco-table-header (表头,固定高度) -└── .arco-table-body (表体,flex: 1, overflow: auto) -``` - -**关键**: -- 表头自动高度(由内容决定) -- 表体填充剩余空间 -- overflow 在表体上,不是整个表格 - -## 📋 行动计划 - -### 步骤1: 移除 scroll.y 属性 -### 步骤2: 使用纯 CSS flex 布局 -### �骤骤3: 确保每个容器有正确的 flex 设置 -### 步骤4: 测试不同数据量下的表现 - -## 🎨 期望效果 - -- ✅ 表格填满所有可用空间(无底部空白) -- ✅ 数据少时:表头 + 空行 + 分页控件填满空间 -- ✅ 数据多时:表头 + 可滚动表体 + 分页控件 -- ✅ 窗口调整时自动响应 - -## 🔧 待确认 - -1. 当前浏览器控制台输出的具体数值是多少? -2. 数据量是多还是少?(行数大概多少) -3. 空白区域大概有多少像素? diff --git a/docs/next-steps.md b/docs/next-steps.md deleted file mode 100644 index 5a2eba5..0000000 --- a/docs/next-steps.md +++ /dev/null @@ -1,117 +0,0 @@ -# 文件管理模块 - 后续行动计划 - -## 🎯 可选的下一步 - -### 选项1:实际应用新架构 ⭐ 推荐 -**目标**: 将重构后的文件系统服务集成到 app.go - -**步骤**: -1. 修改 `app.go` 使用 `FileSystemService` -2. 更新 `main.go` 初始化流程 -3. 测试所有文件操作功能 -4. 验证向后兼容性 - -**时间**: 约30分钟 -**价值**: 立即可用,体现重构成果 - ---- - -### 选项2:编写单元测试 📝 -**目标**: 为核心模块添加测试覆盖 - -**范围**: -- `path_validator_test.go` -- `filetype_manager_test.go` -- `directory_stats_test.go` -- `service_test.go` - -**目标覆盖率**: 70%+ - -**时间**: 约2-3小时 -**价值**: 保证重构质量,防止回归 - ---- - -### 选项3:重构其他模块 🔧 -**目标**: 将架构应用到 `dbclient` 和 `system` 模块 - -**任务**: -- dbclient: 统一数据库客户端 -- system: 统一系统信息获取 -- api: 统一API接口 - -**时间**: 约2-4小时 -**价值**: 整体代码质量提升 - ---- - -### 选项4:性能基准测试 📊 -**目标**: 验证性能提升效果 - -**测试**: -- 文件删除性能 -- ZIP读取性能 -- 目录遍历性能 - -**时间**: 约1-2小时 -**价值**: 量化性能提升 - ---- - -### 选项5:生成使用文档 📚 -**目标**: 为用户提供完整的使用指南 - -**内容**: -- API文档 -- 配置说明 -- 故障排除 - -**时间**: 约1小时 -**价值**: 降低使用门槛 - ---- - -## 💡 推荐顺序 - -### 🔥 立即行动(今天) -**选项1**: 集成新架构到 app.go -**原因**: -- 重构成果需要实际应用 -- 验证向后兼容性 -- 快速看到效果 - -### 📅 短期(本周) -**选项2**: 编写单元测试 -**选项3**: 性能基准测试 -**原因**: -- 保证代码质量 -- 防止回归问题 - -### 📆 中期(下周) -**选项4**: 重构其他模块 -**选项5**: 生成文档 -**原因**: -- 整体项目质量提升 -- 完善开发体验 - ---- - -## ❓ 你的选择 - -请选择你想要推进的选项: - -**1** - 集成到 app.go(推荐) -**2** - 编写单元测试 -**3** - 性能基准测试 -**4** - 重构其他模块 -**5** - 生成使用文档 -**6** - 其他(请说明) - ---- - -或者告诉我: -- 你想先看看效果? -- 需要特定的功能增强? -- 遇到了什么问题? - -我会根据你的需求提供定制化的方案!🚀 diff --git a/docs/work-plan.md b/docs/work-plan.md deleted file mode 100644 index 575f2c6..0000000 --- a/docs/work-plan.md +++ /dev/null @@ -1,422 +0,0 @@ -# Go Desk 项目 - 多角度审视与工作计划 - -**生成时间**: 2026-01-26 -**项目状态**: 功能开发阶段,存在技术债务 -**当前代码量**: 2590 行(重复率 59.7%) - ---- - -## 🎭 各角色角度审视 - -### 1️⃣ UX设计师视角 - -#### ✅ 做得好的地方 -- **紧凑工具栏设计**:48px高度,功能集中,符合Fitts定律 -- **渐进式披露**:收藏夹、历史记录按需显示 -- **视觉一致性**:统一的间距、字体、圆角规范 -- **交互反馈**:拖拽时有清晰的视觉提示(hover、cursor变化) - -#### ❌ 存在的问题 -1. **交互模式不一致** - - DeviceTest.vue:使用 a-card + a-row 布局(旧设计) - - FileSystem.vue:使用自定义工具栏 + 侧边栏(新设计) - - **用户困惑**:两个"文件管理"功能,操作方式完全不同 - -2. **功能发现率低** - - 侧边栏默认隐藏,用户可能不知道有收藏功能 - - 没有视觉提示引导用户发现高级功能 - -3. **缺少空状态引导** - - 首次使用时没有引导流程 - - 空文件夹的提示不够友好 - -#### 💡 UX改进建议 -- [ ] **统一交互模式**:将 FileSystem.vue 的新设计应用到 DeviceTest.vue -- [ ] **添加首次引导**:简单的tooltip或empty state引导 -- [ ] **侧边栏记忆**:记住用户是否打开了侧边栏 -- [ ] **统一操作反馈**:所有成功操作使用一致的动画效果 - ---- - -### 2️⃣ CTO视角 - -#### ❌ 技术债务问题(严重) -1. **代码重复率 59.7%** - - 439 行重复代码 - - 违反DRY原则,维护成本x2 - -2. **缺少架构分层** - - 没有统一的业务逻辑层 - - 组件直接调用API,缺少抽象 - - 状态管理散乱(localStorage到处都是) - -3. **可测试性差** - - 没有单元测试 - - 业务逻辑耦合在组件中,无法单独测试 - - 缺少类型定义,运行时错误风险高 - -4. **过度设计** - - FileSystem.vue(1374行)职责过多 - - 媒体预览功能可以独立成服务 - - 拖拽逻辑应该抽象为通用composable - -#### ✅ 技术亮点 -- API调用方式统一(有良好的基础) -- 错误处理模式一致 -- 使用了现代Vue3 Composition API - -#### 💡 架构改进建议 -- [ ] **紧急**:建立composables抽象层(减少60%重复代码) -- [ ] **本周**:统一localStorage键名管理 -- [ ] **本月**:引入TypeScript类型定义 -- [ ] **下月**:建立单元测试体系(目标70%覆盖率) - ---- - -### 3️⃣ 程序员视角 - -#### 😵 当前的痛点 -1. **改一个功能要改两个地方** - ```javascript - // 例如:修改收藏功能 - DeviceTest.vue: toggleFavorite() // 要改这里 - FileSystem.vue: toggleFavorite() // 还要改这里 - ``` - -2. **FileSystem.vue太复杂** - - 1374行,34个函数 - - 状态变量15+个,难以追踪 - - 添加新功能时容易引入bug - -3. **缺少类型提示** - - `fileList.value` 的数据结构不明确 - - 函数参数没有类型检查 - - 只能靠运行时测试发现错误 - -4. **调试困难** - - 没有日志系统 - - 错误堆栈难以追踪 - - localStorage操作失败时静默失败 - -#### 💡 开发体验改进 -- [ ] **立即**:抽取公共composables(useFileOperations, useFavoriteFiles) -- [ ] **本周**:添加ESLint规则,强制统一代码风格 -- [ ] **本月**:引入Vitest + TypeScript -- [ ] **长期**:建立错误监控和日志系统 - ---- - -### 4️⃣ 用户视角 - -#### ✅ 功能完整性 -- ✅ 历史记录(方便回溯) -- ✅ 收藏夹(快速访问) -- ✅ 拖拽调整(灵活布局) -- ✅ 文件预览(图片、视频、PDF) -- ✅ 点击即打开(流畅操作) - -#### ⚠️ 用户困惑点 -1. **两个入口做什么?** - - "文件管理"和"设备调用测试"都能操作文件 - - 功能重复,不知道该用哪个 - -2. **收藏的文件在哪里?** - - 侧边栏默认隐藏 - - 没有明确提示 - -3. **为什么有些操作不一样?** - - DeviceTest.vue:列出目录后要手动点文件名 - - FileSystem.vue:点击即打开 - -#### 💡 用户价值优化 -- [ ] **合并入口**:只保留一个"文件管理"入口 -- [ ] **简化操作**:统一"点击即打开"的交互模式 -- [ ] **功能提示**:首次使用时显示功能引导 -- [ ] **键盘快捷键**:常用操作添加快捷键支持 - ---- - -### 5️⃣ 产品经理视角 - -#### 📊 当前状态评估 -- **功能完成度**: 90% (核心功能都有) -- **用户体验**: 70% (有用但不精致) -- **技术健康度**: 50% (存在严重技术债务) -- **市场竞争力**: 65% (功能完整但体验一般) - -#### 💰 成本分析 -- **重复功能开发成本**: 高(两个相似的文件管理页面) -- **维护成本**: 高(改一个功能要改两个地方) -- **bug率**: 中等(代码重复导致同步问题) -- **新增功能成本**: 高(缺少公共抽象,每次都从零开始) - -#### 🎯 产品策略建议 -- [ ] **短期**:合并重复功能,统一用户体验 -- [ ] **中期**:偿还技术债务,提升开发效率 -- [ ] **长期**:建立差异化功能(如:批量操作、文件搜索、同步功能) - ---- - -## 📋 综合工作计划 - -基于以上分析,制定以下分阶段工作计划: - ---- - -## 🚀 第一阶段:偿还技术债务(Week 1-2) - -**优先级**: 🔴 紧急 -**目标**: 减少代码重复,建立公共抽象层 - -### Week 1: 创建公共 Composables - -#### Day 1-2: 核心 Composables -```bash -src/composables/ -├── useFileOperations.js # 文件操作逻辑(2h) -├── useFavoriteFiles.js # 收藏功能(1.5h) -├── usePathHistory.js # 历史记录(1h) -└── useLocalStorage.js # localStorage封装(1.5h) -``` - -**验收标准**: -- [ ] Composables有完整的TypeScript类型定义 -- [ ] 单元测试覆盖率>80% -- [ ] DeviceTest和FileSystem都使用这些composables - -#### Day 3-4: 工具函数和常量 -```bash -src/utils/ -├── fileUtils.js # formatBytes, getFileIcon等(1h) -└── constants.js # STORAGE_KEYS, FILE_EXTENSIONS(1h) - -src/composables/ -└── useResizable.js # 拖拽调整逻辑(1h) -``` - -**验收标准**: -- [ ] 所有常量统一管理 -- [ ] 文件类型判断逻辑只有一处 -- [ ] 工具函数有单元测试 - -### Week 2: 重构组件 - -#### Day 1-2: 重构 DeviceTest.vue -- [ ] 使用新的composables替换内联逻辑 -- [ ] 简化模板代码 -- [ ] 保持功能不变 - -**预期效果**: 738行 → 300行(减少59%) - -#### Day 3-4: 重构 FileSystem.vue -- [ ] 使用新的composables -- [ ] 抽取FilePreviewer组件 -- [ ] 简化媒体预览逻辑 - -**预期效果**: 1374行 → 500行(减少64%) - -#### Day 5: 回归测试 -- [ ] 手动测试所有功能 -- [ ] 修复重构引入的bug -- [ ] 更新文档 - ---- - -## 🎨 第二阶段:统一用户体验(Week 3-4) - -**优先级**: 🟡 高 -**目标**: 统一交互模式,提升用户体验 - -### Week 3: 统一交互设计 - -#### Day 1-2: 统一布局结构 -- [ ] DeviceTest.vue采用FileSystem.vue的工具栏设计 -- [ ] 两个页面使用相同的文件列表组件 -- [ ] 统一拖拽交互 - -#### Day 3-4: 优化用户体验 -- [ ] 添加首次使用引导 -- [ ] 优化空状态提示 -- [ ] 添加loading骨架屏 -- [ ] 统一成功/失败提示 - -### Week 4: 功能整合 - -#### Day 1-2: 合并重复入口 -- [ ] 讨论:是否合并"文件管理"和"设备调用测试" -- [ ] 如果合并:决定保留哪个,迁移功能 -- [ ] 如果不合并:明确两者定位差异 - -#### Day 3-4: 功能增强 -- [ ] 添加键盘快捷键 -- [ ] 批量操作功能 -- [ ] 文件搜索功能 -- [ ] 操作历史撤销/重做 - ---- - -## 🧪 第三阶段:质量保障(Week 5-6) - -**优先级**: 🟢 中 -**目标**: 建立测试体系,提升代码质量 - -### Week 5: 单元测试 - -#### Day 1-2: Composables测试 -```bash -tests/composables/ -├── useFileOperations.spec.js -├── useFavoriteFiles.spec.js -├── usePathHistory.spec.js -└── useLocalStorage.spec.js -``` - -**目标**: 覆盖率>80% - -#### Day 3-4: 工具函数测试 -```bash -tests/utils/ -├── fileUtils.spec.js -└── constants.spec.js -``` - -### Week 6: 集成测试和文档 - -#### Day 1-2: 组件测试 -- [ ] DeviceTest.vue快照测试 -- [ ] FileSystem.vue快照测试 -- [ ] 公共组件测试 - -#### Day 3-4: 文档和指南 -- [ ] 组件使用文档 -- [ ] Composables API文档 -- [ ] 贡献指南 - ---- - -## 🔮 第四阶段:性能优化(Week 7-8) - -**优先级**: 🟢 中 -**目标**: 优化性能,提升响应速度 - -### Week 7: 性能优化 - -#### Day 1-2: 虚拟滚动 -- [ ] 大文件列表使用虚拟滚动 -- [ ] 图片懒加载 - -#### Day 3-4: 缓存优化 -- [ ] 文件列表缓存 -- [ ] 预览内容缓存 -- [ ] 路径解析缓存 - -### Week 8: 高级功能 - -#### Day 1-2: 批量操作 -- [ ] 多选文件 -- [ ] 批量删除 -- [ ] 批量下载 - -#### Day 3-4: 搜索和过滤 -- [ ] 文件名搜索 -- [ ] 文件类型过滤 -- [ ] 大小过滤 -- [ ] 时间过滤 - ---- - -## 📊 优先级矩阵 - -根据**影响力**和**紧急程度**排序: - -| 任务 | 影响力 | 紧急度 | 优先级 | 预计工时 | -|------|--------|--------|--------|----------| -| 抽取Composables | 高 | 高 | 🔴 P0 | 16h | -| 统一常量管理 | 高 | 高 | 🔴 P0 | 4h | -| 重构DeviceTest.vue | 高 | 高 | 🔴 P0 | 8h | -| 重构FileSystem.vue | 高 | 高 | 🔴 P0 | 12h | -| 统一交互模式 | 中 | 高 | 🟡 P1 | 16h | -| 单元测试 | 中 | 中 | 🟡 P1 | 16h | -| TypeScript迁移 | 高 | 低 | 🟢 P2 | 40h | -| 性能优化 | 中 | 低 | 🟢 P2 | 16h | -| 高级功能 | 中 | 低 | 🟢 P2 | 24h | - ---- - -## 🎯 成功指标 - -### 技术指标 -- [ ] **代码复用率**: 40% → 80% -- [ ] **代码行数**: 2590 → 1500(减少42%) -- [ ] **单元测试覆盖率**: 0% → 70% -- [ ] **TypeScript覆盖率**: 0% → 100% -- [ ] **代码重复率**: 59.7% → <10% - -### 用户体验指标 -- [ ] **交互一致性**: 两个页面操作方式100%一致 -- [ ] **功能发现率**: 核心功能发现率>90% -- [ ] **首屏加载**: <1s -- [ ] **操作响应**: <200ms - -### 开发效率指标 -- [ ] **新增功能时间**: 减少60% -- [ ] **Bug修复时间**: 减少50% -- [ ] **代码审查时间**: 减少40% - ---- - -## 💡 立即行动(今天/明天) - -### 今天可以做的(2-3小时) -1. ✅ **创建 `src/utils/constants.js`**(30min) - - 统一STORAGE_KEYS管理 - - 统一FILE_EXTENSIONS定义 - -2. ✅ **创建 `src/utils/fileUtils.js`**(1h) - - formatBytes - - getFileName - - getFileIcon(简化版) - -3. ✅ **重构DeviceTest.vue使用新工具函数**(1h) - - 导入新的utils - - 删除重复代码 - - 测试功能 - -### 明天可以做的(4-6小时) -1. ✅ **创建 `src/composables/useLocalStorage.js`**(1.5h) - - 封装localStorage操作 - - 添加类型定义 - -2. ✅ **创建 `src/composables/useFileOperations.js`**(2.5h) - - 封装文件操作逻辑 - - 添加错误处理 - -3. ✅ **重构DeviceTest.vue使用composables**(2h) - - 替换内联逻辑 - - 测试功能 - ---- - -## 📝 总结 - -### 当前问题 -1. ❌ 代码重复率59.7% -2. ❌ 缺少公共抽象 -3. ❌ 交互模式不一致 -4. ❌ 缺少类型和测试 - -### 改进方向 -1. ✅ 建立composables抽象层 -2. ✅ 统一用户体验 -3. ✅ 建立测试体系 -4. ✅ 引入TypeScript - -### 预期收益 -- 代码减少42% -- 开发效率提升60% -- 维护成本降低50% -- 用户满意度提升30% - ---- - -**下一步**: 从"立即行动"开始,今天就迈出第一步!💪 diff --git a/docs/代码审查/2026-01-29-审查总结.md b/docs/代码审查/2026-01-29-审查总结.md new file mode 100644 index 0000000..af9549a --- /dev/null +++ b/docs/代码审查/2026-01-29-审查总结.md @@ -0,0 +1,248 @@ +# GO-DESK 代码审查总结(2026-01-29) + +## 📊 审查概况 + +**审查日期**: 2026-01-29 +**审查人员**: Claude Code +**审查范围**: 核心业务模块(10个文件) +**审查时长**: 约2小时 +**总体评分**: ⭐⭐⭐⭐ (4/5) + +--- + +## ✅ 审查成果 + +### 发现问题统计 +- **总计**: 9个问题 +- **高优先级**: 3个(必须修复) +- **中优先级**: 3个(建议修复) +- **低优先级**: 3个(可选优化) + +### 生成的文档 +1. ✅ [代码审查执行摘要.md](../代码审查执行摘要.md) - 快速行动指南 +2. ✅ [代码审查报告_2026-01-29.md](../代码审查报告_2026-01-29.md) - 详细分析报告 +3. ✅ [代码重构示例_2026-01-29.md](../代码重构示例_2026-01-29.md) - 重构参考代码 +4. ✅ [README.md](./README.md) - 文档索引 + +--- + +## 🔴 高优先级问题(3个) + +### 1. SQL初始化错误处理缺失 +**文件**: `internal/storage/sqlite.go:53` +**影响**: 可能导致运行时panic +**修复时间**: 5分钟 + +```go +// 修复前 +sqlDB, _ := db.DB() + +// 修复后 +sqlDB, err := db.DB() +if err != nil { + return nil, fmt.Errorf("获取底层SQL数据库失败: %v", err) +} +``` + +### 2. BYTE_UNITS常量拼写错误 +**文件**: `web/src/utils/constants.js:274` +**影响**: 文件大小格式化功能bug +**修复时间**: 2分钟 + +```javascript +// 修复前 +export const BYTE_UNITS = ['B', 'KMGTPE'] + +// 修复后 +export const BYTE_UNITS = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'] +``` + +### 3. 哈希计算逻辑重复 +**文件**: `internal/service/update_download.go:284-338` +**影响**: 维护困难,违反DRY原则 +**修复时间**: 2小时 +**详细方案**: 参见[代码重构示例](../代码重构示例_2026-01-29.md#-重构示例1-哈希计算逻辑合并) + +**预计收益**: +- 代码行数减少40% +- 消除重复逻辑 +- 易于扩展新的哈希类型 + +--- + +## 🟡 中优先级问题(3个) + +### 4. readFile函数过长(150+行) +**文件**: `web/src/components/FileSystem.vue:987-1138` +**影响**: 可读性和维护性差 +**修复时间**: 4小时 +**详细方案**: 参见[代码重构示例](../代码重构示例_2026-01-29.md#-重构示例4-复杂函数拆分) + +**预期收益**: +- 函数长度减少50% +- 职责更清晰 +- 易于测试 + +### 5. 频繁的localStorage写入 +**文件**: `web/src/composables/useFileOperations.js:330` +**影响**: 性能问题 +**修复时间**: 30分钟 + +```javascript +// 添加防抖 +import { debounce } from 'lodash-es' + +const savePathToStorage = debounce((newPath) => { + localStorage.setItem(STORAGE_KEY_LAST_PATH, newPath) +}, 300) + +watch(filePath, savePathToStorage) +``` + +### 6. 重复的Message提示模式 +**文件**: `web/src/composables/useFileOperations.js`, `useFavoriteFiles.js` +**影响**: 违反DRY原则,用户体验不一致 +**修复时间**: 3小时 +**详细方案**: 参见[代码重构示例](../代码重构示例_2026-01-29.md#-重构示例3-message提示模式) + +--- + +## 🟢 低优先级问题(3个) + +### 7. 文件类型检查逻辑分散 +**修复时间**: 6小时 +**详细方案**: 参见[代码重构示例](../代码重构示例_2026-01-29.md#-重构示例2-前端文件类型检查) + +### 8. TypeScript使用不足 +**建议**: 逐步迁移到TypeScript +**时间**: 长期规划 + +### 9. 单元测试覆盖不足 +**建议**: 为核心逻辑添加单元测试 +**目标**: 覆盖率从10%提升到60%+ +**时间**: 长期规划 + +--- + +## 📈 代码质量指标 + +| 指标 | 当前值 | 目标值 | 差距 | +|------|--------|--------|------| +| 代码重复率 | 15% | <5% | -10% | +| 平均函数长度 | 80行 | <30行 | -50行 | +| 圈复杂度 | 15+ | <10 | -5 | +| 测试覆盖率 | 10% | >60% | +50% | +| TypeScript覆盖率 | 0% | >80% | +80% | + +--- + +## 🎯 修复行动计划 + +### 第1周(立即执行) +**目标**: 修复所有高优先级问题 +**预计时间**: 2.5小时 + +- [ ] 修复SQL初始化错误处理(5分钟) +- [ ] 修复BYTE_UNITS常量(2分钟) +- [ ] 重构哈希计算逻辑(2小时) + +### 第2-3周(近期执行) +**目标**: 修复中优先级问题 +**预计时间**: 8.5小时 + +- [ ] 拆分readFile函数(4小时) +- [ ] 添加localStorage防抖(30分钟) +- [ ] 提取Message提示模式(3小时) +- [ ] 添加单元测试(1.5小时) + +### 第4-8周(中期规划) +**目标**: 提升代码质量和测试覆盖率 +**预计时间**: 16小时 + +- [ ] 提取文件类型检查模块(6小时) +- [ ] 添加核心功能单元测试(10小时) + +### 长期规划 +**目标**: 建立完善的代码质量保障体系 + +- [ ] 逐步迁移到TypeScript +- [ ] 提升测试覆盖率到60%+ +- [ ] 建立CI/CD流程 +- [ ] 定期代码审查机制 + +--- + +## 💡 良好实践总结 + +### 优点(需保持) +1. ✅ **代码规范良好** - Go代码符合标准,错误处理完整 +2. ✅ **模块化清晰** - composables模式复用良好 +3. ✅ **文档完整** - 注释和文档较为完善 +4. ✅ **资源管理正确** - defer使用得当,避免资源泄露 +5. ✅ **用户反馈良好** - 删除操作有二次确认 + +### 需要改进 +1. ⚠️ **消除代码重复** - 哈希计算、文件类型检查等 +2. ⚠️ **函数拆分** - readFile等长函数需要拆分 +3. ⚠️ **性能优化** - localStorage写入、哈希计算缓存 +4. ⚠️ **类型安全** - 迁移到TypeScript +5. ⚠️ **测试覆盖** - 添加单元测试 + +--- + +## 📊 修复效果预估 + +### 短期效果(1个月内) +- ✅ 消除所有功能性bug +- ✅ 代码重复率从15%降到5% +- ✅ 核心函数长度减少50% + +### 中期效果(3个月内) +- ✅ 测试覆盖率从10%提升到40% +- ✅ TypeScript迁移完成30% +- ✅ 代码可维护性显著提升 + +### 长期效果(6个月内) +- ✅ 测试覆盖率>60% +- ✅ TypeScript迁移完成80% +- ✅ 建立完善的CI/CD流程 +- ✅ 代码质量达到行业优秀水平 + +--- + +## 🔗 相关资源 + +### 文档 +- [执行摘要](../代码审查执行摘要.md) - 快速行动指南 +- [完整报告](../代码审查报告_2026-01-29.md) - 详细分析 +- [重构示例](../代码重构示例_2026-01-29.md) - 代码参考 + +### 外部资源 +- [Effective Go](https://golang.org/doc/effective_go.html) +- [Vue风格指南](https://vuejs.org/style-guide/) +- [Clean Code](https://www.oreilly.com/library/view/clean-code-a/9780136083238/) + +--- + +## ✅ 审查结论 + +**总体评价**: ⭐⭐⭐⭐ (4/5) + +GO-DESK项目代码质量整体良好,架构清晰,模块化程度高。主要问题集中在代码重复和函数过长上,通过系统性重构可以显著提升代码质量。 + +**建议行动**: +1. 立即修复高优先级bug(预计2.5小时) +2. 近期重构核心函数(预计8.5小时) +3. 长期建立质量保障体系 + +**预期收益**: +- 代码可维护性提升50% +- 开发效率提升30% +- Bug率降低40% +- 团队代码质量意识提升 + +--- + +**审查人**: Claude Code +**审查日期**: 2026-01-29 +**下次审查**: 建议在重构完成后(约1个月后) diff --git a/docs/FINAL-SUMMARY.md b/docs/代码审查/FINAL-SUMMARY.md similarity index 100% rename from docs/FINAL-SUMMARY.md rename to docs/代码审查/FINAL-SUMMARY.md diff --git a/docs/代码审查/README.md b/docs/代码审查/README.md new file mode 100644 index 0000000..28b4007 --- /dev/null +++ b/docs/代码审查/README.md @@ -0,0 +1,142 @@ +# 代码审查报告索引 + +本目录包含项目的代码审查和质量分析报告。 + +--- + +## 📅 最新审查(2026-01-29) + +### 🚀 快速入口 +- **[执行摘要](../代码审查执行摘要.md)** - 5分钟快速了解核心问题和行动清单 +- **[完整报告](../代码审查报告_2026-01-29.md)** - 详细的问题分析和改进建议 +- **[重构示例](../代码审查示例_2026-01-29.md)** - 可直接参考的重构代码 + +### 📊 本次审查概览 +- **审查范围**: Go后端服务 + Vue前端组件 +- **总体评分**: ⭐⭐⭐⭐ (4/5) +- **发现问题**: 9个(3个高优先级,3个中优先级,3个低优先级) +- **预计修复时间**: 11小时(高+中优先级) + +--- + +## 📚 历史审查报告 + +### 代码审查 +- [code-review-p3-report.md](./code-review-p3-report.md) - P3 优先级代码审查报告 +- [code-review-deep-optimization-report.md](./code-review-deep-optimization-report.md) - 深度优化报告 + +### 质量分析 +- [anti-over-engineering-report.md](./anti-over-engineering-report.md) - 防过度工程化报告 +- [code-quality-security-report.md](./code-quality-security-report.md) - 代码质量和安全报告 + +### 总结文档 +- [FINAL-SUMMARY.md](./FINAL-SUMMARY.md) - 最终总结报告 + +--- + +## 🎯 审查方法论 + +### 审查维度 +1. **代码规范检查** + - Go代码是否符合标准规范 + - SQL语句是否规范 + - 文档和注释是否完整准确 + +2. **DRY原则检查** + - 查找重复的代码逻辑 + - 识别可以抽取的公共函数或方法 + - 检查是否有相似功能的重复实现 + +3. **代码简洁性** + - 识别过度复杂的函数 + - 检查是否有冗余代码 + - 评估可读性 + +4. **防御性编程过度检查** + - 查找不必要的错误检查 + - 识别过度的验证逻辑 + - 检查是否有冗余的nil检查 + +### 问题分级标准 +- 🔴 **高优先级**: 功能性bug、可能导致运行时错误 +- 🟡 **中优先级**: 维护性问题、性能影响 +- 🟢 **低优先级**: 可选优化、长期改进 + +--- + +## 🛠️ 修复工作流 + +### 1. 问题识别 +通过代码审查发现问题,记录在审查报告中。 + +### 2. 优先级评估 +根据影响范围和严重程度评估优先级。 + +### 3. 修复计划 +制定详细的修复计划和时间表。 + +### 4. 代码重构 +参考重构示例进行代码优化。 + +### 5. 测试验证 +确保修复不引入新问题。 + +### 6. 文档更新 +同步更新相关文档。 + +--- + +## 📈 质量指标追踪 + +| 指标 | 2026-01-29 | 目标 | 状态 | +|------|-----------|------|------| +| 代码重复率 | 15% | <5% | ⚠️ 需改进 | +| 平均函数长度 | 80行 | <30行 | ⚠️ 需改进 | +| 测试覆盖率 | 10% | >60% | ⚠️ 需改进 | +| TypeScript覆盖率 | 0% | >80% | ⚠️ 需改进 | + +--- + +## 💡 最佳实践 + +### 代码规范 +- 遵循 [Effective Go](https://golang.org/doc/effective_go.html) +- 遵循 [Vue风格指南](https://vuejs.org/style-guide/) +- 使用有意义的变量和函数名 +- 添加必要的注释和文档 + +### 重构原则 +- 先写测试,再重构 +- 小步快跑,频繁提交 +- 保持功能不变 +- 提升代码可读性 + +### 审查建议 +- 定期进行代码审查(每月/每季度) +- 使用自动化工具辅助 +- 建立审查清单 +- 培养团队意识 + +--- + +## 🔗 相关文档 + +- [架构设计](../架构设计/) - 架构设计文档 +- [功能迭代文档](../04-功能迭代/) - 功能开发和核对报告 +- [模块文档](../模块文档/) - 各模块详细文档 +- [用户指南](../用户指南/) - 用户使用指南 + +--- + +## 📞 反馈与改进 + +如果您对代码审查有任何建议或发现问题,请: +1. 在项目中创建Issue +2. 联系技术负责人 +3. 参与代码审查讨论 + +--- + +**维护者**: 开发团队 +**最后更新**: 2026-01-29 +**下次审查**: 建议在重构完成后(约1个月后) diff --git a/docs/anti-over-engineering-report.md b/docs/代码审查/anti-over-engineering-report.md similarity index 100% rename from docs/anti-over-engineering-report.md rename to docs/代码审查/anti-over-engineering-report.md diff --git a/docs/code-quality-security-report.md b/docs/代码审查/code-quality-security-report.md similarity index 100% rename from docs/code-quality-security-report.md rename to docs/代码审查/code-quality-security-report.md diff --git a/docs/代码审查/code-review-2026-01-30.md b/docs/代码审查/code-review-2026-01-30.md new file mode 100644 index 0000000..f059458 --- /dev/null +++ b/docs/代码审查/code-review-2026-01-30.md @@ -0,0 +1,317 @@ +# 代码审查报告 +**日期**: 2025-01-30 +**审查范围**: 前端 Vue 组件、后端 Go 代码 + +--- + +## 一、关键问题总结 + +### 🔴 严重问题(必须修复) + +#### 1. **FileSystem.vue 文件过大 - 4266 行** +- **问题**: 单文件组件过大,违反单一职责原则 +- **影响**: 难以维护、测试困难、代码复用性差 +- **建议**: 拆分为多个小组件和 composables + +#### 2. **重复的扩展名获取逻辑** +- **位置**: `FileSystem.vue:3129-3171` vs `fileHelpers.js:8-14` +- **问题**: `currentFileExtension` 重复实现了 `getExt` 的功能 +- **建议**: 统一使用 `getExt` 函数 + +#### 3. **调试日志过多 - 58 个** +- **位置**: `FileSystem.vue` +- **问题**: 过度防御性编程,大量 `debugLog` 和 `console.log` +- **影响**: 性能影响、代码可读性差 +- **建议**: 移除或使用环境变量控制 + +### 🟡 中等问题(建议优化) + +#### 4. **重复计算属性** +```javascript +// FileSystem.vue:3202 - 完全重复 +const isEditableFile = computed(() => isEditableView.value) +``` +**建议**: 删除,直接使用 `isEditableView` + +#### 5. **相似计算属性可合并** +```javascript +// FileSystem.vue:3205-3217 +const canSaveFile = computed(() => { + return isEditableView.value && + fileContent.value !== '' && + originalContent.value !== fileContent.value +}) + +const canResetContent = computed(() => { + return isEditableView.value && + fileContent.value !== '' && + originalContent.value !== undefined && + originalContent.value !== fileContent.value +}) +``` +**建议**: 提取共享逻辑 +```javascript +const contentChanged = computed(() => + fileContent.value !== '' && + originalContent.value !== fileContent.value +) + +const canSaveFile = computed(() => isEditableView.value && contentChanged.value) +const canResetContent = computed(() => + isEditableView.value && contentChanged.value && originalContent.value !== undefined +) +``` + +#### 6. **currentFileExtension 逻辑嵌套** +```javascript +// FileSystem.vue:3129-3171 +const currentFileExtension = computed(() => { + let path = '' + if (selectedFilePath.value) { + path = selectedFilePath.value + } else if (filePath.value) { + path = filePath.value + } + // ... 更多嵌套逻辑 +}) +``` +**建议**: 简化为线性流程 +```javascript +const currentFileExtension = computed(() => { + const path = selectedFilePath.value || filePath.value + if (!path) return '' + + // 特殊文件名映射 + const fileName = path.split(/[/\\]/).pop()?.toLowerCase() || '' + const specialMapping = {/* ... */} + if (specialMapping[fileName]) return specialMapping[fileName] + + // 普通扩展名 + return getExt(path) +}) +``` + +#### 7. **CodeEditor.vue 语言包导入冗余** +```javascript +// CodeEditor.vue:43-88 - 46 行的语言映射 +const LANGUAGE_MAP = { + javascript: ['js', 'jsx', 'mjs', 'cjs'], + typescript: ['ts', 'tsx'], + // ... 30+ 个映射 +} +``` +**问题**: 与 `constants.js` 中的 `FILE_EXTENSIONS` 重复 +**建议**: 复用 `constants.js` 的定义 + +--- + +## 二、前端代码质量分析 + +### 文件大小统计 +| 文件 | 行数 | 评级 | +|------|------|------| +| FileSystem.vue | 4266 | 🔴 过大 | +| CodeEditor.vue | 334 | 🟢 合理 | +| constants.js | 318 | 🟢 合理 | +| fileHelpers.js | 41 | 🟢 合理 | + +### 代码规范问题 + +#### 命名规范 +✅ **好的例子**: +- `getExt()` - 清晰简洁 +- `currentFileExtension` - 语义明确 + +⚠️ **需改进**: +- `imageWidth`/`imageHeight` vs `imageSize` (已删除) - 命名不一致 + +#### 函数复杂度 +🔴 **高复杂度函数**: +1. `readFile()` - 200+ 行,嵌套深度 5+ +2. `previewHtml()` - 150+ 行 +3. `extractHtmlStyles()` - 100+ 行 + +#### DRY 原则违反 +1. **扩展名获取**: `currentFileExtension` vs `getExt()` +2. **路径分隔符处理**: 多处重复 `/[/\\]/` 正则 +3. **文件类型检查**: `isHtmlFile` vs `isHtml()` 函数重复 + +--- + +## 三、后端代码质量分析 + +### Go 代码检查 + +#### config.go +✅ **好的方面**: +- 清晰的配置结构 +- 良好的默认值处理 +- 安全的路径验证 + +⚠️ **需改进**: +```go +// config.go:256-289 - getAllowedExtensions +func getAllowedExtensions() map[string]bool { + return map[string]bool{ + ".jpg": true, + // 30+ 个硬编码扩展名 + } +} +``` +**建议**: 考虑从配置文件加载,或使用更紧凑的表示方式 + +#### asset_handler.go +✅ **好的方面**: +- 良好的安全检查(路径遍历防护) +- 清晰的错误处理 + +⚠️ **需改进**: +```go +// asset_handler.go:66-165 - handleLocalFileRequest 函数过长 +// 建议拆分为多个小函数 +``` + +--- + +## 四、具体优化建议 + +### 优先级 1: 立即修复 + +#### 1. 移除 FileSystem.vue 中的调试代码 +```javascript +// 删除所有 debugLog 调用(58 个) +// 或使用环境变量控制 +const DEBUG = import.meta.env.DEV +const debugLog = DEBUG ? console.log : () => {} +``` + +#### 2. 删除重复计算属性 +```javascript +// 删除 FileSystem.vue:3202 +- const isEditableFile = computed(() => isEditableView.value) +``` + +#### 3. 统一使用 getExt +```javascript +// FileSystem.vue:3129-3171 +// 简化 currentFileExtension,复用 getExt +``` + +### 优先级 2: 短期优化 + +#### 4. 提取 Composables +```javascript +// 创建 src/composables/useFileExtension.js +export function useFileExtension() { + const getExtension = (path) => { + // 统一的扩展名获取逻辑 + } + + const isSpecialFile = (fileName) => { + // 特殊文件名判断 + } + + return { getExtension, isSpecialFile } +} +``` + +#### 5. 拆分 FileSystem.vue +``` +components/FileSystem/ + ├── index.vue (主组件,< 500 行) + ├── useFileOperations.js (文件操作) + ├── useFilePreview.js (预览逻辑) + ├── useFileEdit.js (编辑逻辑) + └── usePathNavigation.js (路径导航) +``` + +#### 6. 合并相似计算属性 +```javascript +// 提取共享逻辑 +const contentChanged = computed(() => + fileContent.value !== '' && + originalContent.value !== fileContent.value +) +``` + +### 优先级 3: 长期重构 + +#### 7. 统一文件类型定义 +```javascript +// 将 LANGUAGE_MAP 迁移到 constants.js +// 与 FILE_EXTENSIONS 合并 +export const FILE_CATEGORIES = { + CODE: { extensions: ['js', 'ts', /* ... */ }, syntaxHighlight: javascript }, + MARKUP: { extensions: ['html', 'css', /* ... */ ], syntaxHighlight: html }, + // ... +} +``` + +#### 8. 类型安全 +```typescript +// 添加 TypeScript 类型定义 +interface FileExtension { + name: string + category: FileCategory + syntaxHighlight?: Language +} +``` + +--- + +## 五、代码质量指标 + +### 当前状态 +| 指标 | 当前值 | 目标值 | 评级 | +|------|--------|--------|------| +| 单文件最大行数 | 4266 | < 500 | 🔴 | +| 函数平均行数 | ~50 | < 30 | 🟡 | +| 代码重复率 | ~5% | < 3% | 🟡 | +| 调试语句数量 | 58 | 0 (生产) | 🔴 | +| 圈复杂度 | 15+ | < 10 | 🟡 | + +--- + +## 六、检查清单 + +### 前端代码 +- [ ] 移除所有调试日志 +- [ ] 删除重复计算属性 +- [ ] 简化 currentFileExtension +- [ ] 提取 composables +- [ ] 拆分 FileSystem.vue +- [ ] 统一扩展名获取逻辑 +- [ ] 复用 constants.js + +### 后端代码 +- [ ] 简化 handleLocalFileRequest +- [ ] 提取配置到独立文件 +- [ ] 添加单元测试 +- [ ] 统一错误处理 + +--- + +## 七、后续行动 + +1. **立即执行** (1-2 天) + - 移除调试代码 + - 删除重复代码 + - 简化函数逻辑 + +2. **短期计划** (1 周) + - 拆分 FileSystem.vue + - 提取 composables + - 统一工具函数 + +3. **长期优化** (2-4 周) + - TypeScript 迁移 + - 添加单元测试 + - 性能优化 + +--- + +## 八、参考资源 + +- [Vue 3 风格指南](https://vuejs.org/style-guide/) +- [代码整洁之道](https://www.oreilly.com/library/view/clean-code-a/9780136083238/) +- [重构:改善既有代码的设计](https://www.refactoring.com/) diff --git a/docs/code-review-deep-optimization-report.md b/docs/代码审查/code-review-deep-optimization-report.md similarity index 100% rename from docs/code-review-deep-optimization-report.md rename to docs/代码审查/code-review-deep-optimization-report.md diff --git a/docs/code-review-p3-report.md b/docs/代码审查/code-review-p3-report.md similarity index 100% rename from docs/code-review-p3-report.md rename to docs/代码审查/code-review-p3-report.md diff --git a/docs/代码审查/composable-integration-failure-analysis.md b/docs/代码审查/composable-integration-failure-analysis.md new file mode 100644 index 0000000..ddd7c05 --- /dev/null +++ b/docs/代码审查/composable-integration-failure-analysis.md @@ -0,0 +1,508 @@ +# Composable 集成失败根因分析报告 +**日期**: 2025-01-30 +**目标**: 分析为什么 useFileEdit 和 useFilePreview 无法集成到 FileSystem.vue + +--- + +## 执行摘要 + +集成尝试失败的根本原因:**Composables 和本地实现在设计哲学、API 契约、功能范围上存在系统性差异**。 + +- ❌ **useFileEdit**: 不兼容(状态变量不匹配:`isEditMode` vs `isEditableView`) +- ❌ **useFilePreview**: 不兼容(URL 格式、路径处理、ZIP 模式支持差异) +- ✅ **useNavigation**: 兼容(已成功集成) + +--- + +## 一、useFileEdit.js vs FileSystem.vue + +### 1.1 状态变量差异 + +| 功能点 | useFileEdit.js | FileSystem.vue | 兼容性 | +|--------|----------------|----------------|--------| +| **编辑模式开关** | `isEditMode` (简单 ref) | `isEditableView` (复杂 computed) | ❌ 不兼容 | +| **路径来源** | `filePath` (单一) | `selectedFilePath` \| `filePath` (双重) | ❌ 不兼容 | +| **文件修改检测** | 简单比较 | 复杂逻辑(含新建文件) | ❌ 不兼容 | + +### 1.2 致命差异:`canSaveFile` 的条件 + +**useFileEdit.js:87-89** +```javascript +const canSaveFile = computed(() => { + return isEditMode.value && contentChanged.value +}) +``` + +**FileSystem.vue:2997** +```javascript +const canSaveFile = computed(() => isEditableView.value && contentChanged.value) +``` + +**问题**: +- `isEditMode`: 简单的布尔值 ref,来自 localStorage +- `isEditableView`: 复杂的 computed,依赖预览状态 + +```javascript +// FileSystem.vue:2968-2974 +const isEditableView = computed(() => { + return !isImageView.value && + !isVideoView.value && + !isAudioView.value && + !isPdfFile.value && + !isBinaryFile.value +}) +``` + +**影响**: +- 使用 `isEditMode` → 保存按钮可能在图片预览时也显示(错误) +- 使用 `isEditableView` → 保存按钮只在文本编辑时显示(正确) + +### 1.3 致命差异:`isFileModified` 的逻辑 + +**useFileEdit.js:71-74** +```javascript +const isFileModified = computed(() => { + return originalContent.value !== undefined && + originalContent.value !== fileContent.value +}) +``` + +**FileSystem.vue:2977-2988** +```javascript +const isFileModified = computed(() => { + const hasContent = fileContent.value !== '' && fileContent.value.trim() !== '' + const hasModified = selectedFilePath.value && fileContent.value !== originalContent.value + const isNewFile = !selectedFilePath.value && hasContent // ← 新建文件检测 + return isEditableView.value && (hasModified || isNewFile) +}) +``` + +**缺失功能**: +- Composable 版本**不支持新建文件场景** +- FileSystem.vue 版本可以检测到"未选择文件路径但有内容"的新建文件状态 + +### 1.4 依赖图对比 + +**useFileEdit 依赖树**: +``` +canSaveFile + ├─ isEditMode (ref) + └─ contentChanged + ├─ fileContent + └─ originalContent +``` + +**FileSystem.vue 依赖树**: +``` +canSaveFile + ├─ isEditableView (computed) + │ ├─ isImageView + │ ├─ isVideoView + │ ├─ isAudioView + │ ├─ isPdfFile + │ └─ isBinaryFile + └─ contentChanged + ├─ fileContent + └─ originalContent +``` + +**结论**: FileSystem.vue 的依赖更复杂,Composable 过于简化 + +--- + +## 二、useFilePreview.js vs FileSystem.vue + +### 2.1 URL 构建差异(致命) + +**useFilePreview.js:163** +```javascript +const encodedPath = encodeURIComponent(pathToPreview) +previewUrl.value = `${fileServerURL.value}/file?path=${encodedPath}` +``` + +**FileSystem.vue:1503** +```javascript +previewUrl.value = `${fileServerURL.value}/localfs/${normalizeFilePath(pathToPreview, true)}` +``` + +**问题**: +- Composable: `/file?path=xxx` (查询参数格式) +- FileSystem.vue: `/localfs/xxx` (路径格式,需要规范化) + +**不兼容原因**: +- 后端可能只支持其中一种格式 +- `normalizeFilePath()` 可能有特殊处理(如 Windows 路径转换) + +### 2.2 路径参数优先级差异 + +**useFilePreview.js:148** +```javascript +const previewImage = async (targetPath) => { + const pathToPreview = targetPath || filePath.value // 只用 filePath + // ... +} +``` + +**FileSystem.vue:1487** +```javascript +const previewImageLocal = async (targetPath) => { + const pathToPreview = targetPath || selectedFilePath.value || filePath.value // 三级优先级 + // ... +} +``` + +**三级优先级**: +1. `targetPath` (显式传入) +2. `selectedFilePath` (当前选中的文件) +3. `filePath` (当前目录) + +**影响**: +- Composable 在"选中文件但未传参"时会失败 +- FileSystem.vue 可以自动回退到 `selectedFilePath` + +### 2.3 computed 属性功能差异 + +**currentFileName** 对比: + +| 功能 | useFilePreview | FileSystem.vue | 差异 | +|------|----------------|----------------|------| +| **ZIP 模式支持** | ❌ 无 | ✅ 有 | 关键差异 | +| **目录检测** | ❌ 无 | ✅ 有 | UX 增强 | +| **路径截断** | ❌ 无 | ✅ 有 | UX 增强 | +| **错误处理** | ❌ 无 | ✅ try-catch | 健壮性 | + +**FileSystem.vue:1437-1460** (23行,包含 ZIP 逻辑) +```javascript +const currentFileNameDisplay = computed(() => { + if (isBrowsingZip.value && selectedFilePath.value) { + // ZIP 模式:从 zip 内路径中提取文件名 + const parts = selectedFilePath.value.split('/') + return parts[parts.length - 1] || parts[parts.length - 2] || '' + } + if (selectedFilePath.value) { + // 正常模式:如果文件在当前目录,只显示文件名;否则显示完整路径 + try { + if (isFileInCurrentDirectory.value) { + return getFileName(selectedFilePath.value) + } else { + return selectedFilePath.value // 返回完整路径 + } + } catch (error) { + debugWarn('[currentFileName] 计算失败,返回文件名:', error) + return getFileName(selectedFilePath.value) + } + } + return '' +}) +``` + +**useFilePreview.js:122-126** (5行,无特殊逻辑) +```javascript +const currentFileName = computed(() => { + if (!filePath.value) return '' + const parts = filePath.value.split(/[/\\]/) + return parts[parts.length - 1] +}) +``` + +### 2.4 函数命名体系差异 + +| 功能 | useFilePreview | FileSystem.vue | +|------|----------------|----------------| +| 图片预览 | `previewImage` | `previewImageLocal` | +| 视频预览 | `previewVideo` | `previewVideoLocal` | +| 音频预览 | `previewAudio` | `previewAudioLocal` | +| PDF 预览 | `previewPdf` | `previewPdfLocal` | +| HTML 预览 | `previewHtml` | `previewHtmlLocal` | +| Markdown 预览 | `previewMarkdown` | `previewMarkdownLocal` | + +**Local 后缀的意义**: +- 表明这是本地实现,避免与外部库或全局函数冲突 +- 如果替换为 Composable,需要全局重命名模板中的所有调用点(30+ 处) + +--- + +## 三、useNavigation.js vs FileSystem.vue + +### 3.1 集成状态 + +✅ **已成功集成** (FileSystem.vue:605-625) + +```javascript +const { + navHistory, + navIndex, + isNavigating, + canGoBack, + canGoForward, + addToHistory, + pushNav, + goBack, + goForward, + onPathSelect, + onPathEnter, + browseDirectory, +} = useNavigation({ + filePath, + onListDirectory: async (path) => { + filePath.value = path + await listDirectory() + } +}) +``` + +### 3.2 为什么成功? + +1. **清晰的回调接口**: `onListDirectory` 作为回调,连接到本地实现 +2. **状态变量简单**: 只依赖 `filePath`,没有复杂的 computed 依赖 +3. **无 API 假设**: 不涉及 URL 格式、网络请求等 +4. **功能独立**: 导航逻辑不依赖预览、编辑等其他模块 + +### 3.3 集成模式 + +``` +┌─────────────────┐ +│ useNavigation │ +└────────┬────────┘ + │ + │ onListDirectory(path) + ▼ +┌─────────────────┐ +│ FileSystem.vue │ +│ listDirectory()│ +└─────────────────┘ +``` + +这种模式清晰、解耦、易于测试。 + +--- + +## 四、根因总结 + +### 4.1 设计哲学差异 + +| 维度 | Composables | FileSystem.vue | +|------|-------------|----------------| +| **复杂度** | 追求简洁、纯粹 | 追求功能完整 | +| **假设** | 单一路径、标准API | 多路径源、自定义API | +| **范围** | 单一职责 | 全功能 | +| **演进** | 从头设计 | 增量演进(ZIP、新建文件等) | + +### 4.2 API 契议不匹配 + +**Composable 隐式假设**: +```javascript +// 假设 1: URL 格式 +`${fileServerURL}/file?path=${encodedPath}` + +// 假设 2: 路径来源 +const path = filePath.value // 单一来源 + +// 假设 3: 状态变量 +const canSave = isEditMode && changed // 简单布尔值 +``` + +**FileSystem.vue 实际**: +```javascript +// 实际 1: URL 格式 +`${fileServerURL}/localfs/${normalizeFilePath(path, true)}` + +// 实际 2: 路径来源 +const path = targetPath || selectedFilePath || filePath // 三级优先级 + +// 实际 3: 状态变量 +const canSave = isEditableView && changed // 复杂 computed +``` + +### 4.3 功能演进差距 + +**FileSystem.vue 独有功能**: +- ✅ ZIP 文件浏览模式 +- ✅ 新建文件检测 +- ✅ 目录感知显示 +- ✅ 路径规范化 +- ✅ 文件是否在当前目录检测 + +**useFileEdit/useFilePreview 创建时未考虑这些功能** + +--- + +## 五、集成失败的三个层次 + +### 层次 1: 语法层面(易于发现) +``` +❌ ReferenceError: loadDraft is not defined +❌ Identifier 'previewImage' has already been declared +``` + +### 层次 2: 语义层面(运行时错误) +``` +❌ 保存按钮在图片预览时也显示 (isEditMode vs isEditableView) +❌ URL 404 错误 (/file?path= vs /localfs/) +❌ 新建文件无法保存 +``` + +### 层次 3: 设计层面(深层不兼容) +``` +❌ 单一路径模型 vs 多路径源 +❌ 简单布尔值 vs 复杂 computed +❌ 标准API vs 自定义API +❌ 静态功能 vs 增量演进 +``` + +--- + +## 六、解决方案 + +### 方案 A: 保持现状 + 提取工具函数(推荐) + +**理由**: +- 功能完整性优先 +- 避免破坏性重构 +- 渐进式优化 + +**行动**: +1. 保留 `useNavigation` 集成 +2. 删除 `useFileEdit` 和 `useFilePreview`(或作为参考文档) +3. 提取真正的通用工具函数: + ```javascript + // utils/pathHelpers.js + export const splitPath = (path) => path.split(/[/\\]/) + export const getFileName = (path) => { /* ... */ } + export const getParentPath = (path) => { /* ... */ } + + // utils/fileHelpers.js + export const isImageFile = (ext) => FILE_EXTENSIONS.IMAGE.includes(ext) + export const isVideoFile = (ext) => FILE_EXTENSIONS.VIDEO_BROWSER.includes(ext) + ``` + +4. 减少调试日志(65 → 10) + +### 方案 B: 重构 FileSystem.vue(激进) + +**风险**: 高 +**时间**: 2-3周 +**收益**: 长期可维护性 + +**步骤**: +1. 统一状态管理(单一 `filePath` vs `selectedFilePath`) +2. 标准化 API(统一 URL 格式) +3. 组件化拆分(子组件) +4. 然后重新集成 Composables + +### 方案 C: 创建轻量级 Composables(折中) + +```javascript +// useFileEditMinimal.js +export function useFileEditMinimal({ fileContent, originalContent }) { + const contentChanged = computed(() => + fileContent.value !== '' && + fileContent.value !== originalContent.value + ) + + return { contentChanged } +} + +// FileSystem.vue +const { contentChanged } = useFileEditMinimal({ fileContent, originalContent }) +const canSaveFile = computed(() => isEditableView.value && contentChanged.value) +``` + +--- + +## 七、检查清单 + +### 立即行动(本周) + +- [x] 分析集成失败根因 +- [ ] 修复 `loadDraft is not defined` 运行时错误 +- [ ] 决定方案 A/B/C +- [ ] 执行决定 + +### 短期优化(2周) + +- [ ] 提取路径工具函数 +- [ ] 提取文件类型判断函数 +- [ ] 统一 localStorage 键名 +- [ ] 减少调试日志 + +### 长期重构(1个月) + +- [ ] 组件化拆分(子组件) +- [ ] 状态管理优化 +- [ ] TypeScript 迁移 +- [ ] 单元测试覆盖 + +--- + +## 八、关键发现 + +### 发现 1: Composables 是"理想版本" + +Composables 基于**理想假设**设计: +- 单一路径来源 +- 标准 API +- 简单状态 +- 纯净功能 + +但 FileSystem.vue 是**现实版本**: +- 多路径源(历史包袱) +- 自定义 API(性能优化) +- 复杂状态(功能完整) +- 增量演进(业务需求) + +### 发现 2: 命名体系反映演进历史 + +所有预览函数都有 `Local` 后缀: +```javascript +previewImageLocal // 表明"本地实现" +previewVideoLocal // 避免"全局冲突" +``` + +这说明开发者在添加这些函数时,**已经意识到可能存在外部冲突**,因此添加后缀。 + +如果强行使用无后缀的 Composable 版本,会破坏这种防御性设计。 + +### 发现 3: useNavigation 成功的启示 + +useNavigation 成功的关键: +1. **清晰的边界**: 只负责导航历史 +2. **回调接口**: 不直接操作文件系统 +3. **状态简单**: 只依赖 `filePath` +4. **无副作用**: 不涉及 UI 状态 + +**教训**: 如果要提取 Composables,应该遵循同样的原则。 + +--- + +## 九、最终建议 + +### 推荐:方案 A - 提取工具函数 + +**原因**: +1. **风险最低**: 不破坏现有功能 +2. **收益明确**: 减少代码重复(路径处理、文件类型判断) +3. **时间可控**: 1周内完成 +4. **渐进式**: 为未来重构铺路 + +**具体行动**: +```javascript +// 第1步:提取工具函数 +// utils/pathHelpers.js +// utils/fileTypeHelpers.js + +// 第2步:替换重复代码 +// path.split(/[/\\/]/) → splitPath(path) + +// 第3步:删除未使用的 Composables +// rm useFileEdit.js useFilePreview.js + +// 第4步:减少调试日志 +// 保留 10 个关键日志,删除 55 个 +``` + +**预期结果**: +- 代码减少 ~200 行 +- DRY 评分改善 5% +- 维护成本降低 +- 为长期重构打好基础 diff --git a/docs/代码审查/refactoring-review-2026-01-30.md b/docs/代码审查/refactoring-review-2026-01-30.md new file mode 100644 index 0000000..3085d11 --- /dev/null +++ b/docs/代码审查/refactoring-review-2026-01-30.md @@ -0,0 +1,628 @@ +# 重构缺漏检查报告 +**日期**: 2025-01-30 +**审查范围**: FileSystem.vue + 3个Composables + +--- + +## 一、严重问题 🔴 + +### 1. **重构目标未达成 - FileSystem.vue 仍然过大** + +| 文件 | 当前行数 | 目标行数 | 差距 | 状态 | +|------|----------|----------|------|------| +| FileSystem.vue | 4047 | < 500 | +3547 | 🔴 | +| useNavigation.js | 273 | - | - | ✅ | +| useFileEdit.js | 369 | - | - | ✅ | +| useFilePreview.js | 611 | - | - | ✅ | +| **总计** | 5300 | < 1500 | +3800 | 🔴 | + +**问题**: +- Composables已创建(1253行),但**未真正集成** +- FileSystem.vue仍然包含所有原始逻辑(4047行) +- **代码总量增加**:从4241行 → 5300行(+25%) + +**根本原因**: +- 之前因20+个重复函数声明错误,撤销了composable集成 +- 保留了所有本地实现,导致双重代码存在 + +--- + +### 2. **重复的计算属性(DRY违反)** + +#### 问题1: `isFileModified` 重复定义 + +**FileSystem.vue:2977-2988** +```javascript +const isFileModified = computed(() => { + const hasContent = fileContent.value !== '' && fileContent.value.trim() !== '' + const hasModified = selectedFilePath.value && fileContent.value !== originalContent.value + const isNewFile = !selectedFilePath.value && hasContent + return isEditableView.value && (hasModified || isNewFile) +}) +``` + +**useFileEdit.js:71-74** (未使用) +```javascript +const isFileModified = computed(() => { + return originalContent.value !== undefined && + originalContent.value !== fileContent.value +}) +``` + +**差异**:FileSystem.vue版本包含"新建文件"逻辑,useFileEdit版本更简单 + +--- + +#### 问题2: 文件名计算属性重复 + +**FileSystem.vue:1437-1460** +```javascript +const currentFileNameDisplay = computed(() => { + if (!selectedFilePath.value && !filePath.value) return '无文件' + + const path = selectedFilePath.value || filePath.value + const parts = path.split(/[/\\]/) + const fileName = parts[parts.length - 1] + + if (fileName.length > 30) { + return fileName.substring(0, 15) + '...' + fileName.substring(fileName.length - 10) + } + return fileName +}) +``` + +**useFilePreview.js:122-126** (未使用) +```javascript +const currentFileName = computed(() => { + if (!filePath.value) return '' + const parts = filePath.value.split(/[/\\]/) + return parts[parts.length - 1] +}) +``` + +**重复**:都做路径分割取文件名,但Display版本有截断逻辑 + +--- + +#### 问题3: 文件路径计算属性重复 + +**FileSystem.vue:1462-1485** +```javascript +const currentFileFullPathDisplay = computed(() => { + if (isBrowsingZip.value) { + return `ZIP: ${currentZipPath.value} → ${currentZipDirectory.value || '/'}` + } + + if (!selectedFilePath.value) { + return filePath.value || '未选择文件' + } + + const path = selectedFilePath.value + if (path.length > 50) { + return '...' + path.substring(path.length - 50) + } + return path +}) +``` + +**useFilePreview.js:131** (未使用) +```javascript +const currentFileFullPath = computed(() => filePath.value || '') +``` + +**重复**:获取文件路径,但Display版本有ZIP模式和截断逻辑 + +--- + +#### 问题4: 内容修改检测重复 + +**FileSystem.vue:2991-2994** +```javascript +const contentChanged = computed(() => { + return fileContent.value !== '' && + fileContent.value !== originalContent.value +}) +``` + +**useFileEdit.js:79-82** (未使用) +```javascript +const contentChanged = computed(() => { + return fileContent.value !== '' && + fileContent.value !== originalContent.value +}) +``` + +**完全相同**:100%重复代码 + +--- + +#### 问题5: 保存/重置按钮状态重复 + +**FileSystem.vue:2997-3004** +```javascript +const canSaveFile = computed(() => isEditableView.value && contentChanged.value) +const canResetContent = computed(() => + isEditableView.value && + contentChanged.value && + originalContent.value !== undefined +) +``` + +**useFileEdit.js:87-98** (未使用) +```javascript +const canSaveFile = computed(() => { + return isEditMode.value && contentChanged.value +}) + +const canResetContent = computed(() => { + return isEditMode.value && + contentChanged.value && + originalContent.value !== undefined +}) +``` + +**差异**:FileSystem.vue用`isEditableView`,useFileEdit用`isEditMode` + +--- + +### 3. **调试日志仍然过多 - 65个** + +```bash +$ grep -c "debug(Log|Warn|Error|Info)" web/src/components/FileSystem.vue +65 +``` + +**分布**: +- `debugLog`: ~45处 +- `debugWarn`: ~12处 +- `debugError`: ~8处 + +**问题**: +- 已从raw console替换为debugLog,但**数量仍然过多** +- 过度防御性编程,每个分支都记录日志 +- 影响代码可读性和运行时性能 + +--- + +## 二、中等问题 🟡 + +### 4. **currentFileExtension 逻辑嵌套过多** + +**FileSystem.vue:2941-2960** (19行) +```javascript +const currentFileExtension = computed(() => { + const path = selectedFilePath.value || filePath.value + if (!path) return '' + + const fileName = path.split(/[/\\]/).pop()?.toLowerCase() || '' + const specialFiles = { + 'dockerfile': 'dockerfile', + 'containerfile': 'dockerfile', + 'makefile': 'makefile', + 'cmakelists.txt': 'cmake', + '.gitignore': 'gitignore', + '.env': 'properties', + } + + if (specialFiles[fileName]) return specialFiles[fileName] + return getExt(path) +}) +``` + +**可以改进为**(使用fileHelpers.js中的函数): +```javascript +const currentFileExtension = computed(() => { + const path = selectedFilePath.value || filePath.value + return getExtensionForHighlight(path) // 复用现有工具函数 +}) +``` + +--- + +### 5. **函数命名不一致** + +| FileSystem.vue | useFilePreview.js | 用途 | +|----------------|-------------------|------| +| `currentFileNameDisplay` | `currentFileName` | 获取文件名 | +| `currentFileFullPathDisplay` | `currentFileFullPath` | 获取完整路径 | +| `currentImageDimensionsLocal` | `currentImageDimensions` | 图片尺寸 | + +**问题**: +- 有的带`Display`后缀,有的不带 +- 有的带`Local`后缀,含义不明 +- 命名不一致导致维护困难 + +--- + +### 6. **Go代码配置函数重复** + +**internal/filesystem/config.go:256-295** +```go +func getAllowedExtensions() map[string]bool { + return map[string]bool{ + ".jpg": true, ".jpeg": true, ".png": true, + // ... 30+ 个硬编码扩展名 + } +} +``` + +**web/src/utils/constants.js:27-73** (重复定义) +```javascript +export const FILE_EXTENSIONS = { + IMAGE: ['jpg', 'jpeg', 'png', /* ... */], + VIDEO_BROWSER: ['mp4', 'webm', /* ... */], + // ... 类似的30+个扩展名 +} +``` + +**问题**:前后端用不同格式重复定义相同的数据 + +**建议**:后端从配置文件加载,或生成JSON供前端使用 + +--- + +## 三、代码规范问题 ⚠️ + +### 7. **路径分隔符正则重复** + +**出现次数**: 15+ + +```javascript +// FileSystem.vue 多处 +path.split(/[/\\]/) // 行 719, 798, 819, 833, 845, 2946, ... + +// useFilePreview.js:124 +path.split(/[/\\/]/) + +// useNavigation.js:304 +const parts = path.split(/[/\\]/) +``` + +**建议**:提取为共享常量 +```javascript +// utils/pathConstants.js +export const PATH_SEPARATOR_REGEX = /[/\\]/ +export const splitPath = (path) => path.split(PATH_SEPARATOR_REGEX) +``` + +--- + +### 8. **文件类型判断分散** + +**FileSystem.vue:857-869** +```javascript +const previewableTypes = [ + ...FILE_EXTENSIONS.IMAGE, + ...FILE_EXTENSIONS.VIDEO_BROWSER, + ...FILE_EXTENSIONS.AUDIO, + 'pdf', 'html', 'htm', 'md', 'markdown' +] + +const knownBinaryTypes = [ + 'exe', 'dll', 'so', 'bin', + 'zip', 'rar', '7z', 'tar', 'gz', 'iso', 'img', 'dmg', + 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx' +] +``` + +**问题**: +- 内联定义在函数内部 +- 应该定义在constants.js中复用 + +--- + +### 9. **localStorage键名分散** + +**多处重复定义**: +- FileSystem.vue: 使用`STORAGE_KEYS.FILESYSTEM.*` +- useFileEdit.js: 直接定义`DRAFT_STORAGE_KEY` +- useNavigation.js: 直接定义`STORAGE_KEY_PATH_HISTORY` + +**应该统一使用**:`STORAGE_KEYS`常量对象 + +--- + +## 四、DRY原则违反统计 + +### 重复代码统计 + +| 类型 | 重复次数 | 总行数 | 浪费 | +|------|----------|--------|------| +| 计算属性 | 5组 | ~80行 | 40行 | +| 路径分割正则 | 15+次 | ~15行 | 14行 | +| 文件类型判断 | 8+次 | ~50行 | 40行 | +| localStorage键 | 6+处 | ~12行 | 8行 | +| **总计** | **34+处** | **~157行** | **102行** | + +--- + +## 五、优化建议 + +### 优先级1: 立即修复 🔴 + +#### 1.1 移除未使用的Composables +```bash +# 由于composables未被实际使用,应该删除或文档化 +rm web/src/composables/useNavigation.js +rm web/src/composables/useFileEdit.js +rm web/src/composables/useFilePreview.js +``` + +**理由**:如果不用,就不应该存在,避免混淆 + +--- + +#### 1.2 删除重复计算属性 + +**FileSystem.vue - 保留更完整的版本,删除useFileEdit/useFilePreview中的**: + +```javascript +// 保留 FileSystem.vue:1437 - currentFileNameDisplay (有截断逻辑) +// 保留 FileSystem.vue:1462 - currentFileFullPathDisplay (有ZIP模式) +// 保留 FileSystem.vue:2977 - isFileModified (有新建文件逻辑) +// 删除 useFileEdit.js:71, useFilePreview.js:122-126 等重复定义 +``` + +**或者相反**:如果决定使用composables,则删除FileSystem.vue中的重复定义 + +--- + +#### 1.3 大幅减少调试日志 + +**策略A: 环境变量控制**(已部分实现) +```javascript +// utils/debugLog.js +const ENABLE_DEBUG = import.meta.env.DEV + +export const debugLog = ENABLE_DEBUG ? console.log : () => {} +export const debugWarn = ENABLE_DEBUG ? console.warn : () => {} +export const debugError = console.error // 始终保留错误日志 +``` + +**策略B: 删除非关键日志**(推荐) +```javascript +// 删除这些类型的日志: +debugLog('[readFile] 开始读取文件') // 显而易见的操作 +debugLog('[handleKeyDown] F2 pressed') // 用户操作 +debugLog('[startResizeHorizontal] 开始拖拽') // UI交互 + +// 保留这些: +debugError('[readFile] 读取失败:', error) // 错误 +debugWarn('[loadCommonPaths] Wails API未就绪') // 降级场景 +``` + +**目标**: 从65个 → < 10个(只保留错误和关键警告) + +--- + +### 优先级2: 短期优化 🟡 + +#### 2.1 提取共享工具函数 + +**创建 web/src/utils/pathHelpers.js**: +```javascript +export const PATH_SEPARATOR_REGEX = /[/\\]/ + +export const splitPath = (path) => path.split(PATH_SEPARATOR_REGEX) + +export const getFileName = (path) => { + if (!path) return '' + const parts = splitPath(path) + return parts[parts.length - 1] || path +} + +export const getParentPath = (path) => { + if (!path) return '' + const lastSep = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')) + return lastSep > 0 ? path.substring(0, lastSep) : path +} +``` + +**替换所有** `path.split(/[/\\]/)` 为 `splitPath(path)` + +--- + +#### 2.2 统一文件类型常量 + +**创建 web/src/utils/fileTypeCategories.js**: +```javascript +import { FILE_EXTENSIONS } from './constants' + +export const PREVIEWABLE_TYPES = [ + ...FILE_EXTENSIONS.IMAGE, + ...FILE_EXTENSIONS.VIDEO_BROWSER, + ...FILE_EXTENSIONS.AUDIO, + 'pdf', 'html', 'htm', 'md', 'markdown' +] + +export const KNOWN_BINARY_TYPES = [ + 'exe', 'dll', 'so', 'bin', + 'zip', 'rar', '7z', 'tar', 'gz', 'iso', 'img', 'dmg', + 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx' +] + +export const TEXT_EDITABLE_TYPES = [ + ...FILE_EXTENSIONS.TEXT, + ...FILE_EXTENSIONS.CODE +] +``` + +**替换所有内联定义** + +--- + +#### 2.3 统一localStorage键名 + +**只在 constants.js 中定义一次**: +```javascript +export const STORAGE_KEYS = { + FILESYSTEM: { + PATH_HISTORY: 'app-filesystem-path-history', + EDIT_MODE: 'app-filesystem-edit-mode', + PANEL_WIDTH: 'app-filesystem-panel-width', + DRAFT_CONTENT: 'filesystem-draft-content', + DRAFT_TIME: 'filesystem-draft-time', + FAVORITE_FILES: 'filesystem-favorite-files', + } +} + +// 删除所有其他文件中的重复定义 +``` + +--- + +### 优先级3: 长期重构 🔵 + +#### 3.1 真正拆分FileSystem.vue + +**目标**: 从4047行 → < 500行 + +**策略**: +1. **提取子组件** (~1500行) + - `FileListPanel.vue` (文件列表, ~300行) + - `CodeEditorPanel.vue` (编辑器面板, ~400行) + - `PreviewPanel.vue` (预览面板, ~300行) + - `FavoriteSidebar.vue` (收藏夹侧边栏, ~200行) + - `Toolbar.vue` (顶部工具栏, ~150行) + - `ContextMenu.vue` (右键菜单, ~150行) + +2. **提取composables** (~1000行) + - `useFileSystem.js` (核心文件系统操作, ~300行) + - `useFileEditor.js` (编辑器逻辑, ~200行) + - `useFilePreview.js` (预览逻辑, ~250行) + - `useFavoriteFiles.js` (收藏夹管理, ~150行) + - `useKeyboardShortcuts.js` (快捷键, ~100行) + +3. **主组件保留** (~500行) + - 布局和状态协调 + - 子组件通信 + - 生命周期管理 + +**时间估算**: 2-3周 + +--- + +#### 3.2 TypeScript迁移 + +**目标**: 添加类型安全,减少运行时错误 + +```typescript +// types/file.ts +export interface FileItem { + path: string + name: string + is_dir: boolean + size: number + modified: string +} + +export interface PreviewState { + isImageView: boolean + isVideoView: boolean + isAudioView: boolean + isPdfFile: boolean + isHtmlFile: boolean + isMarkdownFile: boolean + isBinaryFile: boolean +} +``` + +--- + +#### 3.3 统一前后端文件类型定义 + +**方案A: 后端生成JSON** +```go +// internal/filesystem/export_types.go +func ExportFileTypes() string { + types := map[string][]string{ + "image": getAllowedExtensions(), + "binary": getForbiddenExtensions(), + } + json, _ := json.Marshal(types) + return string(json) +} +``` + +**方案B: 独立配置文件** +```yaml +# config/file_types.yaml +image: + - jpg + - jpeg + - png +binary: + - exe + - dll +``` + +前后端都从同一配置读取 + +--- + +## 六、检查清单 + +### 立即执行(本周) + +- [ ] **决定**: 删除还是使用composables +- [ ] **删除重复**: 移除5组重复计算属性(102行) +- [ ] **减少日志**: 从65个debugLog → < 10个 +- [ ] **提取工具**: 创建pathHelpers.js +- [ ] **统一常量**: 合并文件类型定义 +- [ ] **统一键名**: 只使用STORAGE_KEYS + +### 短期计划(2周) + +- [ ] **提取子组件**: FileListPanel, Toolbar, ContextMenu +- [ ] **提效composables**: 真正集成useFileSystem, useFilePreview +- [ ] **优化函数**: 简化currentFileExtension逻辑 +- [ ] **命名统一**: 统一Display/Local后缀规则 + +### 长期优化(1个月) + +- [ ] **组件化**: 完成所有子组件提取 +- [ ] **TypeScript**: 添加类型定义 +- [ ] **前后端统一**: 文件类型配置共享 +- [ ] **单元测试**: 覆盖核心逻辑 + +--- + +## 七、代码质量指标(更新后) + +| 指标 | 当前值 | 目标值 | 评级 | +|------|--------|--------|------| +| 单文件最大行数 | 4047 | < 500 | 🔴 | +| 函数平均行数 | ~50 | < 30 | 🟡 | +| 代码重复率 | ~8% | < 3% | 🔴 | +| 调试语句数量 | 65 | < 10 | 🔴 | +| 圈复杂度 | 15+ | < 10 | 🟡 | +| 未使用代码 | 1253行 | 0 | 🔴 | + +--- + +## 八、总结 + +### 关键发现 + +1. **重构未完成**: Composables已创建但未使用,反而增加了总代码量 +2. **重复代码严重**: 5组计算属性重复,102行浪费 +3. **过度防御性编程**: 65个调试日志,远超必要数量 +4. **命名不一致**: Display/Local后缀混乱 + +### 下一步行动 + +**推荐方案A: 激进重构** +- 删除3个未使用的composables +- 立即开始拆分子组件 +- 1个月内完成组件化 + +**推荐方案B: 渐进优化(更稳妥)** +- 先清理重复代码和日志 +- 提取共享工具函数 +- 逐步拆分子组件 + +### 风险提示 + +⚠️ **当前状态**: 代码库处于"半重构"状态,既有旧实现又有新参考,容易造成混淆 + +**建议**: 尽快决定方向(彻底重构 vs 回滚到重构前),避免技术债务累积 diff --git a/docs/架构改进完成总结.md b/docs/架构改进完成总结.md deleted file mode 100644 index 5aa016f..0000000 --- a/docs/架构改进完成总结.md +++ /dev/null @@ -1,305 +0,0 @@ -# 架构改进完成总结 - -## 📋 改进概览 - -### 核心改进 -- ✅ **事件驱动架构**:使用 `useEventBus` 实现组件间解耦通信 -- ✅ **单例 Store 模式**:使用 `useStructureStore` 实现全局状态管理 -- ✅ **响应式优化**:直接暴露 `ref`,确保响应式链完整 -- ✅ **代码清理**:移除所有调试代码和冗余逻辑 - -## 📁 文件结构 - -### 新增文件 -``` -web/src/views/db-cli/composables/ -├── useEventBus.ts # 事件总线(核心) -├── useStructureStore.ts # 表结构 Store(单例) -└── useStructureStoreLegacy.ts # 旧版本备份 -``` - -### 修改文件 -``` -web/src/views/db-cli/ -├── index.vue # 使用新 Store -└── components/ - └── ResultPanel.vue # 清理调试代码 -``` - -## 🎯 架构对比 - -### 旧架构问题 -```typescript -// ❌ 问题1:状态分散,每个组件实例独立 -const structureState = useStructureState() -const { structureData, loadStructure } = structureState - -// ❌ 问题2:响应式传递复杂,容易丢失 -const computedStructureData = computed(() => structureState.structureData.value) - - -// ❌ 问题3:调试困难,不知道数据在哪里丢失 -console.log('structureData:', structureData.value) -``` - -### 新架构优势 -```typescript -// ✅ 优点1:单例 Store,全局共享状态 -const structureStore = useStructureStore() - -// ✅ 优点2:直接访问 ref,响应式完整 -const structureData = computed(() => structureStore.data.value) - - -// ✅ 优点3:事件可追踪,调试友好 -// Store 内部自动发出事件,可通过事件总线监听 -eventBus.on('structure:data', ({ data, info }) => { - console.log('数据更新:', data) -}) -``` - -## 🔧 核心实现 - -### 1. 事件总线 (`useEventBus.ts`) - -```typescript -// 类型安全的事件定义 -interface DbCliEvents { - 'structure:loading': { loading: boolean } - 'structure:data': { data: any; info: StructureInfo } - 'structure:error': { error: string } - 'structure:clear': {} -} - -// 使用 -const eventBus = useEventBus() -eventBus.on('structure:data', ({ data, info }) => { - // 处理数据更新 -}) -eventBus.emit('structure:loading', { loading: true }) -``` - -**特性:** -- 类型安全:TypeScript 完整类型支持 -- 自动日志:所有事件触发都有日志 -- 错误处理:事件处理器异常不会影响其他监听器 - -### 2. 单例 Store (`useStructureStore.ts`) - -```typescript -class StructureStore { - // 直接暴露 ref,确保响应式 - public readonly loading = ref(false) - public readonly error = ref('') - public readonly data = ref(null) - public readonly info = ref(null) - - // 自动事件通知 - setData(data: any, info: StructureInfo): void { - this.data.value = data - this.info.value = info - this.eventBus.emit('structure:data', { data, info }) - } - - async loadStructure(...): Promise { - // 业务逻辑 + 状态管理 + 事件通知 - } -} - -// 单例模式 -export function useStructureStore(): StructureStore { - if (!structureStoreInstance) { - structureStoreInstance = new StructureStore() - } - return structureStoreInstance -} -``` - -**特性:** -- 单例模式:全局唯一实例,状态不会丢失 -- 自动事件:状态变化自动发出事件 -- 完整日志:所有状态变化都有日志追踪 - -### 3. 组件集成 - -```typescript -// index.vue -const structureStore = useStructureStore() - -// 使用 computed 包装确保类型安全 -const structureLoading = computed(() => structureStore.loading.value) -const structureError = computed(() => structureStore.error.value) -const structureData = computed(() => structureStore.data.value) -const structureInfo = computed(() => structureStore.info.value) - -// 模板中使用 - -``` - -## 📊 改进效果 - -| 指标 | 改进前 | 改进后 | 提升 | -|------|--------|--------|------| -| 状态丢失问题 | ❌ 经常出现 | ✅ 已解决 | 100% | -| 响应式传递 | ⚠️ 复杂,易出错 | ✅ 简洁可靠 | 显著 | -| 调试难度 | ❌ 困难 | ✅ 事件流清晰 | 显著 | -| 代码行数 | 713行 | ~600行 | -15% | -| 类型安全 | ⚠️ 部分 | ✅ 完整 | 100% | - -## 🚀 使用指南 - -### 基本使用 - -```typescript -// 1. 获取 Store -const structureStore = useStructureStore() - -// 2. 访问状态(响应式) -const loading = computed(() => structureStore.loading.value) -const data = computed(() => structureStore.data.value) - -// 3. 调用方法 -await structureStore.loadStructure( - connectionId, - database, - tableName, - dbType, - nodeType -) - -// 4. 监听事件(可选) -const eventBus = useEventBus() -eventBus.on('structure:data', ({ data, info }) => { - console.log('数据已更新:', data) -}) -``` - -### 事件监听 - -```typescript -import { useEventBus } from './composables/useEventBus' - -const eventBus = useEventBus() - -// 监听表结构加载 -eventBus.on('structure:loading', ({ loading }) => { - if (loading) { - console.log('开始加载表结构...') - } -}) - -// 监听数据更新 -eventBus.on('structure:data', ({ data, info }) => { - console.log('表结构数据:', data) - console.log('表信息:', info) -}) - -// 监听错误 -eventBus.on('structure:error', ({ error }) => { - console.error('加载失败:', error) -}) -``` - -## 🔍 调试支持 - -### 日志追踪 - -所有状态变化和事件触发都有日志: - -``` -🏪 Store.setLoading: true -📢 事件触发 [structure:loading]: { loading: true } -🏪 Store.loadStructure 开始: { connectionId: 6, database: 'flux_pro', ... } -🏪 表结构加载成功: { ... } -🏪 Store.setData: { data: {...}, info: {...} } -📢 事件触发 [structure:data]: { data: {...}, info: {...} } -``` - -### 事件流追踪 - -通过事件总线可以追踪完整的数据流: - -```typescript -// 在开发模式下,可以在控制台看到所有事件 -📢 事件触发 [structure:loading]: { loading: true } -📢 事件触发 [structure:data]: { data: {...}, info: {...} } -📢 事件触发 [structure:error]: { error: "..." } -``` - -## ✅ 测试清单 - -- [x] 表结构加载正常 -- [x] 状态响应式正确 -- [x] 事件触发正常 -- [x] 错误处理正确 -- [x] 类型检查通过 -- [x] 构建通过 -- [x] 调试代码已清理 - -## 📝 后续优化建议 - -### 1. 状态持久化 -```typescript -// 可以添加 localStorage 持久化 -class StructureStore { - saveToLocalStorage() { - localStorage.setItem('structure:info', JSON.stringify(this.info.value)) - } - - loadFromLocalStorage() { - const saved = localStorage.getItem('structure:info') - if (saved) { - this.info.value = JSON.parse(saved) - } - } -} -``` - -### 2. 状态回滚 -```typescript -// 添加状态历史记录 -class StructureStore { - private history: Array<{ data: any; info: StructureInfo }> = [] - - saveSnapshot() { - this.history.push({ data: this.data.value, info: this.info.value! }) - } - - rollback() { - const snapshot = this.history.pop() - if (snapshot) { - this.setData(snapshot.data, snapshot.info) - } - } -} -``` - -### 3. 扩展到其他模块 -- SQL 执行结果 Store -- 消息日志 Store -- 连接管理 Store - -## 🎓 最佳实践 - -1. **使用 Store 而非 Composable 实例**:单例模式确保状态一致性 -2. **通过事件监听状态变化**:而非直接 watch Store 状态 -3. **保持 Store 方法原子性**:一个方法只做一件事 -4. **使用类型安全的事件**:充分利用 TypeScript -5. **保留架构层日志**:便于生产环境问题追踪 - -## 📚 相关文档 - -- [架构改进方案](./架构改进方案-状态管理优化.md) -- [迁移指南](../web/src/views/db-cli/composables/MIGRATION.md) -- [事件总线 API](../web/src/views/db-cli/composables/useEventBus.ts) -- [Store API](../web/src/views/db-cli/composables/useStructureStore.ts) - ---- - -**完成时间:** 2026-01-03 -**架构版本:** v2.0 (事件驱动架构) diff --git a/docs/架构改进方案-状态管理优化.md b/docs/架构改进方案-状态管理优化.md deleted file mode 100644 index 431f1cd..0000000 --- a/docs/架构改进方案-状态管理优化.md +++ /dev/null @@ -1,485 +0,0 @@ -# 架构改进方案:状态管理优化 - -## 问题分析 - -当前遇到的问题属于"响应式状态同步灾难",主要问题: - -1. **状态分散**:多个 Composables 各自管理状态,难以追踪数据流 -2. **响应式失效**:computed/watch 在复杂场景下失效,难以调试 -3. **数据传递复杂**:props/computed/provide 多层传递,容易丢失 -4. **缺乏状态快照**:无法回溯状态变化历史 -5. **调试困难**:大量 console.log 散布在代码中,难以系统化 - -## 改进方案 - -### 1. 引入 Pinia 统一状态管理 - -#### 1.1 安装 Pinia - -```bash -npm install pinia -``` - -#### 1.2 创建 Store 结构 - -``` -stores/ - ├── db-cli/ - │ ├── index.ts # 主 store - │ ├── connection.ts # 连接状态 - │ ├── structure.ts # 表结构状态 - │ ├── result.ts # 查询结果状态 - │ ├── editor.ts # 编辑器状态 - │ └── message.ts # 消息日志状态 - └── devtools.ts # 开发工具(状态快照/回放) -``` - -#### 1.3 核心 Store 设计 - -**stores/db-cli/structure.ts** - 表结构状态管理 - -```typescript -import { defineStore } from 'pinia' -import { ref, computed } from 'vue' - -export interface StructureInfo { - connectionId: number - database: string - tableName: string - dbType: 'mysql' | 'mongo' | 'redis' - nodeType: string -} - -export interface StructureData { - type: string - columns?: any[] - database?: string - table?: string - // ... 其他字段 -} - -export const useStructureStore = defineStore('structure', () => { - // 状态定义 - const loading = ref(false) - const error = ref(null) - const data = ref(null) - const info = ref(null) - - // 计算属性(自动响应式) - const hasData = computed(() => data.value !== null && info.value !== null) - const isReady = computed(() => !loading.value && hasData.value) - - // Actions(统一的数据变更入口) - async function loadStructure(params: { - connectionId: number - database: string - tableName: string - dbType: 'mysql' | 'mongo' | 'redis' - nodeType: string - }) { - // 防止重复加载 - if (loading.value) { - console.warn('结构正在加载中,跳过重复请求') - return - } - - try { - loading.value = true - error.value = null - - // 验证参数 - if (params.nodeType === 'connection' || params.nodeType === 'database') { - info.value = { - ...params, - tableName: '' - } - data.value = null - return - } - - if (!params.tableName) { - info.value = { - ...params, - tableName: '' - } - data.value = null - return - } - - // 调用后端 - if (!window.go?.main?.App?.GetTableStructure) { - throw new Error('Go 后端未就绪') - } - - const result = await window.go.main.App.GetTableStructure( - params.connectionId, - params.database, - params.tableName - ) - - // 原子性更新(确保数据一致性) - data.value = result - info.value = params - - // 状态变更日志(开发环境) - if (import.meta.env.DEV) { - console.log('[StructureStore] 数据加载成功', { info: params, data: result }) - } - } catch (err) { - const errorMessage = err instanceof Error ? err.message : '加载表结构失败' - error.value = errorMessage - data.value = null - info.value = null - - if (import.meta.env.DEV) { - console.error('[StructureStore] 加载失败', err) - } - } finally { - loading.value = false - } - } - - function clear() { - data.value = null - info.value = null - error.value = null - } - - function reset() { - loading.value = false - error.value = null - data.value = null - info.value = null - } - - return { - // 状态 - loading, - error, - data, - info, - // 计算属性 - hasData, - isReady, - // 方法 - loadStructure, - clear, - reset - } -}) -``` - -**stores/db-cli/index.ts** - 主 Store - -```typescript -import { defineStore } from 'pinia' -import { useStructureStore } from './structure' -import { useConnectionStore } from './connection' -// ... 其他 stores - -// 组合 Store,提供统一访问入口 -export const useDbCliStore = () => { - return { - structure: useStructureStore(), - connection: useConnectionStore(), - // ... 其他 stores - } -} -``` - -### 2. 组件中使用 Store - -**views/db-cli/index.vue** - -```typescript - - - -``` - -### 3. 状态调试工具 - -**stores/devtools.ts** - 开发工具 - -```typescript -import { watch } from 'vue' - -/** - * 状态变更追踪器(仅开发环境) - */ -export function setupStateDebugger() { - if (!import.meta.env.DEV) return - - // 追踪所有 store 的状态变更 - const stateHistory: Array<{ - timestamp: number - store: string - action: string - oldValue: any - newValue: any - }> = [] - - return { - log(store: string, action: string, oldValue: any, newValue: any) { - stateHistory.push({ - timestamp: Date.now(), - store, - action, - oldValue: JSON.parse(JSON.stringify(oldValue)), - newValue: JSON.parse(JSON.stringify(newValue)) - }) - - console.group(`[${store}] ${action}`) - console.log('旧值:', oldValue) - console.log('新值:', newValue) - console.log('历史记录:', stateHistory.slice(-10)) - console.groupEnd() - }, - - getHistory() { - return stateHistory - }, - - clearHistory() { - stateHistory.length = 0 - } - } -} -``` - -### 4. 类型安全增强 - -**types/db-cli.ts** - -```typescript -// 统一类型定义 -export type DbType = 'mysql' | 'mongo' | 'redis' -export type NodeType = 'connection' | 'database' | 'table' | 'collection' | 'key' - -export interface ConnectionInfo { - id: number - name: string - type: DbType - host: string - port: number - database?: string -} - -export interface StructureInfo { - connectionId: number - database: string - tableName: string - dbType: DbType - nodeType: NodeType -} - -// 严格类型检查 -export function assertStructureInfo(info: unknown): asserts info is StructureInfo { - if (!info || typeof info !== 'object') { - throw new Error('Invalid StructureInfo') - } - // ... 类型检查逻辑 -} -``` - -### 5. 状态持久化策略 - -```typescript -// stores/db-cli/structure.ts -import { defineStore } from 'pinia' -import { useStorage } from '@vueuse/core' - -export const useStructureStore = defineStore('structure', () => { - // 使用 localStorage 持久化(可选) - const lastStructureInfo = useStorage( - 'db-cli-last-structure-info', - null - ) - - // 恢复上次查看的结构 - function restoreLastStructure() { - if (lastStructureInfo.value) { - loadStructure(lastStructureInfo.value) - } - } - - // 在 loadStructure 中保存 - async function loadStructure(params: StructureInfo) { - // ... 加载逻辑 - info.value = params - lastStructureInfo.value = params // 自动保存到 localStorage - } - - return { /* ... */ } -}) -``` - -### 6. 错误边界和恢复机制 - -```typescript -// stores/db-cli/structure.ts -export const useStructureStore = defineStore('structure', () => { - const retryCount = ref(0) - const maxRetries = 3 - - async function loadStructure(params: StructureInfo, retry = 0) { - try { - // ... 加载逻辑 - retryCount.value = 0 // 成功后重置 - } catch (err) { - if (retry < maxRetries) { - console.warn(`[StructureStore] 重试加载 (${retry + 1}/${maxRetries})`) - await new Promise(resolve => setTimeout(resolve, 1000 * (retry + 1))) - return loadStructure(params, retry + 1) - } - // 超过重试次数,记录错误 - error.value = `加载失败(已重试 ${maxRetries} 次): ${err}` - } - } - - return { /* ... */ } -}) -``` - -### 7. 组件级状态同步检查 - -```typescript -// composables/useStateSync.ts -import { watch, nextTick } from 'vue' - -/** - * 状态同步检查器 - * 确保 Store 状态和组件 props 保持同步 - */ -export function useStateSync( - storeValue: () => T, - propValue: () => T, - name: string -) { - if (!import.meta.env.DEV) return - - watch( - () => storeValue(), - (storeVal) => { - nextTick(() => { - const propVal = propValue() - if (storeVal !== propVal) { - console.error( - `[StateSync] ${name} 不同步!`, - `Store: ${JSON.stringify(storeVal)}`, - `Prop: ${JSON.stringify(propVal)}` - ) - } - }) - }, - { deep: true } - ) -} -``` - -### 8. 测试策略 - -```typescript -// stores/db-cli/structure.test.ts -import { setActivePinia, createPinia } from 'pinia' -import { useStructureStore } from './structure' - -describe('StructureStore', () => { - beforeEach(() => { - setActivePinia(createPinia()) - }) - - it('应该正确加载结构数据', async () => { - const store = useStructureStore() - - await store.loadStructure({ - connectionId: 1, - database: 'test', - tableName: 'users', - dbType: 'mysql', - nodeType: 'table' - }) - - expect(store.loading).toBe(false) - expect(store.data).not.toBeNull() - expect(store.info).not.toBeNull() - }) - - it('应该在加载失败时设置错误', async () => { - // ... 测试错误处理 - }) -}) -``` - -## 迁移步骤 - -1. **阶段一:引入 Pinia** - - 安装依赖 - - 创建基础 Store 结构 - - 在主应用初始化 Pinia - -2. **阶段二:迁移状态** - - 先迁移 structure store(当前问题所在) - - 逐步迁移其他 stores - - 保持双写一段时间(Composable + Store) - -3. **阶段三:清理代码** - - 移除旧的 Composables - - 统一使用 Store - - 添加类型定义 - -4. **阶段四:优化和测试** - - 添加状态调试工具 - - 编写单元测试 - - 性能优化 - -## 优势总结 - -1. **单一数据源**:所有状态集中在 Store,避免分散 -2. **自动响应式**:Pinia 自动处理响应式,无需手动 computed -3. **开发工具**:Pinia DevTools 可以可视化状态变化 -4. **类型安全**:TypeScript 支持更好 -5. **易于测试**:Store 可以独立测试 -6. **状态持久化**:内置支持 localStorage/sessionStorage -7. **调试友好**:可以回放状态变更历史 - -## 注意事项 - -1. **不要过度使用**:简单的局部状态仍可使用 ref/reactive -2. **避免循环依赖**:Store 之间不要相互依赖 -3. **性能考虑**:大数据量使用 shallowRef -4. **SSR 兼容**:如需 SSR,注意状态初始化 - -## 参考资料 - -- [Pinia 官方文档](https://pinia.vuejs.org/) -- [Vue 3 Composition API 最佳实践](https://vuejs.org/guide/extras/composition-api-faq.html) diff --git a/docs/架构迁移完成指南.md b/docs/架构迁移完成指南.md deleted file mode 100644 index 0b525a2..0000000 --- a/docs/架构迁移完成指南.md +++ /dev/null @@ -1,350 +0,0 @@ -# 架构迁移完成指南 - 事件驱动架构 - -## 当前状态 - -已创建以下新文件: - -1. **`web/src/views/db-cli/composables/useEventBus.ts`** - 事件总线 - - 类型安全的事件定义 - - 支持事件订阅/取消/触发 - - 自动错误处理和日志 - -2. **`web/src/views/db-cli/composables/useStructureStore.ts`** - 新的表结构 Store - - 单例模式,全局共享状态 - - 事件驱动的状态更新 - - 清晰的日志追踪 - -3. **`web/src/views/db-cli/composables/useStructureStoreLegacy.ts`** - 旧版本(已重命名) - - 原 `useStructureState.ts` 的副本 - - 保留用于兼容和参考 - -4. **`web/src/views/db-cli/composables/MIGRATION.md`** - 迁移文档 - - 详细的对表和迁移步骤 - - 使用示例和注意事项 - -## 手动完成迁移步骤 - -### 步骤 1:修改 `index.vue` 的导入 - -**位置**:`web/src/views/db-cli/index.vue` 第 120 行 - -**原代码**: -```typescript -import { useStructureState } from './composables/useStructureState' -``` - -**修改为**: -```typescript -import { useStructureStore } from './composables/useStructureStore' -``` - ---- - -### 步骤 2:替换状态初始化(第 166-219 行) - -**原代码**(删除第 166-219 行): -```typescript -const structureState = useStructureState() -const { - structureLoading, - structureError, - structureData, - structureInfo, - loadStructure, - clearStructure, - refreshStructure -} = structureState - -// 使用计算属性确保响应式传递到子组件 -const computedStructureLoading = computed(() => { - const val = structureState.structureLoading.value - console.log('🔵 computedStructureLoading 计算:', val) - return val -}) -const computedStructureError = computed(() => { - const val = structureState.structureError.value - console.log('🔵 computedStructureError 计算:', val) - return val -}) -const computedStructureData = computed(() => { - const val = structureState.structureData.value - console.log('🔵 computedStructureData 计算:', val) - return val -}) -const computedStructureInfo = computed(() => { - const val = structureState.structureInfo.value - console.log('🔵 computedStructureInfo 计算:', val) - return val -}) - -// 添加调试监听,检查响应式 -watch(() => structureState.structureInfo.value, (newVal, oldVal) => { - // ... 所有 watch 代码 -}, { deep: true, immediate: true }) -watch(() => structureState.structureData.value, (newVal, oldVal) => { - // ... 所有 watch 代码 -}, { deep: true, immediate: true }) -``` - -**替换为**(在第 164 行之后添加): -```typescript -// 新架构:使用单例 Store(事件驱动) -const structureStore = useStructureStore() -// 直接使用 Store 的状态(无需计算属性,无需 watch) -// 状态是只读的,通过 Store 方法修改 -``` - ---- - -### 步骤 3:修改组件传参(第 65-68 行) - -**原代码**: -```vue - -``` - -**修改为**: -```vue - -``` - ---- - -### 步骤 4:修改 `handleTableStructure` 函数(第 357-389 行) - -**原代码**: -```typescript -const handleTableStructure = async (data: TableStructureEvent) => { - console.log('handleTableStructure 被调用:', data) - - // ... Tab 切换代码 ... - - // 加载表结构数据(在Tab切换后加载,确保用户能看到加载状态) - try { - await loadStructure( - data.connectionId, - data.database, - data.tableName, - data.dbType, - data.nodeType - ) - // ... 大量调试日志 ... - } catch (error) { - console.error('handleTableStructure 出错:', error) - } -} -``` - -**修改为**: -```typescript -const handleTableStructure = async (data: TableStructureEvent) => { - console.log('🚀 handleTableStructure 被调用:', data) - - // 如果结果面板隐藏,自动显示编辑器(这样结果面板也会显示) - if (!editorVisible.value) { - toggleEditor() - } - - // 先切换到结果面板的"结构"Tab(确保Tab可见) - if (resultPanelRef.value) { - (resultPanelRef.value as any).switchToStructureTab() - } - - // 等待一下确保Tab切换完成 - await new Promise(resolve => setTimeout(resolve, 100)) - - // 新架构:直接调用 Store 的 loadStructure 方法 - // Store 会自动管理状态和事件通知,无需手动追踪 - await structureStore.loadStructure( - data.connectionId, - data.database, - data.tableName, - data.dbType, - data.nodeType - ) - - console.log('✅ 加载完成,Store 当前状态:', { - loading: structureStore.loading.value, - data: structureStore.data.value, - info: structureStore.info.value, - error: structureStore.error.value - }) -} -``` - ---- - -### 步骤 5:修改 `handleRefreshStructure` 函数(第 456-462 行) - -**原代码**: -```typescript -const handleRefreshStructure = async () => { - await refreshStructure() -} -``` - -**修改为**: -```typescript -const handleRefreshStructure = async () => { - await structureStore.refreshStructure() -} -``` - ---- - -### 步骤 6:删除未使用的导入 - -检查是否有其他 `useStructureState` 的使用,全部替换为 `useStructureStore` - ---- - -## 验证迁移 - -完成以上步骤后,验证以下内容: - -### 1. 检查日志输出 - -运行应用,点击表结构,应该看到以下日志: - -``` -🚀 handleTableStructure 被调用: { connectionId: 6, database: 'flux_pro', tableName: 'clue_info', dbType: 'mysql', nodeType: 'table' } -📢 事件触发 [structure:loading]: { loading: true } -🏪 Store.loadStructure 开始: { connectionId: 6, database: 'flux_pro', tableName: 'clue_info', dbType: 'mysql', nodeType: 'table' } -🏪 表结构加载成功: { connectionId: 6, database: 'flux_pro', tableName: 'clue_info', result: {...} } -🏪 Store.setData: { data: {...}, info: {...} } -📢 事件触发 [structure:data]: { data: {...}, info: {...} } -📢 事件触发 [structure:loading]: { loading: false } -✅ 加载完成,Store 当前状态: { loading: false, data: {...}, info: {...}, error: '' } -``` - -### 2. 检查界面 - -切换到"结构"标签页,应该能看到: -- ✅ 红色测试框(如果存在) -- ✅ 调试信息块显示正确的数据 -- ✅ 表结构数据正常显示 - -### 3. 删除调试代码 - -确认功能正常后,删除: -- `ResultPanel.vue` 中的红色调试框 -- `ResultPanel.vue` 中的全局调试信息 -- `index.vue` 中不必要的日志 - ---- - -## 新架构的优势 - -### 1. 单一数据源 -- 所有状态集中在 Store -- 避免多个 Composable 实例 -- 全局共享,不会丢失 - -### 2. 事件驱动 -- 所有状态变更自动通知 -- 可追踪完整的事件流 -- 易于调试和问题定位 - -### 3. 自动响应式 -- Store 自动处理响应式 -- 无需手动计算属性 -- 无需 watch 监听 - -### 4. 类型安全 -- 完整的 TypeScript 类型定义 -- 事件和状态都有类型约束 -- 编译时错误检查 - -### 5. 清晰的日志 -- 所有关键操作都有日志 -- 使用 emoji 标识不同的日志来源 -- 易于过滤和搜索 - ---- - -## 故障排除 - -### 问题:Store 数据为 null - -**可能原因**: -1. 组件未正确引用 Store -2. 事件未正确触发 -3. Store 方法未正确调用 - -**解决方法**: -1. 检查控制台是否有 `🏪` 开头的日志 -2. 检查是否有 `📢` 开头的日志 -3. 确认 Store 是单例(只有一次 `useStructureStore` 调用) - -### 问题:Tab 内容不显示 - -**可能原因**: -1. Arco Tabs 配置问题 -2. CSS 样式冲突 -3. 数据未正确传递 - -**解决方法**: -1. 检查 props 是否正确传递 -2. 检查 CSS 中 `display: flex !important` 是否生效 -3. 检查浏览器开发工具中的元素状态 - ---- - -## 后续改进 - -1. **引入 Pinia**(可选) - - 更强大的状态管理 - - 内置 DevTools 支持 - - 持久化支持 - -2. **添加单元测试** - - 测试 Store 的各种场景 - - 测试事件总线的可靠性 - - 提高代码质量 - -3. **性能优化** - - 使用 `shallowRef` 处理大数据 - - 添加防抖和节流 - - 优化事件监听 - -4. **错误边界** - - 全局错误捕获 - - 错误恢复机制 - - 用户友好的错误提示 - ---- - -## 总结 - -新的事件驱动架构解决了当前的核心问题: - -✅ **状态丢失问题** - 单例模式确保全局唯一实例 -✅ **响应式失效问题** - 自动事件通知,无需手动追踪 -✅ **调试困难问题** - 完整的日志体系,清晰的事件流 -✅ **组件通信问题** - 事件总线解耦,易于维护 - -**下一步**:按照上述步骤手动完成代码迁移,然后测试验证。 diff --git a/go.mod b/go.mod index 6ec74de..2e498a3 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module u-desk -go 1.25.4 +go 1.25.6 require ( github.com/glebarez/sqlite v1.11.0 @@ -8,7 +8,7 @@ require ( github.com/redis/go-redis/v9 v9.17.3 github.com/shirou/gopsutil/v3 v3.24.5 github.com/wailsapp/wails/v2 v2.11.0 - go.mongodb.org/mongo-driver v1.17.7 + go.mongodb.org/mongo-driver/v2 v2.5.0 gorm.io/driver/mysql v1.6.0 gorm.io/gorm v1.31.1 ) @@ -22,7 +22,6 @@ require ( github.com/glebarez/go-sqlite v1.22.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/godbus/dbus/v5 v5.2.2 // indirect - github.com/golang/snappy v1.0.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect @@ -38,7 +37,6 @@ require ( github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/montanaflynn/stats v0.7.1 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect diff --git a/go.sum b/go.sum index 51af1ef..4599d69 100644 --- a/go.sum +++ b/go.sum @@ -25,8 +25,6 @@ github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1 github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= -github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= -github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= @@ -68,8 +66,6 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= -github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= @@ -124,8 +120,8 @@ github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfS github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -go.mongodb.org/mongo-driver v1.17.7 h1:a9w+U3Vt67eYzcfq3k/OAv284/uUUkL0uP75VE5rCOU= -go.mongodb.org/mongo-driver v1.17.7/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= +go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= diff --git a/internal/dbclient/mongo.go b/internal/dbclient/mongo.go index 0f5af75..9a7dd25 100644 --- a/internal/dbclient/mongo.go +++ b/internal/dbclient/mongo.go @@ -7,9 +7,9 @@ import ( "u-desk/internal/common" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" + "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/mongo" + "go.mongodb.org/mongo-driver/v2/mongo/options" ) // MongoClient MongoDB 客户端 @@ -111,11 +111,12 @@ func tryConnectMongo(config *MongoConfig, authSource, authMechanism string) (*Mo SetConnectTimeout(common.TimeoutConnect). SetServerSelectionTimeout(common.TimeoutConnect) - // 创建客户端 + // 创建客户端 (v2: 移除了 context 参数) + client, err := mongo.Connect(clientOptions) + + // 创建 context 用于其他操作 ctx, cancel := context.WithTimeout(context.Background(), common.TimeoutConnect) defer cancel() - - client, err := mongo.Connect(ctx, clientOptions) if err != nil { return nil, fmt.Errorf("连接 MongoDB 失败: %v", err) } @@ -659,14 +660,17 @@ func (c *MongoClient) PreviewCollectionIndexes(ctx context.Context, database, co continue } - // 构建索引选项 + // 构建索引选项,并跟踪 unique 状态(v2: IndexOptionsBuilder 无 Unique 字段可读) indexOptions := options.Index() indexOptions.SetName(name) + isUnique := false if unique, ok := idx["unique"].(bool); ok && unique { indexOptions.SetUnique(true) + isUnique = true } else if nonUnique, ok := idx["Non_unique"].(float64); ok && nonUnique == 0 { indexOptions.SetUnique(true) + isUnique = true } // 如果索引已存在,先删除再创建 @@ -686,7 +690,7 @@ func (c *MongoClient) PreviewCollectionIndexes(ctx context.Context, database, co keysStr += "}" optionsStr := "{name: \"" + name + "\"" - if indexOptions.Unique != nil && *indexOptions.Unique { + if isUnique { optionsStr += ", unique: true" } optionsStr += "}" @@ -748,7 +752,8 @@ func (c *MongoClient) UpdateCollectionIndexes(ctx context.Context, database, col // 删除不存在的索引 for name := range currentIndexMap { if !newIndexMap[name] { - _, err := coll.Indexes().DropOne(ctx, name) + // v2: DropOne 只返回 error,不再返回 bson.Raw + err := coll.Indexes().DropOne(ctx, name) if err != nil { return commands, fmt.Errorf("删除索引失败: %v, 索引名: %s", err, name) } @@ -803,7 +808,8 @@ func (c *MongoClient) UpdateCollectionIndexes(ctx context.Context, database, col // 如果索引已存在,先删除再创建 if currentIndexMap[name] { - _, err := coll.Indexes().DropOne(ctx, name) + // v2: DropOne 只返回 error,不再返回 bson.Raw + err := coll.Indexes().DropOne(ctx, name) if err != nil { return commands, fmt.Errorf("删除旧索引失败: %v, 索引名: %s", err, name) } diff --git a/internal/filesystem/content_detector.go b/internal/filesystem/content_detector.go new file mode 100644 index 0000000..d60a070 --- /dev/null +++ b/internal/filesystem/content_detector.go @@ -0,0 +1,133 @@ +package filesystem + +import ( + "bytes" + "fmt" + "os" +) + +const maxDetectSize = 500 * 1024 // 500KB + +// FileTypeInfo 文件类型信息 +type FileTypeInfo struct { + Extension string `json:"extension"` + Category string `json:"category"` // image, text, binary + MIMEType string `json:"mime_type"` + Confidence float64 `json:"confidence"` +} + +// 常见文件魔数 +var magicNumbers = []struct { + magic []byte + ext string + category string + mime string +}{ + // 图片 + {[]byte{0xFF, 0xD8, 0xFF}, "jpg", "image", "image/jpeg"}, + {[]byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}, "png", "image", "image/png"}, + {[]byte{0x47, 0x49, 0x46, 0x38}, "gif", "image", "image/gif"}, + {[]byte{0x42, 0x4D}, "bmp", "image", "image/bmp"}, + {[]byte{0x57, 0x45, 0x42, 0x50}, "webp", "image", "image/webp"}, + + // 文档 + {[]byte{0x25, 0x50, 0x44, 0x46}, "pdf", "pdf", "application/pdf"}, + + // 压缩 + {[]byte{0x50, 0x4B, 0x03, 0x04}, "zip", "archive", "application/zip"}, +} + +// DetectFileTypeByContent 通过文件内容检测文件类型 +func (s *FileSystemService) DetectFileTypeByContent(path string) (*FileTypeInfo, error) { + if err := s.validatePath(path); err != nil { + return nil, fmt.Errorf("路径验证失败: %w", err) + } + + info, err := os.Stat(path) + if err != nil { + return nil, fmt.Errorf("无法访问文件: %w", err) + } + + if info.Size() > maxDetectSize { + return &FileTypeInfo{Category: "unknown", Confidence: 0}, nil + } + + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("读取文件失败: %w", err) + } + + // 检测魔数 + for _, m := range magicNumbers { + if len(data) >= len(m.magic) && bytes.Equal(data[:len(m.magic)], m.magic) { + return &FileTypeInfo{ + Extension: m.ext, + Category: m.category, + MIMEType: m.mime, + Confidence: 0.95, + }, nil + } + } + + // 检测是否为文本 + if isTextContent(data) { + return &FileTypeInfo{ + Extension: "txt", + Category: "text", + MIMEType: "text/plain", + Confidence: 0.8, + }, nil + } + + return &FileTypeInfo{ + Extension: "", + Category: "binary", + MIMEType: "application/octet-stream", + Confidence: 0.5, + }, nil +} + +// isTextContent 检测是否为文本内容 +func isTextContent(data []byte) bool { + if len(data) == 0 { + return false + } + + textBytes := 0 + for _, b := range data[:min(len(data), 512)] { + if b == 9 || b == 10 || b == 13 || (b >= 32 && b <= 126) { + textBytes++ + } else if b == 0 { + return false + } + } + + return float64(textBytes)/float64(min(len(data), 512)) > 0.9 +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +// DetectFileTypeByContentSimple 简化接口 +func DetectFileTypeByContentSimple(path string) (map[string]interface{}, error) { + service, err := GetGlobalService() + if err != nil { + return nil, err + } + + info, err := service.DetectFileTypeByContent(path) + if err != nil { + return nil, err + } + + return map[string]interface{}{ + "extension": info.Extension, + "category": info.Category, + "mime_type": info.MIMEType, + "confidence": info.Confidence, + }, nil +} diff --git a/internal/filesystem/path_validator.go b/internal/filesystem/path_validator.go index b8253b9..5b9513e 100644 --- a/internal/filesystem/path_validator.go +++ b/internal/filesystem/path_validator.go @@ -122,15 +122,13 @@ func (v *DefaultPathValidator) checkWindowsSystemPaths(path string) *ValidationE if len(lowerPath) >= 3 && lowerPath[1] == ':' { driveLetter := lowerPath[0:1] - // 检查系统关键目录 + // 检查系统关键目录(仅保留最关键的系统目录) forbiddenDirs := []string{ driveLetter + ":\\windows", driveLetter + ":\\program files", driveLetter + ":\\program files (x86)", driveLetter + ":\\program files (arm)", - driveLetter + ":\\programdata", driveLetter + ":\\system volume information", - driveLetter + ":\\recovery", driveLetter + ":\\boot", } @@ -138,7 +136,7 @@ func (v *DefaultPathValidator) checkWindowsSystemPaths(path string) *ValidationE if strings.HasPrefix(lowerPath, fb) { return &ValidationError{ Path: path, - Reason: fmt.Sprintf("禁止访问系统目录: %s", fb), + Reason: "禁止访问系统关键目录", IsError: true, } } diff --git a/web/.eslintrc.js b/web/.eslintrc.js new file mode 100644 index 0000000..ef5aba6 --- /dev/null +++ b/web/.eslintrc.js @@ -0,0 +1,35 @@ +module.exports = { + root: true, + env: { + browser: true, + es2021: true, + node: true + }, + extends: [ + 'eslint:recommended', + 'plugin:vue/vue3-recommended', + 'plugin:@typescript-eslint/recommended' + ], + parser: 'vue-eslint-parser', + parserOptions: { + ecmaVersion: 'latest', + parser: '@typescript-eslint/parser', + sourceType: 'module' + }, + plugins: [ + 'vue', + '@typescript-eslint' + ], + rules: { + // 发现未使用的变量 + 'no-unused-vars': 'error', + '@typescript-eslint/no-unused-vars': 'error', + + // 禁止变量在声明前使用 + 'no-use-before-define': 'error', + + // Vue 规则 + 'vue/multi-word-component-names': 'off', + 'vue/no-unused-vars': 'error' + } +} diff --git a/web/package-lock.json b/web/package-lock.json index b38b68b..70bfa8f 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -29,7 +29,11 @@ "@codemirror/state": "^6.5.3", "@codemirror/theme-one-dark": "^6.1.3", "@codemirror/view": "^6.39.8", + "@types/highlight.js": "^9.12.4", + "@types/mermaid": "^9.1.0", + "highlight.js": "^11.11.1", "marked": "^17.0.1", + "mermaid": "^11.12.2", "vue": "^3.5.26" }, "devDependencies": { @@ -37,6 +41,19 @@ "vite": "^7.3.0" } }, + "node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/@arco-design/color": { "version": "0.4.0", "license": "MIT", @@ -100,6 +117,63 @@ "node": ">=6.9.0" } }, + "node_modules/@braintree/sanitize-url": { + "version": "7.1.2", + "resolved": "https://registry.npmmirror.com/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", + "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", + "license": "MIT" + }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "11.0.3", + "resolved": "https://registry.npmmirror.com/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz", + "integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "11.0.3", + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@chevrotain/cst-dts-gen/node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT" + }, + "node_modules/@chevrotain/gast": { + "version": "11.0.3", + "resolved": "https://registry.npmmirror.com/@chevrotain/gast/-/gast-11.0.3.tgz", + "integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@chevrotain/gast/node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT" + }, + "node_modules/@chevrotain/regexp-to-ast": { + "version": "11.0.3", + "resolved": "https://registry.npmmirror.com/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz", + "integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/types": { + "version": "11.0.3", + "resolved": "https://registry.npmmirror.com/@chevrotain/types/-/types-11.0.3.tgz", + "integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/utils": { + "version": "11.0.3", + "resolved": "https://registry.npmmirror.com/@chevrotain/utils/-/utils-11.0.3.tgz", + "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==", + "license": "Apache-2.0" + }, "node_modules/@codemirror/autocomplete": { "version": "6.20.0", "resolved": "https://registry.npmmirror.com/@codemirror/autocomplete/-/autocomplete-6.20.0.tgz", @@ -892,6 +966,23 @@ "node": ">=18" } }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/@iconify/utils/-/utils-3.1.0.tgz", + "integrity": "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==", + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.1.0", + "@iconify/types": "^2.0.0", + "mlly": "^1.8.0" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "license": "MIT" @@ -1057,6 +1148,15 @@ "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", "license": "MIT" }, + "node_modules/@mermaid-js/parser": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/@mermaid-js/parser/-/parser-0.6.3.tgz", + "integrity": "sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==", + "license": "MIT", + "dependencies": { + "langium": "3.3.1" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.53", "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", @@ -1088,11 +1188,289 @@ "win32" ] }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmmirror.com/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmmirror.com/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmmirror.com/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmmirror.com/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmmirror.com/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmmirror.com/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmmirror.com/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmmirror.com/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmmirror.com/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmmirror.com/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmmirror.com/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmmirror.com/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmmirror.com/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmmirror.com/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmmirror.com/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmmirror.com/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmmirror.com/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmmirror.com/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmmirror.com/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "dev": true, "license": "MIT" }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmmirror.com/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, + "node_modules/@types/highlight.js": { + "version": "9.12.4", + "resolved": "https://registry.npmmirror.com/@types/highlight.js/-/highlight.js-9.12.4.tgz", + "integrity": "sha512-t2szdkwmg2JJyuCM20e8kR2X59WCE5Zkl4bzm1u1Oukjm79zpbiAv+QjnwLnuuV0WHEcX2NgUItu0pAMKuOPww==", + "license": "MIT" + }, + "node_modules/@types/mermaid": { + "version": "9.1.0", + "resolved": "https://registry.npmmirror.com/@types/mermaid/-/mermaid-9.1.0.tgz", + "integrity": "sha512-rc8QqhveKAY7PouzY/p8ljS+eBSNCv7o79L97RSub/Ic2SQ34ph1Ng3s8wFLWVjvaEt6RLOWtSCsgYWd95NY8A==", + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@vitejs/plugin-vue": { "version": "6.0.3", "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-6.0.3.tgz", @@ -1192,6 +1570,18 @@ "version": "3.5.26", "license": "MIT" }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/b-tween": { "version": "0.3.3", "license": "MIT" @@ -1200,6 +1590,38 @@ "version": "1.5.3", "license": "MIT" }, + "node_modules/chevrotain": { + "version": "11.0.3", + "resolved": "https://registry.npmmirror.com/chevrotain/-/chevrotain-11.0.3.tgz", + "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/cst-dts-gen": "11.0.3", + "@chevrotain/gast": "11.0.3", + "@chevrotain/regexp-to-ast": "11.0.3", + "@chevrotain/types": "11.0.3", + "@chevrotain/utils": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/chevrotain-allstar": { + "version": "0.3.1", + "resolved": "https://registry.npmmirror.com/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz", + "integrity": "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==", + "license": "MIT", + "dependencies": { + "lodash-es": "^4.17.21" + }, + "peerDependencies": { + "chevrotain": "^11.0.0" + } + }, + "node_modules/chevrotain/node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT" + }, "node_modules/color": { "version": "3.2.1", "license": "MIT", @@ -1227,10 +1649,34 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/compute-scroll-into-view": { "version": "1.0.20", "license": "MIT" }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "license": "MIT" + }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "license": "MIT", + "dependencies": { + "layout-base": "^1.0.0" + } + }, "node_modules/crelt": { "version": "1.0.6", "resolved": "https://registry.npmmirror.com/crelt/-/crelt-1.0.6.tgz", @@ -1241,10 +1687,529 @@ "version": "3.2.3", "license": "MIT" }, + "node_modules/cytoscape": { + "version": "3.33.1", + "resolved": "https://registry.npmmirror.com/cytoscape/-/cytoscape-3.33.1.tgz", + "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "license": "MIT", + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "license": "MIT" + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmmirror.com/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmmirror.com/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmmirror.com/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmmirror.com/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmmirror.com/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmmirror.com/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.13", + "resolved": "https://registry.npmmirror.com/dagre-d3-es/-/dagre-d3-es-7.0.13.tgz", + "integrity": "sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q==", + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "lodash-es": "^4.17.21" + } + }, "node_modules/dayjs": { "version": "1.11.19", "license": "MIT" }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/dompurify": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/dompurify/-/dompurify-3.3.1.tgz", + "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/entities": { "version": "7.0.0", "license": "BSD-2-Clause", @@ -1334,10 +2299,104 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/hachure-fill": { + "version": "0.5.2", + "resolved": "https://registry.npmmirror.com/hachure-fill/-/hachure-fill-0.5.2.tgz", + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", + "license": "MIT" + }, + "node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-arrayish": { "version": "0.3.4", "license": "MIT" }, + "node_modules/katex": { + "version": "0.16.28", + "resolved": "https://registry.npmmirror.com/katex/-/katex-0.16.28.tgz", + "integrity": "sha512-YHzO7721WbmAL6Ov1uzN/l5mY5WWWhJBSW+jq4tkfZfsxmo1hu6frS0EOswvjBUnWE6NtjEs48SFn5CQESRLZg==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmmirror.com/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + }, + "node_modules/langium": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/langium/-/langium-3.3.1.tgz", + "integrity": "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==", + "license": "MIT", + "dependencies": { + "chevrotain": "~11.0.3", + "chevrotain-allstar": "~0.3.0", + "vscode-languageserver": "~9.0.1", + "vscode-languageserver-textdocument": "~1.0.11", + "vscode-uri": "~3.0.8" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.17.23", + "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.23.tgz", + "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", + "license": "MIT" + }, "node_modules/magic-string": { "version": "0.30.21", "license": "MIT", @@ -1357,6 +2416,58 @@ "node": ">= 20" } }, + "node_modules/mermaid": { + "version": "11.12.2", + "resolved": "https://registry.npmmirror.com/mermaid/-/mermaid-11.12.2.tgz", + "integrity": "sha512-n34QPDPEKmaeCG4WDMGy0OT6PSyxKCfy2pJgShP+Qow2KLrvWjclwbc3yXfSIf4BanqWEhQEpngWwNp/XhZt6w==", + "license": "MIT", + "dependencies": { + "@braintree/sanitize-url": "^7.1.1", + "@iconify/utils": "^3.0.1", + "@mermaid-js/parser": "^0.6.3", + "@types/d3": "^7.4.3", + "cytoscape": "^3.29.3", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.13", + "dayjs": "^1.11.18", + "dompurify": "^3.2.5", + "katex": "^0.16.22", + "khroma": "^2.1.0", + "lodash-es": "^4.17.21", + "marked": "^16.2.1", + "roughjs": "^4.6.6", + "stylis": "^4.3.6", + "ts-dedent": "^2.2.0", + "uuid": "^11.1.0" + } + }, + "node_modules/mermaid/node_modules/marked": { + "version": "16.4.2", + "resolved": "https://registry.npmmirror.com/marked/-/marked-16.4.2.tgz", + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/mlly": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/mlly/-/mlly-1.8.0.tgz", + "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.1" + } + }, "node_modules/nanoid": { "version": "3.3.11", "funding": [ @@ -1377,6 +2488,24 @@ "version": "1.6.0", "license": "MIT" }, + "node_modules/package-manager-detector": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/package-manager-detector/-/package-manager-detector-1.6.0.tgz", + "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", + "license": "MIT" + }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "license": "ISC" @@ -1395,6 +2524,33 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", + "license": "MIT" + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "resolved": "https://registry.npmmirror.com/points-on-path/-/points-on-path-0.2.1.tgz", + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", + "license": "MIT", + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, "node_modules/postcss": { "version": "8.5.6", "funding": [ @@ -1425,6 +2581,12 @@ "version": "1.5.1", "license": "MIT" }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense" + }, "node_modules/rollup": { "version": "4.54.0", "dev": true, @@ -1465,6 +2627,30 @@ "fsevents": "~2.3.2" } }, + "node_modules/roughjs": { + "version": "4.6.6", + "resolved": "https://registry.npmmirror.com/roughjs/-/roughjs-4.6.6.tgz", + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", + "license": "MIT", + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/scroll-into-view-if-needed": { "version": "2.2.31", "license": "MIT", @@ -1492,6 +2678,21 @@ "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", "license": "MIT" }, + "node_modules/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmmirror.com/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -1509,6 +2710,34 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, + "node_modules/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmmirror.com/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmmirror.com/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/vite": { "version": "7.3.0", "resolved": "https://registry.npmmirror.com/vite/-/vite-7.3.0.tgz", @@ -1585,6 +2814,55 @@ } } }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmmirror.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "9.0.1", + "resolved": "https://registry.npmmirror.com/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmmirror.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmmirror.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmmirror.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "license": "MIT" + }, + "node_modules/vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", + "license": "MIT" + }, "node_modules/vue": { "version": "3.5.26", "license": "MIT", diff --git a/web/package.json b/web/package.json index 70917ff..881b550 100644 --- a/web/package.json +++ b/web/package.json @@ -29,7 +29,11 @@ "@codemirror/state": "^6.5.3", "@codemirror/theme-one-dark": "^6.1.3", "@codemirror/view": "^6.39.8", + "@types/highlight.js": "^9.12.4", + "@types/mermaid": "^9.1.0", + "highlight.js": "^11.11.1", "marked": "^17.0.1", + "mermaid": "^11.12.2", "vue": "^3.5.26" }, "devDependencies": { diff --git a/web/package.json.md5 b/web/package.json.md5 index 5465935..c4604a6 100644 --- a/web/package.json.md5 +++ b/web/package.json.md5 @@ -1 +1 @@ -810f4ede0f42ca4e7c9d9a4b9c07f018 \ No newline at end of file +db157c3d15eff27c46a5fa33f3b95e47 \ No newline at end of file diff --git a/web/scripts/check.sh b/web/scripts/check.sh new file mode 100644 index 0000000..4eb0907 --- /dev/null +++ b/web/scripts/check.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +echo "🔍 开始代码质量检查..." +echo "" + +# 1. TypeScript 类型检查 +echo "1️⃣ TypeScript 类型检查" +npx vue-tsc --noEmit 2>&1 | tee type-errors.log | grep -E "error TS" | wc -l +echo "" + +# 2. ESLint 检查 +echo "2️⃣ ESLint 静态分析" +npx eslint src --ext .vue,.ts,.js 2>&1 | tee eslint-errors.log | wc -l +echo "" + +# 3. 统计错误类型 +echo "📊 错误统计:" +echo "TypeScript 错误:" +grep "error TS" type-errors.log | awk '{print $2}' | sort | uniq -c | sort -rn | head -10 +echo "" + +echo "ESLint 错误:" +cat eslint-errors.log | head -20 +echo "" + +echo "✅ 检查完成!" +echo "详细日志:type-errors.log, eslint-errors.log" diff --git a/web/src/App.vue b/web/src/App.vue index 0fe3f2a..394ab5c 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -7,20 +7,20 @@
- +
@@ -51,36 +51,35 @@ - + -` - ) - } else if (html.includes('')) { - html = html.replace( - //i, - ` - ` - ) - } - - // 在 之前插入链接拦截脚本 - if (html.includes('')) { - const linkScript = ' - - diff --git a/web/src/components/FileSystem/components/ContextMenu.vue b/web/src/components/FileSystem/components/ContextMenu.vue new file mode 100644 index 0000000..141b73c --- /dev/null +++ b/web/src/components/FileSystem/components/ContextMenu.vue @@ -0,0 +1,166 @@ + + + + + diff --git a/web/src/components/FileSystem/components/FileEditor/BinaryInfo.vue b/web/src/components/FileSystem/components/FileEditor/BinaryInfo.vue new file mode 100644 index 0000000..7974fc5 --- /dev/null +++ b/web/src/components/FileSystem/components/FileEditor/BinaryInfo.vue @@ -0,0 +1,102 @@ + + + + + diff --git a/web/src/components/FileSystem/components/FileEditor/CodeEditor.vue b/web/src/components/FileSystem/components/FileEditor/CodeEditor.vue new file mode 100644 index 0000000..5640282 --- /dev/null +++ b/web/src/components/FileSystem/components/FileEditor/CodeEditor.vue @@ -0,0 +1,98 @@ + + + + + diff --git a/web/src/components/FileSystem/components/FileEditor/MediaPreview.vue b/web/src/components/FileSystem/components/FileEditor/MediaPreview.vue new file mode 100644 index 0000000..b379d88 --- /dev/null +++ b/web/src/components/FileSystem/components/FileEditor/MediaPreview.vue @@ -0,0 +1,113 @@ + + + + + diff --git a/web/src/components/FileSystem/components/FileEditorPanel.new.vue b/web/src/components/FileSystem/components/FileEditorPanel.new.vue new file mode 100644 index 0000000..3ae981f --- /dev/null +++ b/web/src/components/FileSystem/components/FileEditorPanel.new.vue @@ -0,0 +1,190 @@ + + + + + diff --git a/web/src/components/FileSystem/components/FileEditorPanel.vue b/web/src/components/FileSystem/components/FileEditorPanel.vue new file mode 100644 index 0000000..f1f5bc0 --- /dev/null +++ b/web/src/components/FileSystem/components/FileEditorPanel.vue @@ -0,0 +1,863 @@ + + + + + diff --git a/web/src/components/FileSystem/components/FileItemRow.vue b/web/src/components/FileSystem/components/FileItemRow.vue new file mode 100644 index 0000000..9438e97 --- /dev/null +++ b/web/src/components/FileSystem/components/FileItemRow.vue @@ -0,0 +1,256 @@ + + + + + diff --git a/web/src/components/FileSystem/components/FileListPanel.vue b/web/src/components/FileSystem/components/FileListPanel.vue new file mode 100644 index 0000000..3b119c7 --- /dev/null +++ b/web/src/components/FileSystem/components/FileListPanel.vue @@ -0,0 +1,215 @@ + + + + + diff --git a/web/src/components/FileSystem/components/Sidebar.vue b/web/src/components/FileSystem/components/Sidebar.vue new file mode 100644 index 0000000..1ac27f1 --- /dev/null +++ b/web/src/components/FileSystem/components/Sidebar.vue @@ -0,0 +1,244 @@ + + + + + diff --git a/web/src/components/FileSystem/components/Toolbar.vue b/web/src/components/FileSystem/components/Toolbar.vue new file mode 100644 index 0000000..51e882c --- /dev/null +++ b/web/src/components/FileSystem/components/Toolbar.vue @@ -0,0 +1,309 @@ + + + + + diff --git a/web/src/components/FileSystem/composables/useCommonPaths.ts b/web/src/components/FileSystem/composables/useCommonPaths.ts new file mode 100644 index 0000000..2d158e8 --- /dev/null +++ b/web/src/components/FileSystem/composables/useCommonPaths.ts @@ -0,0 +1,94 @@ +/** + * 系统常用路径 Composable + * 提供系统路径获取和快捷访问路径管理 + */ + +import { ref } from 'vue' +import { PATH_ICONS } from '@/utils/constants' +import type { ShortcutPath } from '@/types/file-system' + +export function useCommonPaths() { + // 系统路径 + const commonPaths = ref([]) + const systemPaths = ref>({}) + + /** + * 加载常用系统路径 + */ + const loadCommonPaths = async () => { + try { + // 检查 Wails API 是否可用 + if (!window.go?.main?.App?.GetCommonPaths) { + // 降级方案:使用默认路径 + commonPaths.value = [ + { name: '💿 C盘', path: 'C:\\' }, + { name: '💿 D盘', path: 'D:\\' } + ] + return + } + + const paths = await window.go.main.App.GetCommonPaths() + if (!paths) { + throw new Error('无法获取系统路径') + } + + systemPaths.value = paths + const platform = window.navigator.platform + const pathList: ShortcutPath[] = [] + + if (platform.includes('Win')) { + // Windows: 先添加基础路径,再添加所有盘符 + if (paths.desktop) pathList.push({ name: `${PATH_ICONS.DESKTOP} 桌面`, path: paths.desktop }) + if (paths.documents) pathList.push({ name: `${PATH_ICONS.DOCUMENTS} 文档`, path: paths.documents }) + if (paths.downloads) pathList.push({ name: `${PATH_ICONS.DOWNLOADS} 下载`, path: paths.downloads }) + if (paths.home) pathList.push({ name: `${PATH_ICONS.HOME} 用户目录`, path: paths.home }) + + // 动态添加所有盘符(按字母顺序) + const drives: Array<{ letter: string; path: string }> = [] + for (const key in paths) { + if (key.startsWith('root_')) { + const driveLetter = key.substring(5) + drives.push({ + letter: driveLetter, + path: paths[key] + }) + } + } + drives.sort((a, b) => a.letter.localeCompare(b.letter)) + + // 添加盘符到路径列表 + drives.forEach(drive => { + pathList.push({ + name: `${PATH_ICONS.DRIVE} ${drive.letter}盘`, + path: drive.path + }) + }) + } else { + // macOS/Linux: 使用系统路径 + if (paths.desktop) pathList.push({ name: `${PATH_ICONS.DESKTOP} 桌面`, path: paths.desktop }) + if (paths.documents) pathList.push({ name: `${PATH_ICONS.DOCUMENTS} 文档`, path: paths.documents }) + if (paths.downloads) pathList.push({ name: `${PATH_ICONS.DOWNLOADS} 下载`, path: paths.downloads }) + if (paths.home) pathList.push({ name: `${PATH_ICONS.HOME} 主目录`, path: paths.home }) + pathList.push({ name: `${PATH_ICONS.ROOT} 根目录`, path: '/' }) + } + + commonPaths.value = pathList.length > 0 ? pathList : [ + { name: '💿 C盘', path: 'C:\\' }, + { name: '💿 D盘', path: 'D:\\' } + ] + } catch (error) { + console.error('加载系统路径失败:', error) + // 降级方案 + commonPaths.value = [ + { name: '💿 C盘', path: 'C:\\' }, + { name: '💿 D盘', path: 'D:\\' } + ] + } + } + + return { + commonPaths, + systemPaths, + loadCommonPaths + } +} diff --git a/web/src/components/FileSystem/composables/useFavorites.ts b/web/src/components/FileSystem/composables/useFavorites.ts new file mode 100644 index 0000000..4a6d85b --- /dev/null +++ b/web/src/components/FileSystem/composables/useFavorites.ts @@ -0,0 +1,231 @@ +/** + * 收藏夹管理 Composable + * 提供收藏文件的添加、删除、排序等功能 + */ + +import { ref, watch } from 'vue' +import { STORAGE_KEYS } from '@/utils/constants' +import type { FavoriteFile, FileItem, DraggingState } from '@/types/file-system' + +export function useFavorites() { + // 收藏列表 + const favorites = ref([]) + + // 拖拽状态 + const draggingState = ref({ + isDragging: false, + draggedIndex: -1, + pressedIndex: -1 + }) + + /** + * 从 localStorage 加载收藏列表 + */ + const loadFavorites = () => { + try { + const stored = localStorage.getItem(STORAGE_KEYS.FAVORITE_FILES) + if (stored) { + favorites.value = JSON.parse(stored) + } + } catch (error) { + console.error('加载收藏列表失败:', error) + } + } + + /** + * 保存收藏列表到 localStorage + */ + const saveFavorites = () => { + try { + localStorage.setItem(STORAGE_KEYS.FAVORITE_FILES, JSON.stringify(favorites.value)) + } catch (error) { + console.error('保存收藏列表失败:', error) + } + } + + /** + * 添加收藏 + */ + const addFavorite = (file: FileItem) => { + // 检查是否已存在 + const exists = favorites.value.some(fav => fav.path === file.path) + if (exists) { + return false + } + + favorites.value.push({ + ...file, + addedAt: Date.now() + } as FavoriteFile) + saveFavorites() + return true + } + + /** + * 标准化路径用于比较(处理正斜杠/反斜杠不一致) + */ + const normalizePath = (path: string): string => { + return path.replace(/\\/g, '/').toLowerCase() + } + + /** + * 删除收藏 + */ + const removeFavorite = (path: string) => { + const normalizedPath = normalizePath(path) + const index = favorites.value.findIndex(fav => normalizePath(fav.path) === normalizedPath) + if (index !== -1) { + favorites.value.splice(index, 1) + saveFavorites() + } + } + + /** + * 切换收藏状态 + */ + const toggleFavorite = (file: FileItem) => { + const exists = isFavorite(file.path) + if (exists) { + removeFavorite(file.path) + return false + } else { + addFavorite(file) + return true + } + } + + /** + * 检查是否已收藏 + */ + const isFavorite = (path: string): boolean => { + const normalizedPath = normalizePath(path) + return favorites.value.some(fav => normalizePath(fav.path) === normalizedPath) + } + + /** + * 长按开始 + */ + const onLongPressStart = (event: MouseEvent | TouchEvent, index: number) => { + const isMouse = event instanceof MouseEvent + const isTouch = event instanceof TouchEvent + + // 只支持鼠标左键或触摸 + if (isMouse && event.button !== 0) return + if (!isMouse && !isTouch) return + + draggingState.value.pressedIndex = index + draggingState.value.draggedIndex = index + } + + /** + * 长按取消 + */ + const onLongPressCancel = () => { + if (!draggingState.value.isDragging) { + draggingState.value.pressedIndex = -1 + draggingState.value.draggedIndex = -1 + } + } + + /** + * 拖拽开始 + */ + const onDragStart = (event: DragEvent, index: number) => { + draggingState.value.isDragging = true + draggingState.value.draggedIndex = index + + // 设置拖拽数据 + if (event.dataTransfer) { + event.dataTransfer.effectAllowed = 'move' + event.dataTransfer.setData('text/plain', index.toString()) + } + } + + /** + * 拖拽经过 + */ + const onDragOver = (event: DragEvent) => { + event.preventDefault() + if (event.dataTransfer) { + event.dataTransfer.dropEffect = 'move' + } + } + + /** + * 放置 + */ + const onDrop = (event: DragEvent, targetIndex: number) => { + event.preventDefault() + + const fromIndex = draggingState.value.draggedIndex + const toIndex = targetIndex + + if (fromIndex === toIndex || fromIndex === -1) { + resetDragging() + return + } + + // 移动元素 + const item = favorites.value.splice(fromIndex, 1)[0] + favorites.value.splice(toIndex, 0, item) + saveFavorites() + + resetDragging() + } + + /** + * 拖拽结束 + */ + const onDragEnd = () => { + resetDragging() + } + + /** + * 重置拖拽状态 + */ + const resetDragging = () => { + draggingState.value.isDragging = false + draggingState.value.draggedIndex = -1 + draggingState.value.pressedIndex = -1 + } + + /** + * 重新排序 + */ + const reorder = (fromIndex: number, toIndex: number) => { + if (fromIndex === toIndex) return + + const item = favorites.value.splice(fromIndex, 1)[0] + favorites.value.splice(toIndex, 0, item) + saveFavorites() + } + + // 组件挂载时加载收藏列表 + loadFavorites() + + return { + // 状态 + favorites, + draggingState, + + // 方法 + addFavorite, + removeFavorite, + toggleFavorite, + isFavorite, + + // 拖拽方法 + onLongPressStart, + onLongPressCancel, + onDragStart, + onDragOver, + onDrop, + onDragEnd, + reorder, + + // 工具方法 + loadFavorites, + saveFavorites, + resetDragging + } +} diff --git a/web/src/components/FileSystem/composables/useFileEdit.ts b/web/src/components/FileSystem/composables/useFileEdit.ts new file mode 100644 index 0000000..25e57a3 --- /dev/null +++ b/web/src/components/FileSystem/composables/useFileEdit.ts @@ -0,0 +1,576 @@ +/** + * 文件编辑 Composable + * 提供文件编辑相关的逻辑,包括草稿管理、保存、撤销等 + */ + +import { ref, computed, watch } from 'vue' +import { Message } from '@arco-design/web-vue' +import { STORAGE_KEYS, FILE_EXTENSIONS, FILE_SIZE_THRESHOLDS } from '@/utils/constants' +import { useFileOperations } from './useFileOperations' + +export interface UseFileEditOptions { + currentFilePath?: any + currentDirectory?: any +} + +// 文件大小限制(5MB) +const MAX_TEXT_FILE_SIZE = 5 * 1024 * 1024 // 5MB + +export function useFileEdit(options: UseFileEditOptions = {}) { + const { currentFilePath = ref(''), currentDirectory = ref('') } = options + + // 文件内容 + const fileContent = ref('') + const originalContent = ref('') + + // 编辑状态 + const isEditMode = ref(false) + const fileContentHeight = ref(400) + const isBinaryFile = ref(false) + + // 草稿管理 + const draftKey = ref('') + + // 保存状态 + const isSaving = ref(false) + + // 使用文件操作 composable + const { readFile, writeFile } = useFileOperations({ + onSuccess: (operation, data) => { + // 可以在这里添加成功处理逻辑 + }, + onError: (operation, error) => { + Message.error(`${operation} 失败: ${error.message}`) + } + }) + + /** + * 获取文件路径(从 FileItem 对象或字符串中提取) + */ + const getFilePath = (input: any): string => { + if (!input) return '' + if (typeof input === 'string') return input + if (input.path) return input.path + return '' + } + + /** + * 获取文件扩展名 + */ + const getFileExtension = (filepath: any): string => { + const path = getFilePath(filepath) + if (!path || typeof path !== 'string') return '' + return path.split('.').pop()?.toLowerCase() || '' + } + + /** + * 判断是否为图片文件 + */ + const isImageFile = (filepath: any): boolean => { + const ext = getFileExtension(filepath) + if (!ext) return false + return FILE_EXTENSIONS.IMAGE.includes(ext) + } + + /** + * 判断是否为视频文件 + */ + const isVideoFile = (filepath: any): boolean => { + const ext = getFileExtension(filepath) + if (!ext) return false + return FILE_EXTENSIONS.VIDEO.includes(ext) + } + + /** + * 判断是否为音频文件 + */ + const isAudioFile = (filepath: any): boolean => { + const ext = getFileExtension(filepath) + if (!ext) return false + return FILE_EXTENSIONS.AUDIO.includes(ext) + } + + /** + * 判断是否为 PDF 文件 + */ + const isPdfFile = (filepath: any): boolean => { + const ext = getFileExtension(filepath) + return ext === 'pdf' + } + + /** + * 判断是否为二进制文件(基于扩展名) + * 注意:媒体文件(图片、视频、音频、PDF)不是二进制文件,它们可以预览 + * 对于无扩展名的文件,返回 null 表示未知,需要内容检测 + */ + const isBinaryFileByExt = (filepath: any): boolean | null => { + const ext = getFileExtension(filepath) + if (!ext) return null // 无扩展名返回 null,表示需要进一步检测 + + // 媒体文件(可预览,不算二进制) + const isMediaFile = FILE_EXTENSIONS.IMAGE.includes(ext) || + FILE_EXTENSIONS.VIDEO.includes(ext) || + FILE_EXTENSIONS.AUDIO.includes(ext) || + ['pdf', 'html', 'htm', 'md', 'markdown'].includes(ext) + + // 文本或代码文件(可编辑) + const isTextFile = FILE_EXTENSIONS.TEXT.includes(ext) || + FILE_EXTENSIONS.CODE.includes(ext) || + ['json', 'xml', 'yaml', 'yml', 'toml', 'ini', 'conf', 'cfg', 'props'].includes(ext) + + // 如果是媒体文件或文本文件,就不是二进制 + if (isMediaFile || isTextFile) return false + + // 确认的二进制文件类型 + const knownBinaryTypes = ['exe', 'dll', 'so', 'bin', 'zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz', 'iso', 'img', 'dmg', 'pdb', 'idb', 'lib', 'obj', 'o', 'a'] + if (knownBinaryTypes.includes(ext)) return true + + // 其他扩展名未知,需要内容检测 + return null + } + + /** + * 计算属性:当前视图是否可编辑 + * 图片、视频、音频、PDF、二进制文件不可编辑 + */ + const isEditableView = computed(() => { + const path = getFilePath(currentFilePath.value) + if (!path) return false + const binaryCheck = isBinaryFileByExt(path) + return !isImageFile(path) && + !isVideoFile(path) && + !isAudioFile(path) && + !isPdfFile(path) && + binaryCheck !== true // true 表示是二进制,不可编辑;false 或 null 表示可尝试编辑 + }) + + /** + * 计算属性:文件内容是否改变 + */ + const contentChanged = computed(() => { + return fileContent.value !== '' && + originalContent.value !== undefined && + originalContent.value !== fileContent.value + }) + + /** + * 计算属性:是否可以保存 + */ + const canSaveFile = computed(() => { + return isEditableView.value && contentChanged.value + }) + + /** + * 计算属性:是否可以重置 + */ + const canResetContent = computed(() => { + return contentChanged.value && originalContent.value !== undefined + }) + + /** + * 检测文件内容是否为二进制 + */ + const detectBinaryContent = (content: string): boolean => { + if (!content || content.length === 0) return false + + // 检查前 1000 个字符中二进制字符的比例 + const checkLength = Math.min(content.length, 1000) + let binaryCharCount = 0 + + for (let i = 0; i < checkLength; i++) { + const charCode = content.charCodeAt(i) + // 空字节肯定是二进制 + // 控制字符(charCode < 32)除了 Tab(9)、LF(10)、CR(13) 外都是二进制 + if (charCode === 0 || (charCode < 32 && charCode !== 9 && charCode !== 10 && charCode !== 13)) { + binaryCharCount++ + } + } + + // 如果二进制字符超过 5%,认为是二进制文件 + const binaryRatio = binaryCharCount / checkLength + return binaryRatio > 0.05 + } + + /** + * 读取文件内容 + */ + const loadFile = async (path: string) => { + try { + isBinaryFile.value = false + + // 先清空内容,避免显示之前文件的内容 + fileContent.value = '' + originalContent.value = '' + + const filename = getFilePath(path) + const ext = getFileExtension(filename) + + // 先检查扩展名,如果是已知的二进制文件,直接生成提示信息 + const binaryCheck = isBinaryFileByExt(filename) + if (binaryCheck === true) { + isBinaryFile.value = true + + const fileTypeDescriptions: Record = { + 'exe': '可执行文件', + 'dll': '动态链接库', + 'so': '共享库', + 'bin': '二进制文件', + 'dat': '数据文件', + 'db': '数据库文件', + 'sqlite': 'SQLite 数据库', + 'zip': 'ZIP 压缩文件', + 'rar': 'RAR 压缩文件', + '7z': '7Z 压缩文件', + 'tar': 'TAR 归档文件', + 'gz': 'GZ 压缩文件', + 'bz2': 'BZ2 压缩文件', + 'xz': 'XZ 压缩文件', + 'iso': '光盘镜像', + 'img': '磁盘镜像', + 'dmg': 'DMG 镜像', + 'pdb': '程序数据库', + 'idb': 'IDA 数据库', + 'lib': '库文件', + 'obj': '目标文件', + 'o': '目标文件', + 'a': '静态库' + } + + const fileTypeDesc = fileTypeDescriptions[ext] || `${ext.toUpperCase()} 文件` + const fileName = filename.split('\\').pop() || filename.split('/').pop() || filename + + fileContent.value = `================================================================ +文件信息:${fileTypeDesc} +================================================================ + +文件名: ${fileName} +完整路径: ${filename} +文件类型: ${fileTypeDesc} + +================================================================ +ℹ️ 这是已知的二进制文件类型,不支持文本预览 + +💡 提示: + • 右键菜单 → "使用系统程序打开" 在默认应用中打开 + • 右键菜单 → "在资源管理器中显示" 查看文件位置 +================================================================` + originalContent.value = fileContent.value + isEditMode.value = false + return + } + + // 对于无扩展名或未知类型文件,先尝试读取 + const content = await readFile(path) + + // 检查文件大小 + const fileSize = content.length // UTF-16 字符数 + if (fileSize > MAX_TEXT_FILE_SIZE) { + const sizeMB = (fileSize / 1024 / 1024).toFixed(2) + const fileName = filename.split('\\').pop() || filename.split('/').pop() || filename + + fileContent.value = `================================================================ +⚠️ 文件过大 (${sizeMB} MB) +================================================================ + +文件名: ${fileName} +完整路径: ${filename} +文件大小: ${sizeMB} MB + +================================================================ +当前文件大小超过 5MB,不适合在编辑器中打开。 + +💡 建议: + • 使用命令行工具查看部分内容 + • 将文件拆分成多个小文件 + • 使用专门的工具处理大文件 +================================================================` + originalContent.value = fileContent.value + isEditMode.value = false + return + } + + // 检测是否为二进制内容 + if (detectBinaryContent(content)) { + isBinaryFile.value = true + const fileTypeDesc = ext ? `${ext.toUpperCase()} 文件` : '未知类型文件' + const fileName = filename.split('\\').pop() || filename.split('/').pop() || filename + + // 根据是否有扩展名,显示不同提示 + const isUnknownType = !ext + const messageTitle = isUnknownType ? '文件信息(未知类型)' : `文件信息:${fileTypeDesc}` + const messageDesc = isUnknownType + ? '此文件没有扩展名,且内容检测显示为二进制格式' + : `此文件扩展名为 .${ext},但内容检测显示为二进制格式` + + fileContent.value = `================================================================ +${messageTitle} +================================================================ + +文件名: ${fileName} +完整路径: ${filename} +${ext ? `文件类型: ${fileTypeDesc}\n` : ''} + +================================================================ +ℹ️ ${messageDesc},不支持文本预览 + +💡 提示: + • 右键菜单 → "使用系统程序打开" 在默认应用中打开 + • 右键菜单 → "在资源管理器中显示" 查看文件位置 +================================================================` + originalContent.value = fileContent.value + isEditMode.value = false + return + } + + // 正常文本文件 + fileContent.value = content + originalContent.value = content + + // 加载草稿(如果存在) + loadDraft(path) + } catch (error) { + Message.error(`读取文件失败: ${error}`) + } + } + + /** + * 保存文件内容 + */ + const saveFile = async (path?: string, isShortcut: boolean = false) => { + // 获取目标路径(优先使用传入的 path,否则从 currentFilePath 中提取) + let targetPath = path + if (!targetPath && currentFilePath.value) { + targetPath = getFilePath(currentFilePath.value) + } + + if (!targetPath) { + Message.error('没有选中的文件') + return + } + + // 检查内容是否真的改变了 + if (fileContent.value === originalContent.value) { + if (!isShortcut) { + Message.info('文件内容未变更') + } + return + } + + isSaving.value = true + + try { + await writeFile(targetPath, fileContent.value) + originalContent.value = fileContent.value + + // 清除草稿 + clearDraft() + + if (!isShortcut) { + Message.success('保存成功') + } + } catch (error) { + Message.error(`保存失败: ${error}`) + } finally { + // 延迟清除保存状态 + setTimeout(() => { + isSaving.value = false + }, isShortcut ? 300 : 500) + } + } + + /** + * 保存草稿 + */ + const saveDraft = () => { + if (!currentFilePath.value) return + + const key = `${STORAGE_KEYS.FILESYSTEM.FILE_DRAFT}-${currentFilePath.value}` + const draft = { + content: fileContent.value, + savedAt: Date.now() + } + + try { + localStorage.setItem(key, JSON.stringify(draft)) + draftKey.value = key + } catch (error) { + console.error('保存草稿失败:', error) + } + } + + /** + * 加载草稿 + */ + const loadDraft = (path: string) => { + const key = `${STORAGE_KEYS.FILESYSTEM.FILE_DRAFT}-${path}` + draftKey.value = key + + try { + const stored = localStorage.getItem(key) + if (stored) { + const draft = JSON.parse(stored) + const ageInHours = (Date.now() - draft.savedAt) / (1000 * 60 * 60) + + // 如果草稿超过 24 小时,自动清除 + if (ageInHours > 24) { + clearDraft() + return + } + + // 恢复草稿内容 + fileContent.value = draft.content + Message.info('已恢复未保存的草稿') + } + } catch (error) { + console.error('加载草稿失败:', error) + } + } + + /** + * 清除草稿 + */ + const clearDraft = () => { + if (!draftKey.value) return + + try { + localStorage.removeItem(draftKey.value) + draftKey.value = '' + } catch (error) { + console.error('清除草稿失败:', error) + } + } + + /** + * 重置文件内容 + */ + const resetContent = () => { + if (originalContent.value !== undefined) { + fileContent.value = originalContent.value + Message.info('已恢复原始内容') + } + } + + /** + * 清空文件内容 + */ + const clearContent = () => { + fileContent.value = '' + originalContent.value = '' + } + + /** + * 切换编辑模式 + */ + const toggleEditMode = () => { + isEditMode.value = !isEditMode.value + } + + /** + * 进入编辑模式 + */ + const enterEditMode = () => { + isEditMode.value = true + } + + /** + * 退出编辑模式 + */ + const exitEditMode = () => { + // 如果有未保存的更改,提示用户 + if (contentChanged.value) { + // 这里可以添加确认对话框 + // 暂时直接退出 + } + isEditMode.value = false + } + + /** + * 更新文件内容 + */ + const updateContent = (content: string) => { + // 确保只有在内容真正改变时才更新 + if (fileContent.value !== content) { + fileContent.value = content + } + + // 自动保存草稿(防抖) + // 实际实现应该使用防抖函数 + // saveDraft() + } + + /** + * 设置编辑器高度 + */ + const setEditorHeight = (height: number) => { + fileContentHeight.value = Math.max(200, height) + } + + /** + * 判断文件是否在当前目录 + */ + const isFileInCurrentDirectory = (filePathInput: any): boolean => { + const filePath = getFilePath(filePathInput) + if (!filePath || !currentDirectory.value) { + return true + } + return filePath.startsWith(currentDirectory.value) + } + + // 监听文件内容变化,自动保存草稿 + watch(fileContent, () => { + // 实际实现应该使用防抖 + // saveDraft() + }, { deep: true }) + + // 监听文件路径变化,清除草稿 + watch(currentFilePath, (newPath, oldPath) => { + if (newPath !== oldPath) { + clearDraft() + } + }) + + return { + // 状态 + fileContent, + originalContent, + isEditMode, + fileContentHeight, + isSaving, + isBinaryFile, + draftKey, + + // 计算属性 + contentChanged, + canSaveFile, + canResetContent, + isEditableView, + + // 文件操作 + loadFile, + saveFile, + updateContent, + + // 草稿管理 + saveDraft, + loadDraft, + clearDraft, + + // 编辑模式 + toggleEditMode, + enterEditMode, + exitEditMode, + + // 其他 + resetContent, + clearContent, + setEditorHeight, + + // 文件类型检查 + isImageFile, + isVideoFile, + isAudioFile, + isPdfFile, + isBinaryFileByExt, + isFileInCurrentDirectory + } +} diff --git a/web/src/components/FileSystem/composables/useFileOperations.ts b/web/src/components/FileSystem/composables/useFileOperations.ts new file mode 100644 index 0000000..a97d988 --- /dev/null +++ b/web/src/components/FileSystem/composables/useFileOperations.ts @@ -0,0 +1,264 @@ +/** + * 文件操作 Composable + * 提供文件读取、写入、删除等基础操作,包括 ZIP 文件浏览 + */ + +import { ref } from 'vue' +import { Message } from '@arco-design/web-vue' +import { + listDir, + readFile as readFileApi, + writeFile as writeFileApi, + deletePath as deletePathApi, + createFile, + createDir, + renamePath as renamePathApi, + listZipContents, + extractFileFromZip, + extractFileFromZipToTemp, + getFileServerURL +} from '@/api' +import type { FileOperationResult } from '@/types/file-system' + +export interface UseFileOperationsOptions { + onSuccess?: (operation: string, data: any) => void + onError?: (operation: string, error: Error) => void +} + +/** + * 文件操作结果 + */ +export function useFileOperations(options: UseFileOperationsOptions = {}) { + const { onSuccess, onError } = options + + /** + * 列出目录内容 + */ + const listDirectory = async (path: string): Promise => { + try { + const result = await listDir(path) + onSuccess?.('listDirectory', result) + return result + } catch (error) { + const err = error as Error + onError?.('listDirectory', err) + throw err + } + } + + /** + * 读取文件内容 + */ + const readFile = async (path: string): Promise => { + try { + const content = await readFileApi(path) + onSuccess?.('readFile', { path, size: content.length }) + return content + } catch (error) { + const err = error as Error + onError?.('readFile', err) + throw err + } + } + + /** + * 写入文件内容 + */ + const writeFile = async ( + path: string, + content: string, + createBackup: boolean = false + ): Promise => { + try { + await writeFileApi(path, content) + onSuccess?.('writeFile', { path, size: content.length }) + } catch (error) { + const err = error as Error + onError?.('writeFile', err) + throw err + } + } + + /** + * 删除路径(文件或目录) + */ + const deletePath = async (path: string): Promise => { + try { + await deletePathApi(path) + onSuccess?.('deletePath', { path }) + } catch (error) { + const err = error as Error + onError?.('deletePath', err) + throw err + } + } + + /** + * 创建新文件 + */ + const createNewFile = async ( + dirPath: string, + filename: string, + content: string = '' + ): Promise => { + try { + await createFile(dirPath, filename, content) + onSuccess?.('createFile', { dirPath, filename }) + } catch (error) { + const err = error as Error + onError?.('createFile', err) + throw err + } + } + + /** + * 创建新目录 + */ + const createNewDir = async (parentPath: string, dirname: string): Promise => { + try { + await createDir(parentPath, dirname) + onSuccess?.('createDir', { parentPath, dirname }) + } catch (error) { + const err = error as Error + onError?.('createDir', err) + throw err + } + } + + /** + * 重命名文件或目录 + */ + const rename = async (oldPath: string, newName: string): Promise => { + // 构造新路径 + const separator = oldPath.includes('\\') ? '\\' : '/' + const parentPath = oldPath.substring( + 0, + Math.max(oldPath.lastIndexOf('\\'), oldPath.lastIndexOf('/')) + ) + const newPath = parentPath + separator + newName + + try { + await renamePathApi(oldPath, newPath) + onSuccess?.('rename', { oldPath, newPath }) + } catch (error) { + const err = error as Error + onError?.('rename', err) + throw err + } + } + + /** + * 复制文件或目录 + */ + const copy = async (fromPath: string, toPath: string): Promise => { + try { + // TODO: 实现复制逻辑 + Message.warning('复制功能暂未实现') + onSuccess?.('copy', { fromPath, toPath }) + } catch (error) { + const err = error as Error + onError?.('copy', err) + throw err + } + } + + /** + * 移动文件或目录 + */ + const move = async (fromPath: string, toPath: string): Promise => { + try { + // TODO: 实现移动逻辑 + Message.warning('移动功能暂未实现') + onSuccess?.('move', { fromPath, toPath }) + } catch (error) { + const err = error as Error + onError?.('move', err) + throw err + } + } + + /** + * 列出 ZIP 文件内容 + */ + const listZipContents = async (zipPath: string): Promise => { + try { + const result = await listZipContents(zipPath) + onSuccess?.('listZipContents', { zipPath, count: result.length }) + return result + } catch (error) { + const err = error as Error + onError?.('listZipContents', err) + throw err + } + } + + /** + * 从 ZIP 中提取文件内容(文本) + */ + const extractZipFile = async (zipPath: string, filePath: string): Promise => { + try { + const content = await extractFileFromZip(zipPath, filePath) + onSuccess?.('extractZipFile', { zipPath, filePath, size: content.length }) + return content + } catch (error) { + const err = error as Error + onError?.('extractZipFile', err) + throw err + } + } + + /** + * 从 ZIP 中提取文件到临时目录(二进制文件,如图片) + */ + const extractZipFileToTemp = async (zipPath: string, filePath: string): Promise => { + try { + const tempPath = await extractFileFromZipToTemp(zipPath, filePath) + onSuccess?.('extractZipFileToTemp', { zipPath, filePath, tempPath }) + return tempPath + } catch (error) { + const err = error as Error + onError?.('extractZipFileToTemp', err) + throw err + } + } + + /** + * 获取文件服务器 URL + */ + const getFileServerURL = async (): Promise => { + try { + const url = await getFileServerURL() + onSuccess?.('getFileServerURL', { url }) + return url + } catch (error) { + const err = error as Error + onError?.('getFileServerURL', err) + throw err + } + } + + return { + // 基础操作 + listDirectory, + readFile, + writeFile, + deletePath, + + // 创建操作 + createNewFile, + createNewDir, + + // 高级操作 + rename, + copy, + move, + + // ZIP 操作 + listZipContents, + extractZipFile, + extractZipFileToTemp, + getFileServerURL + } +} + +// 注意:FileItem 类型已统一定义在 @/types/file-system.ts \ No newline at end of file diff --git a/web/src/components/FileSystem/composables/useFilePreview.ts b/web/src/components/FileSystem/composables/useFilePreview.ts new file mode 100644 index 0000000..f0bf10e --- /dev/null +++ b/web/src/components/FileSystem/composables/useFilePreview.ts @@ -0,0 +1,319 @@ +/** + * 文件预览 Composable + * 提供文件预览 URL 生成、媒体元数据获取等功能 + */ + +import { ref, computed } from 'vue' +import { FILE_EXTENSIONS, FILE_SIZE_THRESHOLDS } from '@/utils/constants' +import { normalizeFilePath } from '@/utils/fileUtils' +import { detectFileTypeByContent } from '@/api/system' +import type { FilePreviewMetadata, FileType } from '@/types/file-system' + +// 内容检测大小限制(与后端一致) +const CONTENT_DETECT_MAX_SIZE = 500 * 1024 // 500KB + +// 缓存检测结果 +const contentDetectCache = new Map() +const CACHE_TTL = 60000 // 1分钟缓存 + +export interface UseFilePreviewOptions { + filePath?: string + isBrowsingZip?: boolean +} + +export function useFilePreview(options: UseFilePreviewOptions = {}) { + const { filePath = ref(''), isBrowsingZip = ref(false) } = options + + // 文件服务器 URL(硬编码,与旧版本保持一致) + const fileServerURL = 'http://localhost:18765' + + // 预览 URL + const previewUrl = ref('') + + // 媒体加载状态 + const imageLoading = ref(false) + const currentImageDimensions = ref('') + + /** + * 获取预览 URL(与旧版本保持一致) + */ + const getPreviewUrl = (path: string): string => { + if (!path) return '' + // 使用与旧版本相同的 URL 格式:/localfs/ 路径 + 规范化路径 + return `${fileServerURL}/localfs/${normalizeFilePath(path, true)}` + } + + /** + * 通过内容检测文件类型(用于小文件) + */ + const detectByContent = async (path: string, fileSize?: number): Promise<{ category: string; ext: string } | null> => { + // 如果文件太大,跳过内容检测 + if (fileSize !== undefined && fileSize > CONTENT_DETECT_MAX_SIZE) { + return null + } + + // 检查缓存 + const cached = contentDetectCache.get(path) + if (cached && Date.now() - cached.timestamp < CACHE_TTL) { + return cached.result + } + + try { + const result = await detectFileTypeByContent(path) + const data = { category: result.category, ext: result.extension } + contentDetectCache.set(path, { timestamp: Date.now(), result: data }) + return data + } catch { + return null + } + } + + /** + * 更新预览 URL + */ + const updatePreviewUrl = (path: string) => { + previewUrl.value = getPreviewUrl(path) + } + + /** + * 获取文件类型 + */ + const getFileType = (filename: string): FileType => { + if (!filename || typeof filename !== 'string') return 'Binary' as FileType + + const ext = filename.split('.').pop()?.toLowerCase() || '' + + // 图片 + if (FILE_EXTENSIONS.IMAGE.includes(ext)) { + return 'Image' as FileType + } + + // 视频 + if (FILE_EXTENSIONS.VIDEO.includes(ext)) { + return 'Video' as FileType + } + + // 音频 + if (FILE_EXTENSIONS.AUDIO.includes(ext)) { + return 'Audio' as FileType + } + + // PDF + if (ext === 'pdf') { + return 'Pdf' as FileType + } + + // HTML + if (['html', 'htm'].includes(ext)) { + return 'Html' as FileType + } + + // Markdown + if (['md', 'markdown'].includes(ext)) { + return 'Markdown' as FileType + } + + // 代码 + if (FILE_EXTENSIONS.CODE.includes(ext)) { + return 'Code' as FileType + } + + // 文本 + if (FILE_EXTENSIONS.TEXT.includes(ext)) { + return 'Text' as FileType + } + + // 默认为二进制 + return 'Binary' as FileType + } + + /** + * 判断是否为图片文件 + */ + const isImageFile = (filename: string): boolean => { + if (!filename || typeof filename !== 'string') return false + const ext = filename.split('.').pop()?.toLowerCase() || '' + return FILE_EXTENSIONS.IMAGE.includes(ext) + } + + /** + * 判断是否为视频文件 + */ + const isVideoFile = (filename: string): boolean => { + if (!filename || typeof filename !== 'string') return false + const ext = filename.split('.').pop()?.toLowerCase() || '' + return FILE_EXTENSIONS.VIDEO.includes(ext) + } + + /** + * 判断是否为音频文件 + */ + const isAudioFile = (filename: string): boolean => { + if (!filename || typeof filename !== 'string') return false + const ext = filename.split('.').pop()?.toLowerCase() || '' + return FILE_EXTENSIONS.AUDIO.includes(ext) + } + + /** + * 判断是否为 PDF 文件 + */ + const isPdfFile = (filename: string): boolean => { + if (!filename || typeof filename !== 'string') return false + const ext = filename.split('.').pop()?.toLowerCase() || '' + return ext === 'pdf' + } + + /** + * 判断是否为 HTML 文件 + */ + const isHtmlFile = (filename: string): boolean => { + if (!filename || typeof filename !== 'string') return false + const ext = filename.split('.').pop()?.toLowerCase() || '' + return ['html', 'htm'].includes(ext) + } + + /** + * 判断是否为 Markdown 文件 + */ + const isMarkdownFile = (filename: string): boolean => { + if (!filename || typeof filename !== 'string') return false + const ext = filename.split('.').pop()?.toLowerCase() || '' + return ['md', 'markdown'].includes(ext) + } + + /** + * 判断是否为代码文件 + */ + const isCodeFile = (filename: string): boolean => { + if (!filename || typeof filename !== 'string') return false + const ext = filename.split('.').pop()?.toLowerCase() || '' + return FILE_EXTENSIONS.CODE.includes(ext) + } + + /** + * 判断是否为文本文件 + */ + const isTextFile = (filename: string): boolean => { + if (!filename || typeof filename !== 'string') return false + const ext = filename.split('.').pop()?.toLowerCase() || '' + return FILE_EXTENSIONS.TEXT.includes(ext) + } + + /** + * 判断文件是否可预览 + */ + const isPreviewable = (filename: string): boolean => { + if (!filename || typeof filename !== 'string') return false + const ext = filename.split('.').pop()?.toLowerCase() || '' + return FILE_EXTENSIONS.IMAGE.includes(ext) || + FILE_EXTENSIONS.VIDEO.includes(ext) || + FILE_EXTENSIONS.AUDIO.includes(ext) || + ext === 'pdf' || + ['html', 'htm'].includes(ext) || + ['md', 'markdown'].includes(ext) + } + + /** + * 判断文件是否可编辑 + */ + const isEditable = (filename: string, fileSize: number): boolean => { + // 检查文件大小 + if (fileSize > FILE_SIZE_THRESHOLDS.BIG_FILE) { + return false + } + + // 检查文件类型 + if (!filename || typeof filename !== 'string') return false + const ext = filename.split('.').pop()?.toLowerCase() || '' + return FILE_EXTENSIONS.CODE.includes(ext) || + FILE_EXTENSIONS.TEXT.includes(ext) || + ['html', 'htm', 'md', 'markdown', 'json', 'xml'].includes(ext) + } + + /** + * 图片加载完成 + */ + const onImageLoad = (event: Event) => { + const img = event.target as HTMLImageElement + if (img.naturalWidth && img.naturalHeight) { + currentImageDimensions.value = `${img.naturalWidth} × ${img.naturalHeight}` + } + imageLoading.value = false + } + + /** + * 图片加载失败 + */ + const onImageError = () => { + imageLoading.value = false + currentImageDimensions.value = '' + } + + /** + * 开始加载图片 + */ + const startImageLoad = () => { + imageLoading.value = true + currentImageDimensions.value = '' + } + + /** + * 获取媒体元数据 + */ + const getMediaMetadata = async (url: string): Promise => { + const metadata: FilePreviewMetadata = {} + + // 对于图片,使用 Image 对象 + if (url.match(/\.(jpg|jpeg|png|gif|webp|svg)$/i)) { + return new Promise((resolve) => { + const img = new Image() + img.onload = () => { + metadata.width = img.naturalWidth + metadata.height = img.naturalHeight + resolve(metadata) + } + img.onerror = () => resolve(metadata) + img.src = url + }) + } + + // 对于视频/音频,可以使用 Video/Audio 对象 + // 但由于跨域等问题,这里简化处理 + return metadata + } + + return { + // 状态 + previewUrl, + imageLoading, + currentImageDimensions, + + // URL 相关 + getPreviewUrl, + updatePreviewUrl, + + // 文件类型判断(同步,基于扩展名) + getFileType, + isImageFile, + isVideoFile, + isAudioFile, + isPdfFile, + isHtmlFile, + isMarkdownFile, + isCodeFile, + isTextFile, + isPreviewable, + isEditable, + + // 内容检测(异步,基于文件内容) + detectByContent, + + // 事件处理 + onImageLoad, + onImageError, + startImageLoad, + + // 工具方法 + getMediaMetadata + } +} diff --git a/web/src/components/FileSystem/composables/usePathNavigation.ts b/web/src/components/FileSystem/composables/usePathNavigation.ts new file mode 100644 index 0000000..ee14aee --- /dev/null +++ b/web/src/components/FileSystem/composables/usePathNavigation.ts @@ -0,0 +1,243 @@ +/** + * 路径导航 Composable + * 提供路径输入、历史记录、前进/后退等功能 + */ + +import { ref, watch, computed } from 'vue' +import { STORAGE_KEYS } from '@/utils/constants' +import type { PathHistory } from '@/types/file-system' + +export interface UsePathNavigationOptions { + onListDirectory?: (path: string) => Promise + initialPath?: string +} + +/** + * 从 localStorage 恢复上次的路径 + */ +const restoreLastPath = (): string | null => { + try { + const lastPath = localStorage.getItem(STORAGE_KEYS.FILESYSTEM.FILE_PATH) + return lastPath + } catch (error) { + console.error('恢复路径失败:', error) + return null + } +} + +/** + * 保存路径到 localStorage + */ +const saveLastPath = (path: string) => { + try { + localStorage.setItem(STORAGE_KEYS.FILESYSTEM.FILE_PATH, path) + } catch (error) { + console.error('保存路径失败:', error) + } +} + +export function usePathNavigation(options: UsePathNavigationOptions = {}) { + const { onListDirectory, initialPath = '' } = options + + // 尝试恢复上次的路径,如果没有则使用初始路径 + const savedPath = restoreLastPath() + const filePath = ref(savedPath || initialPath) + + // 历史记录 + const history = ref({ + paths: [], + currentIndex: -1 + }) + + /** + * 导航到指定路径(带错误处理) + */ + const navigate = async (path: string) => { + if (!path || path === filePath.value) return + + try { + // 路径规范化 + const normalizedPath = normalizePath(path) + filePath.value = normalizedPath + + // 添加到历史记录 + addToHistory(normalizedPath) + + // 触发目录列出 + if (onListDirectory) { + await onListDirectory(normalizedPath) + } + } catch (error) { + console.error('导航失败:', error) + throw error + } + } + + /** + * 添加到历史记录 + */ + const addToHistory = (path: string) => { + const { paths, currentIndex } = history.value + + // 如果当前不在历史记录末尾,删除当前位置之后的所有记录 + if (currentIndex < paths.length - 1) { + history.value.paths = paths.slice(0, currentIndex + 1) + } + + // 避免重复添加相同路径 + const lastPath = history.value.paths[history.value.paths.length - 1] + if (lastPath !== path) { + history.value.paths.push(path) + history.value.currentIndex = history.value.paths.length - 1 + } + } + + /** + * 后退(带错误处理) + */ + const back = async () => { + const { paths, currentIndex } = history.value + + if (currentIndex <= 0) return + + try { + const newIndex = currentIndex - 1 + history.value.currentIndex = newIndex + filePath.value = paths[newIndex] + + if (onListDirectory) { + await onListDirectory(paths[newIndex]) + } + } catch (error) { + console.error('后退失败:', error) + throw error + } + } + + /** + * 前进(带错误处理) + */ + const forward = async () => { + const { paths, currentIndex } = history.value + + if (currentIndex >= paths.length - 1) return + + try { + const newIndex = currentIndex + 1 + history.value.currentIndex = newIndex + filePath.value = paths[newIndex] + + if (onListDirectory) { + await onListDirectory(paths[newIndex]) + } + } catch (error) { + console.error('前进失败:', error) + throw error + } + } + + /** + * 路径输入选择 + */ + const onPathSelect = (value: string) => { + navigate(value) + } + + /** + * 路径输入回车 + */ + const onPathEnter = (value: string) => { + navigate(value) + } + + /** + * 浏览目录(双击或回车) + */ + const browseDirectory = async (path: string) => { + await navigate(path) + } + + /** + * 获取父目录路径 + */ + const getParentPath = (path: string): string => { + const separator = path.includes('\\') ? '\\' : '/' + const lastSeparator = path.lastIndexOf(separator) + return lastSeparator > 0 ? path.substring(0, lastSeparator) : path + } + + /** + * 上级目录 + */ + const goUp = async () => { + const parentPath = getParentPath(filePath.value) + if (parentPath !== filePath.value) { + await navigate(parentPath) + } + } + + /** + * 路径规范化(统一分隔符) + */ + const normalizePath = (path: string): string => { + if (!path) return '' + return path.replace(/\\/g, '/') + } + + /** + * 判断是否可以后退 + */ + const canGoBack = computed(() => { + return history.value.currentIndex > 0 + }) + + /** + * 判断是否可以前进 + */ + const canGoForward = computed(() => { + return history.value.currentIndex < history.value.paths.length - 1 + }) + + /** + * 获取历史记录列表(用于自动完成) + */ + const getPathHistory = computed(() => { + return history.value.paths.slice().reverse() // 最新的在前 + }) + + // 监听路径变化,自动保存到 localStorage + watch(filePath, (newPath) => { + if (newPath) { + saveLastPath(newPath) + } + }) + + return { + // 状态 + filePath, + history, + + // 导航方法 + navigate, + back, + forward, + goUp, + browseDirectory, + + // 事件处理 + onPathSelect, + onPathEnter, + + // 工具方法 + getParentPath, + normalizePath, + + // 计算属性 + canGoBack, + canGoForward, + getPathHistory + } +} + +// 导出类型(用于外部使用) +export type { PathHistory } diff --git a/web/src/components/FileSystem/index-simple.vue b/web/src/components/FileSystem/index-simple.vue new file mode 100644 index 0000000..0afe3e7 --- /dev/null +++ b/web/src/components/FileSystem/index-simple.vue @@ -0,0 +1,198 @@ + + + + + diff --git a/web/src/components/FileSystem/index.vue b/web/src/components/FileSystem/index.vue new file mode 100644 index 0000000..8cae148 --- /dev/null +++ b/web/src/components/FileSystem/index.vue @@ -0,0 +1,1288 @@ + + + + + diff --git a/web/src/composables/useFileEdit.js b/web/src/composables/useFileEdit.js new file mode 100644 index 0000000..2a9ed0f --- /dev/null +++ b/web/src/composables/useFileEdit.js @@ -0,0 +1,369 @@ +/** + * 文件编辑和保存逻辑 composable + * + * @module composables/useFileEdit + * @description 封装文件编辑、保存、草稿管理等逻辑 + */ + +import { ref, computed, watch } from 'vue' +import { Message, Modal } from '@arco-design/web-vue' +import { STORAGE_KEYS } from '@/utils/constants' + +/** + * 草稿存储键 + */ +const DRAFT_STORAGE_KEY = 'filesystem_draft_content' + +/** + * 文件编辑 composable + * @param {Object} options - 配置选项 + * @param {Ref} options.filePath - 当前文件路径 + * @param {Ref} options.fileContent - 文件内容 + * @param {Function} options.onWriteFile - 写入文件的函数 + * @param {Function} options.onReset - 重置内容的函数 + * @returns {UseFileEditReturn} 文件编辑操作 API + */ +export function useFileEdit(options = {}) { + const { + filePath, + fileContent, + onWriteFile, + onReset, + } = options + + // ========== 编辑状态 ========== + + /** + * 是否正在保存 + * @type {Ref} + */ + const isSaving = ref(false) + + /** + * 是否是快捷键触发的保存 + * @type {Ref} + */ + const isShortcutSave = ref(false) + + /** + * 保存成功提示消息 + * @type {Ref} + */ + const saveSuccessMessage = ref('') + + /** + * 原始文件内容(用于检测变更) + * @type {Ref} + */ + const originalContent = ref('') + + /** + * 是否为编辑模式 + * @type {Ref} + */ + const isEditMode = ref(localStorage.getItem(STORAGE_KEYS.FILESYSTEM.EDIT_MODE) === 'true') + + // ========== 计算属性 ========== + + /** + * 文件内容是否已修改 + */ + const isFileModified = computed(() => { + return originalContent.value !== undefined && + originalContent.value !== fileContent.value + }) + + /** + * 内容是否发生变化(用于按钮禁用判断) + */ + const contentChanged = computed(() => { + return fileContent.value !== '' && + fileContent.value !== originalContent.value + }) + + /** + * 是否可以保存文件 + */ + const canSaveFile = computed(() => { + return isEditMode.value && contentChanged.value + }) + + /** + * 是否可以重置内容 + */ + const canResetContent = computed(() => { + return isEditMode.value && + contentChanged.value && + originalContent.value !== undefined + }) + + // ========== 草稿管理 ========== + + /** + * 保存草稿到 localStorage + */ + const saveDraft = () => { + try { + const draft = { + content: fileContent.value, + path: filePath.value, + timestamp: Date.now(), + } + localStorage.setItem(DRAFT_STORAGE_KEY, JSON.stringify(draft)) + localStorage.setItem(DRAFT_STORAGE_KEY + '_time', Date.now().toString()) + } catch (error) { + console.warn('[saveDraft] 保存草稿失败:', error) + } + } + + /** + * 清除草稿 + */ + const clearDraft = () => { + try { + localStorage.removeItem(DRAFT_STORAGE_KEY) + localStorage.removeItem(DRAFT_STORAGE_KEY + '_time') + } catch (error) { + console.warn('[clearDraft] 清除草稿失败:', error) + } + } + + /** + * 加载草稿 + * @returns {Object|null} 草稿数据 + */ + const loadDraft = () => { + try { + const draftStr = localStorage.getItem(DRAFT_STORAGE_KEY) + if (!draftStr) return null + + const draft = JSON.parse(draftStr) + + // 检查草稿是否过期(24小时) + const timeStr = localStorage.getItem(DRAFT_STORAGE_KEY + '_time') + if (timeStr) { + const time = parseInt(timeStr, 10) + const now = Date.now() + const hours = (now - time) / (1000 * 60 * 60) + + if (hours > 24) { + clearDraft() + return null + } + } + + return draft + } catch (error) { + console.warn('[loadDraft] 加载草稿失败:', error) + return null + } + } + + // ========== 保存操作 ========== + + /** + * 显示手动保存对话框 + * @param {boolean} isShortcut - 是否是快捷键触发 + */ + const showManualSaveDialog = (isShortcut) => { + isShortcutSave.value = isShortcut + + Modal.confirm({ + title: '保存文件', + content: `确定要保存文件 ${filePath.value} 吗?`, + okText: '保存', + cancelText: '取消', + onOk: () => { + saveToFile(filePath.value, getFileName(filePath.value), isShortcut) + }, + }) + } + + /** + * 保存到文件 + * @param {string} targetPath - 目标路径 + * @param {string} fileName - 文件名 + * @param {boolean} isShortcut - 是否是快捷键触发 + * @returns {Promise} 是否成功 + */ + const saveToFile = async (targetPath, fileName, isShortcut) => { + isSaving.value = true + try { + const success = await onWriteFile(fileContent.value, targetPath, fileName, isShortcut) + + if (success) { + originalContent.value = fileContent.value + clearDraft() + } + + return success + } finally { + isSaving.value = false + } + } + + /** + * 处理保存内容 + * @returns {Promise} 是否成功 + */ + const handleSaveContent = async () => { + if (!canSaveFile.value) { + return false + } + + return await saveToFile(filePath.value, getFileName(filePath.value), false) + } + + /** + * 另存为 + */ + const handleSaveAs = async () => { + try { + // 简单实现:使用 prompt 获取路径 + const targetPath = prompt('请输入保存路径:', filePath.value) + + if (!targetPath) { + return false + } + + const fileName = getFileName(targetPath) + return await saveToFile(targetPath, fileName, false) + } catch (error) { + Message.error(`保存对话框失败: ${error.message || error}`) + return false + } + } + + /** + * 处理写入文件(快捷键或按钮) + * @param {boolean} isShortcut - 是否是快捷键触发 + * @returns {Promise} 是否成功 + */ + const handleWriteFile = async (isShortcut = false) => { + if (!fileContent.value || !filePath.value) { + Message.warning('没有可保存的内容') + return false + } + + // 如果内容未修改,快捷键保存时静默返回 + if (!isFileModified.value && isShortcut) { + return false + } + + // 快捷键:静默保存 + if (isShortcut) { + return await saveToFile(filePath.value, getFileName(filePath.value), true) + } + + // 按钮:显示确认对话框 + showManualSaveDialog(false) + return false + } + + // ========== 重置操作 ========== + + /** + * 重置内容到原始状态 + */ + const resetContent = () => { + if (onReset) { + onReset() + } else { + fileContent.value = originalContent.value + } + } + + // ========== 编辑模式切换 ========== + + /** + * 切换编辑模式 + */ + const toggleEditMode = () => { + isEditMode.value = !isEditMode.value + + // 持久化 + try { + localStorage.setItem(STORAGE_KEYS.FILESYSTEM.EDIT_MODE, isEditMode.value.toString()) + } catch (e) { + console.warn('[toggleEditMode] 保存编辑模式失败:', e) + } + + // 进入编辑模式时,记录原始内容 + if (isEditMode.value) { + originalContent.value = fileContent.value + } + } + + // ========== 工具函数 ========== + + /** + * 从路径获取文件名 + * @param {string} path - 文件路径 + * @returns {string} 文件名 + */ + const getFileName = (path) => { + if (!path) return '' + const parts = path.split(/[/\\]/) + return parts[parts.length - 1] || path + } + + // ========== 监听内容变化 ========== + + /** + * 监听文件内容变化,自动保存草稿 + */ + watch(fileContent, () => { + if (fileContent.value && fileContent.value !== originalContent.value) { + saveDraft() + } + }) + + /** + * 监听文件路径变化,更新原始内容 + */ + watch(filePath, () => { + originalContent.value = fileContent.value + }) + + return { + // 状态 + isSaving, + isShortcutSave, + saveSuccessMessage, + originalContent, + isEditMode, + isFileModified, + canSaveFile, + canResetContent, + + // 方法 + saveDraft, + clearDraft, + loadDraft, + handleSaveContent, + handleSaveAs, + handleWriteFile, + resetContent, + toggleEditMode, + } +} + +/** + * @typedef {Object} UseFileEditReturn + * @property {Ref} isSaving - 是否正在保存 + * @property {Ref} isShortcutSave - 是否是快捷键触发 + * @property {Ref} saveSuccessMessage - 保存成功提示消息 + * @property {Ref} originalContent - 原始文件内容 + * @property {Ref} isEditMode - 是否为编辑模式 + * @property {ComputedRef} isFileModified - 文件内容是否已修改 + * @property {ComputedRef} canSaveFile - 是否可以保存文件 + * @property {ComputedRef} canResetContent - 是否可以重置内容 + * @property {Function} saveDraft - 保存草稿 + * @property {Function} clearDraft - 清除草稿 + * @property {Function} loadDraft - 加载草稿 + * @property {Function} handleSaveContent - 处理保存内容 + * @property {Function} handleSaveAs - 另存为 + * @property {Function} handleWriteFile - 处理写入文件 + * @property {Function} resetContent - 重置内容 + * @property {Function} toggleEditMode - 切换编辑模式 + */ diff --git a/web/src/composables/useFilePreview.js b/web/src/composables/useFilePreview.js new file mode 100644 index 0000000..9384f08 --- /dev/null +++ b/web/src/composables/useFilePreview.js @@ -0,0 +1,612 @@ +/** + * 文件预览逻辑 composable + * + * @module composables/useFilePreview + * @description 封装文件预览、HTML/Markdown 渲染、二进制文件信息显示等逻辑 + */ + +import { ref, computed } from 'vue' +import { marked } from '@/utils/markedExtensions' +import { FILE_EXTENSIONS, FILE_SIZE_THRESHOLDS } from '@/utils/constants' +import { getExt } from '@/utils/fileHelpers' +import { debugLog, debugWarn, debugError } from '@/utils/debugLog' + +/** + * 文件预览 composable + * @param {Object} options - 配置选项 + * @param {Ref} options.filePath - 当前文件路径 + * @param {Ref} options.fileContent - 文件内容 + * @param {Ref} options.fileList - 文件列表 + * @param {Function} options.onReadFile - 读取文件的函数 + * @returns {UseFilePreviewReturn} 文件预览操作 API + */ +export function useFilePreview(options = {}) { + const { + filePath, + fileContent, + fileList, + onReadFile, + } = options + + // ========== 预览状态 ========== + + /** + * 预览 URL + * @type {Ref} + */ + const previewUrl = ref('') + + /** + * 文件服务器URL + * @type {Ref} + */ + const fileServerURL = ref('http://localhost:18765') + + /** + * 渲染后的 HTML/Markdown 内容 + * @type {Ref} + */ + const rendered = ref('') + + /** + * 图片加载状态 + * @type {Ref} + */ + const imageLoading = ref(false) + + /** + * 图片宽度 + * @type {Ref} + */ + const imageWidth = ref(0) + + /** + * 图片高度 + * @type {Ref} + */ + const imageHeight = ref(0) + + /** + * 是否显示图片预览 + * @type {Ref} + */ + const isImageView = ref(false) + + /** + * 是否显示视频预览 + * @type {Ref} + */ + const isVideoView = ref(false) + + /** + * 是否显示音频预览 + * @type {Ref} + */ + const isAudioView = ref(false) + + /** + * 是否为 PDF 文件 + * @type {Ref} + */ + const isPdfFile = ref(false) + + /** + * 是否为 HTML 文件 + * @type {Ref} + */ + const isHtmlFile = ref(false) + + /** + * 是否为 Markdown 文件 + * @type {Ref} + */ + const isMarkdownFile = ref(false) + + /** + * 是否为二进制文件信息展示 + * @type {Ref} + */ + const isBinaryFile = ref(false) + + /** + * HTML 预览的 blob URL + * @type {Ref} + */ + const htmlPreviewUrl = ref('') + + // ========== 计算属性 ========== + + /** + * 当前文件名 + */ + const currentFileName = computed(() => { + if (!filePath.value) return '' + const pathStr = typeof filePath.value === 'string' ? filePath.value : String(filePath.value || '') + const parts = pathStr.split(/[/\\]/) + return parts[parts.length - 1] + }) + + /** + * 当前文件完整路径 + */ + const currentFileFullPath = computed(() => filePath.value || '') + + /** + * 当前图片尺寸 + */ + const currentImageDimensions = computed(() => { + if (!imageWidth.value || !imageHeight.value) return '' + return `${imageWidth.value}×${imageHeight.value}` + }) + + // ========== 图片预览 ========== + + /** + * 预览图片 + * @param {string} targetPath - 目标路径 + */ + const previewImage = async (targetPath) => { + const pathToPreview = targetPath || filePath.value + if (!pathToPreview) return + + resetPreviewState() + + const ext = getExt(pathToPreview) + if (!FILE_EXTENSIONS.IMAGE.includes(ext)) { + return + } + + imageLoading.value = true + isImageView.value = true + + // 构建预览 URL + const encodedPath = encodeURIComponent(pathToPreview) + previewUrl.value = `${fileServerURL.value}/file?path=${encodedPath}` + } + + /** + * 图片加载成功回调 + * @param {Event} e - 加载事件 + */ + const onImageLoad = (e) => { + imageLoading.value = false + imageWidth.value = e.naturalWidth || e.target?.width || 0 + imageHeight.value = e.naturalHeight || e.target?.height || 0 + } + + /** + * 图片加载失败回调 + */ + const onImageError = () => { + imageLoading.value = false + debugWarn('[onImageError] 图片加载失败') + } + + // ========== 视频/音频/PDF 预览 ========== + + /** + * 预览媒体文件(视频/音频/PDF) + * @param {string} mediaType - 媒体类型 ('video' | 'audio' | 'pdf') + * @param {string} targetPath - 目标路径 + */ + const previewMedia = (mediaType, targetPath) => { + const pathToPreview = targetPath || filePath.value + if (!pathToPreview) return + + resetPreviewState() + + const encodedPath = encodeURIComponent(pathToPreview) + previewUrl.value = `${fileServerURL.value}/file?path=${encodedPath}` + + if (mediaType === 'video') { + isVideoView.value = true + } else if (mediaType === 'audio') { + isAudioView.value = true + } else if (mediaType === 'pdf') { + isPdfFile.value = true + } + } + + /** + * 预览视频 + * @param {string} targetPath - 目标路径 + */ + const previewVideo = (targetPath) => previewMedia('video', targetPath) + + /** + * 预览音频 + * @param {string} targetPath - 目标路径 + */ + const previewAudio = (targetPath) => previewMedia('audio', targetPath) + + /** + * 预览 PDF + * @param {string} targetPath - 目标路径 + */ + const previewPdf = (targetPath) => previewMedia('pdf', targetPath) + + // ========== HTML 预览 ========== + + /** + * 提取 HTML 文件中的样式 + * @param {string} htmlContent - HTML 内容 + * @param {string} basePath - 基础路径 + * @returns {Promise} 提取的 CSS 样式 + */ + const extractHtmlStyles = async (htmlContent, basePath) => { + const linkRegex = /]*href=(["'])([^"']+)\1[^>]*>/gi + const links = [...htmlContent.matchAll(linkRegex)] + + if (links.length === 0) return '' + + let linkCount = 0 + const styles = [] + + for (const match of links) { + const linkTag = match[0] + const hrefMatch = match[2]?.match(/^https?:\/\//i) + + const fullTag = match[0] + const href = match[2] + + debugLog(`[extractHtmlStyles] 发现第 ${linkCount} 个 link 标签:`, fullTag) + + const cssPath = href?.replace(/^\.\//, '').replace(/^\//, '') + debugLog('[extractHtmlStyles] 解析后 CSS 路径:', cssPath) + + if (hrefMatch) { + debugLog('[extractHtmlStyles] 跳过外部 CSS:', hrefMatch[1]) + continue + } + + debugLog('[extractHtmlStyles] 正在读取 CSS 文件:', cssPath) + + try { + // 从 HTML 文件所在目录读取 CSS + const cssFullPath = basePath + '/' + cssPath + const cssContent = await onReadFile(cssFullPath) + + if (cssContent) { + const cssSize = cssContent.length + debugLog(`[extractHtmlStyles] 成功读取并转换 CSS: ${cssSize} 字符`) + + // 转换 CSS 中的 URL 为 base64 + const convertedCss = await convertCssUrls(cssContent, basePath) + styles.push(convertedCss) + } + } catch (error) { + debugWarn('[extractHtmlStyles] 无法读取 CSS:', cssPath, error.message) + } + + linkCount++ + } + + debugLog(`处理完成: 找到 ${linkCount} 个 link 标签, 成功提取 ${styles.length} 个 CSS 文件`) + debugLog(`提取的 CSS 总大小: ${styles.join('\n\n').length} 字符`) + + return styles.join('\n\n') + } + + /** + * 转换 CSS 中的相对 URL 为 base64 + * @param {string} css - CSS 内容 + * @param {string} basePath - 基础路径 + * @returns {Promise} 转换后的 CSS + */ + const convertCssUrls = async (css, basePath) => { + const urlRegex = /url\((["']?)([^"')]+)\1\)/gi + + return css.replace(urlRegex, async (match, quote, url) => { + // 跳过 data: URLs 和绝对 URLs + if (url.startsWith('data:') || /^https?:\/\//i.test(url)) { + return match + } + + try { + const imagePath = basePath + '/' + url.replace(/^\.\//, '') + const base64 = await fileToBase64(imagePath) + + debugLog(`[convertCssUrls] ${url} -> base64`) + + return `url("data:image/${getExt(imagePath)};base64,${base64}")` + } catch (err) { + debugWarn('[convertCssUrls] 失败:', imagePath, err.message) + return match + } + }) + } + + /** + * 将文件转换为 base64 + * @param {string} filePath - 文件路径 + * @returns {Promise} base64 字符串 + */ + const fileToBase64 = async (filePath) => { + // 这里需要调用实际的文件读取 API + // 简化实现,返回空字符串 + return '' + } + + /** + * 预览 HTML 文件 + * @param {string} targetPath - 目标路径 + */ + const previewHtml = async (targetPath) => { + const pathToPreview = targetPath || filePath.value + if (!pathToPreview) return + + resetPreviewState() + isHtmlFile.value = true + + debugLog('开始处理 CSS') + debugLog('HTML 文件路径:', pathToPreview) + + const basePath = pathToPreview.replace(/[^/\\]+$/, '') + + try { + let htmlContent = fileContent.value + + // 提取并转换 CSS + const styles = await extractHtmlStyles(htmlContent, basePath) + + // 转换图片引用 + const imgRegex = /]*src=(["'])([^"']+)\1[^>]*>/gi + htmlContent = htmlContent.replace(imgRegex, (match, quote, src) => { + // 跳过 data: URLs 和绝对 URLs + if (src.startsWith('data:') || /^https?:\/\//i.test(src)) { + return match + } + + debugLog(`[previewHtml] ${src} -> base64`) + + // 转换为绝对路径 + const imagePath = basePath + src.replace(/^\.\//, '').replace(/^\//, '') + + // 简化实现:使用 fileServerURL + const encodedPath = encodeURIComponent(imagePath) + const newSrc = `${fileServerURL.value}/file?path=${encodedPath}` + + return match.replace(src, newSrc) + }) + + // 移除本地脚本 + htmlContent = htmlContent.replace(/]*src=(["'])[^"']+\1[^>]*>/gi, (match, quote, src) => { + const srcMatch = match.match(/src=(["'])([^"']+)\1/i) + if (srcMatch) { + const srcValue = srcMatch[2] + if (!srcValue.startsWith('http')) { + debugLog(`[previewHtml] 移除本地脚本: ${srcValue}`) + return '' + } + } + return match + }) + + // 清理遗漏的 CSS 链接 + htmlContent = htmlContent.replace(/]*rel=(["'])stylesheet\1[^>]*>/gi, (match) => { + const hrefMatch = match.match(/href=(["'])([^"']+)\1/i) + if (hrefMatch && !/^https?:\/\//i.test(hrefMatch[2])) { + debugLog(`[previewHtml] 清理遗漏的CSS链接: ${hrefMatch[2]}`) + return '' + } + return match + }) + + // 构建最终 HTML + const finalHtml = ` + + + + + + + + ${htmlContent} + + + ` + + // 创建 blob URL + const blob = new Blob([finalHtml], { type: 'text/html' }) + htmlPreviewUrl.value = URL.createObjectURL(blob) + rendered.value = finalHtml + } catch (error) { + debugError('[previewHtml] 处理失败:', error) + } + } + + // ========== Markdown 预览 ========== + + /** + * 预览 Markdown 文件 + * @param {string} targetPath - 目标路径 + */ + const previewMarkdown = async (targetPath) => { + const pathToPreview = targetPath || filePath.value + if (!pathToPreview) return + + resetPreviewState() + isMarkdownFile.value = true + + try { + renderMarkdown(fileContent.value) + } catch (error) { + debugError('[renderMarkdown] 解析失败:', error) + } + } + + /** + * 渲染 Markdown + * @param {string} markdown - Markdown 内容 + */ + const renderMarkdown = (markdown) => { + try { + rendered.value = marked(markdown) + } catch (error) { + debugError('[renderMarkdown] 解析失败:', error) + rendered.value = '

Markdown 解析失败

' + } + } + + // ========== 二进制文件信息 ========== + + /** + * 获取字符串显示宽度(用于对齐) + * @param {string} str - 字符串 + * @returns {number} 显示宽度 + */ + const getDisplayWidth = (str) => { + let width = 0 + for (const char of str) { + if (char.match(/[\u4e00-\u9fa5]/)) { + width += 2 + } else { + width += 1 + } + } + return width + } + + /** + * 按显示宽度填充 + * @param {string} str - 字符串 + * @param {number} targetWidth - 目标宽度 + * @returns {string} 填充后的字符串 + */ + const padByDisplayWidth = (str, targetWidth) => { + const currentWidth = getDisplayWidth(str) + const padding = Math.max(0, targetWidth - currentWidth) + return str + ' '.repeat(padding) + } + + /** + * 显示二进制文件信息 + * @param {string} ext - 文件扩展名 + * @param {string} filePathParam - 文件路径 + */ + const showBinaryFileInfo = (ext, filePathParam) => { + resetPreviewState() + isBinaryFile.value = true + + const file = fileList.value.find(f => f.path === filePathParam) + if (!file) return + + const extUpper = ext.toUpperCase() + const extPadded = padByDisplayWidth(extUpper, 6) + const sizeMB = (file.size / 1024 / 1024).toFixed(2) + const sizeStr = `${sizeMB} MB`.padStart(10, ' ') + + rendered.value = ` +
+

+ ${extPadded} 文件 + ${sizeStr} +

+

${file.name}

+
+ ` + } + + // ========== 工具函数 ========== + + /** + * 重置预览状态 + */ + const resetPreviewState = () => { + isImageView.value = false + isVideoView.value = false + isAudioView.value = false + isPdfFile.value = false + isHtmlFile.value = false + isMarkdownFile.value = false + isBinaryFile.value = false + + if (htmlPreviewUrl.value) { + URL.revokeObjectURL(htmlPreviewUrl.value) + htmlPreviewUrl.value = '' + } + + previewUrl.value = '' + rendered.value = '' + imageWidth.value = 0 + imageHeight.value = 0 + } + + /** + * 判断是否为 Office 文件 + * @param {string} fileName - 文件名 + * @returns {boolean} + */ + const isOfficeFile = (fileName) => { + const ext = getExt(fileName).toLowerCase() + return ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'].includes(ext) + } + + return { + // 状态 + previewUrl, + fileServerURL, + rendered, + imageLoading, + imageWidth, + imageHeight, + isImageView, + isVideoView, + isAudioView, + isPdfFile, + isHtmlFile, + isMarkdownFile, + isBinaryFile, + htmlPreviewUrl, + currentFileName, + currentFileFullPath, + currentImageDimensions, + + // 方法 + previewImage, + previewVideo, + previewAudio, + previewPdf, + previewHtml, + previewMarkdown, + renderMarkdown, + showBinaryFileInfo, + onImageLoad, + onImageError, + isOfficeFile, + resetPreviewState, + } +} + +/** + * @typedef {Object} UseFilePreviewReturn + * @property {Ref} previewUrl - 预览 URL + * @property {Ref} fileServerURL - 文件服务器URL + * @property {Ref} rendered - 渲染后的内容 + * @property {Ref} imageLoading - 图片加载状态 + * @property {Ref} imageWidth - 图片宽度 + * @property {Ref} imageHeight - 图片高度 + * @property {Ref} isImageView - 是否显示图片预览 + * @property {Ref} isVideoView - 是否显示视频预览 + * @property {Ref} isAudioView - 是否显示音频预览 + * @property {Ref} isPdfFile - 是否为 PDF 文件 + * @property {Ref} isHtmlFile - 是否为 HTML 文件 + * @property {Ref} isMarkdownFile - 是否为 Markdown 文件 + * @property {Ref} isBinaryFile - 是否为二进制文件信息展示 + * @property {Ref} htmlPreviewUrl - HTML 预览的 blob URL + * @property {ComputedRef} currentFileName - 当前文件名 + * @property {ComputedRef} currentFileFullPath - 当前文件完整路径 + * @property {ComputedRef} currentImageDimensions - 当前图片尺寸 + * @property {Function} previewImage - 预览图片 + * @property {Function} previewVideo - 预览视频 + * @property {Function} previewAudio - 预览音频 + * @property {Function} previewPdf - 预览 PDF + * @property {Function} previewHtml - 预览 HTML + * @property {Function} previewMarkdown - 预览 Markdown + * @property {Function} renderMarkdown - 渲染 Markdown + * @property {Function} showBinaryFileInfo - 显示二进制文件信息 + * @property {Function} onImageLoad - 图片加载成功回调 + * @property {Function} onImageError - 图片加载失败回调 + * @property {Function} isOfficeFile - 判断是否为 Office 文件 + * @property {Function} resetPreviewState - 重置预览状态 + */ diff --git a/web/src/composables/useNavigation.js b/web/src/composables/useNavigation.js new file mode 100644 index 0000000..060b887 --- /dev/null +++ b/web/src/composables/useNavigation.js @@ -0,0 +1,273 @@ +/** + * 导航和路径管理 composable + * + * @module composables/useNavigation + * @description 封装文件系统的导航历史、路径操作等逻辑 + */ + +import { ref, computed } from 'vue' +import { Message } from '@arco-design/web-vue' + +/** + * 路径历史 localStorage 键 + */ +const STORAGE_KEY_PATH_HISTORY = 'app-filesystem-path-history' + +/** + * 导航管理 composable + * @param {Object} options - 配置选项 + * @param {Ref} options.filePath - 当前路径 ref + * @param {Function} options.onListDirectory - 列出目录的函数 + * @param {Function} options.onExitZipMode - 退出 ZIP 模式的函数 + * @returns {UseNavigationReturn} 导航操作 API + */ +export function useNavigation(options = {}) { + const { + filePath, + onListDirectory, + onExitZipMode, + } = options + + // ========== 导航历史记录(支持后退/前进) ========== + + /** + * 导航历史栈 + * @type {Ref>} + */ + const navHistory = ref([]) + + /** + * 当前在历史栈中的位置 + * @type {Ref} + */ + const navIndex = ref(-1) + + /** + * 是否正在导航(防止重复记录) + * @type {Ref} + */ + const isNavigating = ref(false) + + /** + * 路径历史记录(用于下拉列表) + * @type {Ref>} + */ + const pathHistory = ref([]) + + // ========== 计算属性 ========== + + /** + * 是否可以后退 + */ + const canGoBack = computed(() => navIndex.value > 0) + + /** + * 是否可以前进 + */ + const canGoForward = computed(() => navIndex.value < navHistory.value.length - 1) + + // ========== 导航操作 ========== + + /** + * 添加到路径历史记录 + * @param {string} path - 路径 + */ + const addToHistory = (path) => { + if (!path || path === filePath.value) return + + // 去重:如果路径已在历史中,先删除 + const index = pathHistory.value.indexOf(path) + if (index > -1) { + pathHistory.value.splice(index, 1) + } + + // 添加到开头 + pathHistory.value.unshift(path) + + // 限制历史记录数量 + if (pathHistory.value.length > 50) { + pathHistory.value = pathHistory.value.slice(0, 50) + } + + // 持久化 + try { + localStorage.setItem(STORAGE_KEY_PATH_HISTORY, JSON.stringify(pathHistory.value)) + } catch (e) { + // 忽略 localStorage 错误 + } + } + + /** + * 推送到导航历史栈 + * @param {string} path - 路径 + */ + const pushNav = (path) => { + if (isNavigating.value) { + return + } + + // 如果当前位置不在历史末尾,删除后续历史 + if (navIndex.value < navHistory.value.length - 1) { + navHistory.value = navHistory.value.slice(0, navIndex.value + 1) + } + + // 添加到历史 + navHistory.value.push(path) + navIndex.value = navHistory.value.length - 1 + + // 同时添加到路径历史 + addToHistory(path) + } + + /** + * 后退 + * @returns {Promise} 是否成功 + */ + const goBack = async () => { + if (!canGoBack.value) { + return false + } + + isNavigating.value = true + try { + navIndex.value-- + const path = navHistory.value[navIndex.value] + await onListDirectory(path) + return true + } catch (error) { + Message.error(`后退失败: ${error.message || error}`) + return false + } finally { + isNavigating.value = false + } + } + + /** + * 前进 + * @returns {Promise} 是否成功 + */ + const goForward = async () => { + if (!canGoForward.value) { + return false + } + + isNavigating.value = true + try { + navIndex.value++ + const path = navHistory.value[navIndex.value] + await onListDirectory(path) + return true + } catch (error) { + Message.error(`前进失败: ${error.message || error}`) + return false + } finally { + isNavigating.value = false + } + } + + // ========== 路径操作 ========== + + /** + * 路径选择(从下拉列表) + * @param {string} value - 选中的路径 + */ + const onPathSelect = (value) => { + if (value && value !== filePath.value) { + goToPath(value) + } + } + + /** + * 路径输入框回车事件 + */ + const onPathEnter = () => { + const path = filePath.value?.trim() + if (path) { + goToPath(path) + } + } + + /** + * 跳转到指定路径 + * @param {string} path - 目标路径 + * @returns {Promise} 是否成功 + */ + const goToPath = async (path) => { + if (!path) { + return false + } + + // 退出 ZIP 模式 + if (onExitZipMode) { + onExitZipMode() + } + + return await onListDirectory(path) + } + + /** + * 浏览目录(打开系统文件选择对话框) + */ + const browseDirectory = async () => { + Message.info('请手动输入目录路径') + } + + // ========== 初始化 ========== + + /** + * 加载路径历史记录 + */ + const loadPathHistory = () => { + try { + const saved = localStorage.getItem(STORAGE_KEY_PATH_HISTORY) + if (saved) { + pathHistory.value = JSON.parse(saved) + } + } catch (e) { + console.warn('[useNavigation] 加载路径历史失败:', e) + } + } + + // 初始化 + loadPathHistory() + + return { + // 状态 + navHistory, + navIndex, + isNavigating, + pathHistory, + canGoBack, + canGoForward, + + // 方法 + addToHistory, + pushNav, + goBack, + goForward, + onPathSelect, + onPathEnter, + goToPath, + browseDirectory, + loadPathHistory, + } +} + +/** + * @typedef {Object} UseNavigationReturn + * @property {Ref>} navHistory - 导航历史栈 + * @property {Ref} navIndex - 当前在历史栈中的位置 + * @property {Ref} isNavigating - 是否正在导航 + * @property {Ref>} pathHistory - 路径历史记录(下拉列表) + * @property {ComputedRef} canGoBack - 是否可以后退 + * @property {ComputedRef} canGoForward - 是否可以前进 + * @property {Function} addToHistory - 添加到路径历史记录 + * @property {Function} pushNav - 推送到导航历史栈 + * @property {Function} goBack - 后退 + * @property {Function} goForward - 前进 + * @property {Function} onPathSelect - 路径选择 + * @property {Function} onPathEnter - 路径输入框回车事件 + * @property {Function} goToPath - 跳转到指定路径 + * @property {Function} browseDirectory - 浏览目录 + * @property {Function} loadPathHistory - 加载路径历史记录 + */ diff --git a/web/src/types/file-system.ts b/web/src/types/file-system.ts new file mode 100644 index 0000000..30e0d71 --- /dev/null +++ b/web/src/types/file-system.ts @@ -0,0 +1,287 @@ +/** + * 文件系统类型定义 + * @module file-system + */ + +/** + * 文件项 + */ +export interface FileItem { + /** 文件名 */ + name: string + /** 完整路径 */ + path: string + /** 文件大小(字节) */ + size: number + /** 是否为目录 */ + is_dir: boolean + /** 修改时间 */ + modified_time?: string +} + +/** + * 收藏文件 + */ +export interface FavoriteFile extends FileItem { + /** 添加时间(时间戳) */ + addedAt: number +} + +/** + * 文件类型枚举 + */ +export enum FileType { + /** 图片 */ + Image = 'image', + /** 视频 */ + Video = 'video', + /** 音频 */ + Audio = 'audio', + /** PDF */ + Pdf = 'pdf', + /** HTML */ + Html = 'html', + /** Markdown */ + Markdown = 'markdown', + /** 代码 */ + Code = 'code', + /** 文本 */ + Text = 'text', + /** 二进制 */ + Binary = 'binary' +} + +/** + * 拖拽状态 + */ +export interface DraggingState { + /** 是否正在拖拽 */ + isDragging: boolean + /** 被拖拽项的索引 */ + draggedIndex: number + /** 按下的项索引 */ + pressedIndex: number +} + +/** + * 面板宽度配置 + */ +export interface PanelWidth { + /** 左侧面板宽度(百分比) */ + left: number + /** 右侧面板宽度(百分比) */ + right: number +} + +/** + * 快捷路径 + */ +export interface ShortcutPath { + /** 显示名称 */ + name: string + /** 路径 */ + path: string +} + +/** + * 工具栏配置 + */ +export interface ToolbarConfig { + /** 当前文件路径 */ + filePath: string + /** 路径历史记录 */ + pathHistory: string[] + /** 常用路径列表 */ + commonPaths: ShortcutPath[] + /** 是否在 ZIP 浏览模式 */ + isBrowsingZip: boolean + /** 显示路径(ZIP 模式下) */ + displayPath: string + /** ZIP 文件名 */ + zipFileName: string + /** ZIP 面包屑 */ + zipBreadcrumbs: ZipBreadcrumbItem[] + /** 文件加载中 */ + fileLoading: boolean + /** 是否显示侧边栏 */ + showSidebar: boolean +} + +/** + * 侧边栏配置 + */ +export interface SidebarConfig { + /** 是否可见 */ + visible: boolean + /** 收藏文件列表 */ + favoriteFiles: FavoriteFile[] + /** 拖拽状态 */ + draggingState: DraggingState +} + +/** + * 文件列表面板配置 + */ +export interface FileListPanelConfig { + /** 文件列表 */ + fileList: FileItem[] + /** 文件加载中 */ + fileLoading: boolean + /** 选中的文件项 */ + selectedFileItem: FileItem | null + /** 正在编辑的文件路径 */ + editingFilePath: string + /** 编辑中的文件名 */ + editingFileName: string +} + +/** + * 文件编辑器面板配置 + */ +export interface FileEditorPanelConfig { + /** 当前文件名 */ + currentFileName: string + /** 当前文件完整路径 */ + currentFileFullPath: string + /** 预览 URL */ + previewUrl: string + /** 文件内容 */ + fileContent: string + /** 渲染后的内容(HTML/Markdown) */ + rendered: string + /** 是否在编辑模式 */ + isEditMode: boolean + /** 文件内容区域高度 */ + fileContentHeight: number + /** 是否为图片视图 */ + isImageView: boolean + /** 是否为视频视图 */ + isVideoView: boolean + /** 是否为音频视图 */ + isAudioView: boolean + /** 是否为 PDF 文件 */ + isPdfFile: boolean + /** 是否为 HTML 文件 */ + isHtmlFile: boolean + /** 是否为 Markdown 文件 */ + isMarkdownFile: boolean + /** 是否可以保存 */ + canSaveFile: boolean + /** 是否可以重置 */ + canResetContent: boolean + /** 是否可以预览 */ + canPreviewFile: boolean + /** 图片加载中 */ + imageLoading: boolean + /** 当前图片尺寸 */ + currentImageDimensions: string + /** 当前文件扩展名 */ + currentFileExtension: string + /** 是否为二进制文件 */ + isBinaryFile: boolean +} + +/** + * 右键菜单上下文类型 + */ +export type ContextMenuContext = 'file-list' | 'editor' | 'empty' + +/** + * 右键菜单配置 + */ +export interface ContextMenuConfig { + /** 是否可见 */ + visible: boolean + /** X 坐标 */ + x: number + /** Y 坐标 */ + y: number + /** 上下文类型 */ + context: ContextMenuContext + /** 选中的文件(file-list 上下文) */ + selectedFile?: FileItem +} + +/** + * 文件操作结果 + */ +export interface FileOperationResult { + /** 是否成功 */ + success: boolean + /** 错误信息 */ + error?: string + /** 数据 */ + data?: any +} + +/** + * 路径导航历史 + */ +export interface PathHistory { + /** 历史记录数组 */ + paths: string[] + /** 当前索引 */ + currentIndex: number +} + +/** + * 文件预览元数据 + */ +export interface FilePreviewMetadata { + /** 宽度 */ + width?: number + /** 高度 */ + height?: number + /** 时长(视频/音频) */ + duration?: number + /** MIME 类型 */ + mimeType?: string +} + +/** + * 编辑器配置 + */ +export interface EditorConfig { + /** 是否可编辑 */ + editable: boolean + /** 是否显示行号 */ + showLineNumbers: boolean + /** 是否显示折叠按钮 */ + showFoldButtons: boolean + /** 主题 */ + theme?: string + /** 字体大小 */ + fontSize?: number +} + +/** + * 文件保存选项 + */ +export interface FileSaveOptions { + /** 是否创建备份 */ + createBackup?: boolean + /** 是否保留原文件时间戳 */ + preserveTimestamp?: boolean +} + +/** + * ZIP 文件信息 + */ +export interface ZipFileInfo { + /** ZIP 文件路径 */ + zipPath: string + /** ZIP 内部的当前路径 */ + currentPath: string + /** ZIP 文件列表 */ + files: FileItem[] +} + +/** + * ZIP 面包屑项 + */ +export interface ZipBreadcrumbItem { + /** 目录名 */ + name: string + /** 目录路径 */ + path: string +} diff --git a/web/src/utils/constants.js b/web/src/utils/constants.js index 35ed718..70b8abd 100644 --- a/web/src/utils/constants.js +++ b/web/src/utils/constants.js @@ -26,6 +26,7 @@ export const STORAGE_KEYS = { SIDEBAR_VISIBLE: 'app-filesystem-sidebar-visible', FAVORITE_FILES: 'app-filesystem-favorite-files', EDIT_MODE: 'app-filesystem-edit-mode', // HTML/Markdown 编辑模式状态 + FILE_DRAFT: 'app-filesystem-file-draft', // 文件草稿 }, // 设备测试模块 @@ -56,7 +57,8 @@ export const FILE_EXTENSIONS = { // 视频文件 VIDEO_BROWSER: ['mp4', 'webm', 'ogg', 'mov', 'm4v'], // 浏览器原生支持 - VIDEO_EXTERNAL: ['avi', 'mkv', 'wmv', 'flv', 'rmvb', '3gp', 'ts', 'mts'], // 需要外部播放器 + VIDEO_EXTERNAL: ['avi', 'mkv', 'wmv', 'flv', 'rmvb', '3gp', 'mts'], // 需要外部播放器(注意:不用 'ts' 避免 TypeScript 冲突) + VIDEO: ['mp4', 'webm', 'ogg', 'mov', 'm4v', 'avi', 'mkv', 'wmv', 'flv', 'rmvb', '3gp', 'mts'], // 所有视频 // 音频文件 AUDIO: ['mp3', 'wav', 'flac', 'aac', 'ogg', 'm4a', 'opus', 'webm'], @@ -69,10 +71,16 @@ export const FILE_EXTENSIONS = { // 代码文件 CODE: [ - 'js', 'ts', 'jsx', 'tsx', 'vue', 'py', 'java', 'c', 'cpp', 'h', 'go', 'rs', 'php', 'rb', 'cs', 'swift', 'kt', - 'scala', 'css', 'scss', 'sass', 'less', 'json', 'xml', 'yaml', 'yml', 'sql', 'sh', 'bat', 'ps1' + 'js', 'ts', 'jsx', 'tsx', 'cts', 'mts', 'cjs', 'mjs', + 'vue', 'py', 'java', 'c', 'cpp', 'h', 'go', 'rs', 'php', 'rb', 'cs', 'swift', 'kt', + 'scala', 'css', 'scss', 'sass', 'less', 'json', 'xml', 'yaml', 'yml', 'sql', 'sh', 'bat', 'ps1', + 'flow', 'props', 'pch', 'cc', 'cxx', 'hpp', 'hxx', 'tcc', 'defs', 'makefile', 'mk', 'cmake', + 'tex', 'm', 'r', 'matlab', 'latex', 'rst', 'adoc' ], + // 纯文本文件 + TEXT: ['txt', 'text', 'log', 'md', 'markdown', 'rst', 'adoc', 'tex', 'msg', 'csv', 'tsv'], + // 标记语言文件(用于特殊预览) MARKUP: ['html', 'htm', 'md', 'markdown'], @@ -316,3 +324,45 @@ export const FILE_SIZE_THRESHOLDS = { LARGE_FILE: 100 * 1024, // 100KB - 大文件检测阈值 MAX_TEXT_DISPLAY: 5 * 1024 * 1024, // 5MB - 文本文件最大显示大小 } + +/** + * UI 文本常量 + * @description 界面上显示的固定文本 + */ +export const UI_TEXT = { + // 对话框标题 + CREATE_FILE: '📄 新建文件', + CREATE_FOLDER: '📁 新建文件夹', + RENAME_FILE: '重命名文件', + DELETE_CONFIRM: '确认删除', + + // 按钮文本 + CONFIRM: '确定', + CANCEL: '取消', + CREATE: '创建', + SAVE: '保存', + DELETE: '删除', + + // 提示信息 + FILE_NAME_EMPTY: '请输入内容', + FILE_NAME_INVALID: '文件名包含非法字符', + FOLDER_NAME_INVALID: '文件夹名包含非法字符', + FILE_EXISTS: '文件已存在', + FOLDER_EXISTS: '文件夹已存在', + SELECT_DIRECTORY: '请先选择一个目录', + CREATE_SUCCESS: '创建成功', + CREATE_FAILED: '创建失败', + + // 输入提示 + ENTER_FILE_NAME: '请输入文件名(如: todo.md)', + ENTER_FOLDER_NAME: '请输入文件夹名称', +} + +/** + * 验证规则 + * @description 数据验证的正则表达式规则 + */ +export const VALIDATION_RULES = { + // Windows 文件名非法字符 + ILLEGAL_FILE_NAME_CHARS: /[<>:"/\\|?*]/, +} diff --git a/web/src/utils/errorHandler.js b/web/src/utils/errorHandler.js new file mode 100644 index 0000000..cc4f62d --- /dev/null +++ b/web/src/utils/errorHandler.js @@ -0,0 +1,63 @@ +/** + * 错误处理工具函数 + * + * @module utils/errorHandler + * @description 统一的错误处理,避免代码重复 + */ + +import { Message } from '@arco-design/web-vue' + +/** + * 统一的错误处理 + * @param {Error} error - 错误对象 + * @param {string} context - 操作上下文(用于日志) + */ +export function handleError(error, context = '') { + // 1. 记录日志 + console.error(`[${context}]`, error) + + // 2. 显示用户提示 + const message = error?.message || '操作失败' + Message.error(message) +} + +/** + * 包装异步函数,自动处理错误 + * @param {Function} fn - 异步函数 + * @param {string} context - 操作上下文 + * @returns {Function} 包装后的函数 + */ +export function withErrorHandling(fn, context = '') { + return async (...args) => { + try { + return await fn(...args) + } catch (error) { + handleError(error, context) + throw error // 重新抛出,让调用者决定是否继续 + } + } +} + +/** + * 显示成功提示 + * @param {string} message - 成功消息 + */ +export function showSuccess(message) { + Message.success(message) +} + +/** + * 显示警告提示 + * @param {string} message - 警告消息 + */ +export function showWarning(message) { + Message.warning(message) +} + +/** + * 显示信息提示 + * @param {string} message - 信息消息 + */ +export function showInfo(message) { + Message.info(message) +} diff --git a/web/src/utils/fileTypeHelpers.js b/web/src/utils/fileTypeHelpers.js new file mode 100644 index 0000000..77ee6a6 --- /dev/null +++ b/web/src/utils/fileTypeHelpers.js @@ -0,0 +1,161 @@ +/** + * 文件类型判断工具函数 + * + * @module utils/fileTypeHelpers + * @description 统一文件类型判断逻辑,避免内联重复定义 + */ + +import { FILE_EXTENSIONS } from './constants' +import { getExt } from './pathHelpers' + +/** + * 可预览的文件类型(有专门的预览处理) + * @type {string[]} + */ +export const PREVIEWABLE_TYPES = [ + ...FILE_EXTENSIONS.IMAGE, + ...FILE_EXTENSIONS.VIDEO_BROWSER, + ...FILE_EXTENSIONS.AUDIO, + 'pdf', 'html', 'htm', 'md', 'markdown' +] + +/** + * 已知二进制文件类型(直接显示二进制文件信息) + * @type {string[]} + */ +export const KNOWN_BINARY_TYPES = [ + // 可执行文件 + 'exe', 'dll', 'so', 'bin', + // 压缩文件 + 'zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz', 'iso', 'img', 'dmg', + // Office 文档 + 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', + // 其他二进制 + 'pdb', 'idb', 'lib', 'obj', 'o', 'a' +] + +/** + * 文本可编辑类型 + * @type {string[]} + */ +export const TEXT_EDITABLE_TYPES = [ + ...FILE_EXTENSIONS.CODE, + 'md', 'markdown', 'txt', 'json', 'xml', 'yaml', 'yml', 'toml', 'ini', 'cfg', 'conf' +] + +/** + * 判断是否为图片文件 + * @param {string} path - 文件路径或扩展名 + * @returns {boolean} + */ +export const isImageFile = (path) => { + const ext = getExt(path) + return FILE_EXTENSIONS.IMAGE.includes(ext) +} + +/** + * 判断是否为视频文件 + * @param {string} path - 文件路径或扩展名 + * @returns {boolean} + */ +export const isVideoFile = (path) => { + const ext = getExt(path) + return FILE_EXTENSIONS.VIDEO_BROWSER.includes(ext) +} + +/** + * 判断是否为音频文件 + * @param {string} path - 文件路径或扩展名 + * @returns {boolean} + */ +export const isAudioFile = (path) => { + const ext = getExt(path) + return FILE_EXTENSIONS.AUDIO.includes(ext) +} + +/** + * 判断是否为 PDF 文件 + * @param {string} path - 文件路径或扩展名 + * @returns {boolean} + */ +export const isPdfFile = (path) => { + const ext = getExt(path) + return ext === 'pdf' +} + +/** + * 判断是否为 HTML 文件 + * @param {string} path - 文件路径或扩展名 + * @returns {boolean} + */ +export const isHtmlFile = (path) => { + const ext = getExt(path) + return ['html', 'htm'].includes(ext) +} + +/** + * 判断是否为 Markdown 文件 + * @param {string} path - 文件路径或扩展名 + * @returns {boolean} + */ +export const isMarkdownFile = (path) => { + const ext = getExt(path) + return ['md', 'markdown'].includes(ext) +} + +/** + * 判断文件是否支持预览模式 + * @param {string} path - 文件路径 + * @returns {boolean} + */ +export const isPreviewable = (path) => { + const ext = getExt(path) + return PREVIEWABLE_TYPES.includes(ext) +} + +/** + * 判断文件是否为已知二进制类型 + * @param {string} path - 文件路径 + * @returns {boolean} + */ +export const isKnownBinary = (path) => { + const ext = getExt(path) + return KNOWN_BINARY_TYPES.includes(ext) +} + +/** + * 判断文件是否可文本编辑 + * @param {string} path - 文件路径 + * @returns {boolean} + */ +export const isTextEditable = (path) => { + const ext = getExt(path) + return TEXT_EDITABLE_TYPES.includes(ext) +} + +/** + * 获取文件类型分类 + * @param {string} path - 文件路径 + * @returns {string} 类型分类:'image' | 'video' | 'audio' | 'pdf' | 'html' | 'markdown' | 'text' | 'binary' | 'unknown' + */ +export const getFileCategory = (path) => { + if (isImageFile(path)) return 'image' + if (isVideoFile(path)) return 'video' + if (isAudioFile(path)) return 'audio' + if (isPdfFile(path)) return 'pdf' + if (isHtmlFile(path)) return 'html' + if (isMarkdownFile(path)) return 'markdown' + if (isTextEditable(path)) return 'text' + if (isKnownBinary(path)) return 'binary' + return 'unknown' +} + +/** + * 判断是否为 Office 文件 + * @param {string} path - 文件路径 + * @returns {boolean} + */ +export const isOfficeFile = (path) => { + const ext = getExt(path) + return ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'].includes(ext) +} diff --git a/web/src/utils/fileUtils.js b/web/src/utils/fileUtils.js index d320e4d..ff5bab1 100644 --- a/web/src/utils/fileUtils.js +++ b/web/src/utils/fileUtils.js @@ -19,15 +19,16 @@ import { FILE_ICON_MAP, FILE_ICONS, BYTE_UNITS, FILE_SIZE_FORMAT, FILE_EXTENSION */ export function formatBytes(bytes) { if (!bytes || bytes === 0) return '0 B' + if (typeof bytes !== 'number' || isNaN(bytes)) return '0 B' const unit = FILE_SIZE_FORMAT.UNIT const decimals = FILE_SIZE_FORMAT.DECIMAL_PLACES if (bytes < unit) return bytes + ' B' - const exp = Math.floor(Math.log(bytes) / Math.log(unit)) + const exp = Math.min(Math.floor(Math.log(bytes) / Math.log(unit)), BYTE_UNITS.length - 1) const value = bytes / Math.pow(unit, exp) - const unitSymbol = BYTE_UNITS[1][exp - 1] + 'B' + const unitSymbol = BYTE_UNITS[exp] return value.toFixed(decimals) + ' ' + unitSymbol } diff --git a/web/src/utils/markedExtensions.ts b/web/src/utils/markedExtensions.ts new file mode 100644 index 0000000..737c5b9 --- /dev/null +++ b/web/src/utils/markedExtensions.ts @@ -0,0 +1,35 @@ +import { marked } from 'marked' +import hljs from 'highlight.js' +import mermaid from 'mermaid' + +// 导入 highlight.js 核心和两种主题样式 +import 'highlight.js/lib/common' +import 'highlight.js/styles/github-dark.css' +import 'highlight.js/styles/github.css' + +// Mermaid 初始化 +mermaid.initialize({ startOnLoad: false, theme: 'default', securityLevel: 'loose' }) + +// 自定义 renderer +const renderer = new marked.Renderer() + +renderer.code = function(token: any) { + // Mermaid 代码块 + if (token.lang === 'mermaid') { + return `
${token.text}
` + } + + // 普通代码块 - 使用 highlight.js 高亮 + const lang = hljs.getLanguage(token.lang) ? token.lang : 'plaintext' + const highlighted = hljs.highlight(token.text, { language: lang }).value + return `
${highlighted}
` +} + +marked.use({ renderer, breaks: true, gfm: true }) + +export { marked } +export async function renderMermaidDiagrams() { + await mermaid.run() +} + + diff --git a/web/src/utils/pathHelpers.js b/web/src/utils/pathHelpers.js new file mode 100644 index 0000000..e36d997 --- /dev/null +++ b/web/src/utils/pathHelpers.js @@ -0,0 +1,103 @@ +/** + * 路径处理工具函数 + * + * @module utils/pathHelpers + * @description 统一路径分割、文件名获取等操作,避免重复代码 + */ + +import { getExt as getExtFromFileHelpers } from './fileHelpers' + +// 重新导出 getExt,避免重复定义 +export const getExt = getExtFromFileHelpers + +/** + * 路径分隔符正则(匹配 Windows 和 Unix 风格) + * @type {RegExp} + */ +export const PATH_SEPARATOR_REGEX = /[/\\]/ + +/** + * 分割路径为多个部分 + * @param {string} path - 文件路径 + * @returns {string[]} 路径数组 + * @example + * splitPath('C:\\Users\\file.txt') // ['C:', 'Users', 'file.txt'] + * splitPath('/home/user/file.txt') // ['home', 'user', 'file.txt'] + */ +export const splitPath = (path) => { + if (!path) return [] + return path.split(PATH_SEPARATOR_REGEX) +} + +/** + * 获取文件名(含扩展名) + * @param {string} path - 文件路径 + * @returns {string} 文件名 + * @example + * getFileName('C:\\Users\\file.txt') // 'file.txt' + * getFileName('/home/user/file.txt') // 'file.txt' + */ +export const getFileName = (path) => { + if (!path) return '' + const parts = splitPath(path) + return parts[parts.length - 1] || path +} + +/** + * 获取父目录路径 + * @param {string} path - 文件或目录路径 + * @returns {string} 父目录路径 + * @example + * getParentPath('C:\\Users\\file.txt') // 'C:\\Users' + * getParentPath('/home/user/file.txt') // '/home/user' + */ +export const getParentPath = (path) => { + if (!path) return '' + + // 查找最后一个分隔符的位置 + const lastSep = Math.max( + path.lastIndexOf('/'), + path.lastIndexOf('\\') + ) + + if (lastSep <= 0) return path + return path.substring(0, lastSep) +} + +/** + * 获取文件名(不含扩展名) + * @param {string} path - 文件路径 + * @returns {string} 文件名(不含扩展名) + * @example + * getFileNameWithoutExt('file.txt') // 'file' + * getFileNameWithoutExt('archive.tar.gz') // 'archive.tar' + */ +export const getFileNameWithoutExt = (path) => { + const fileName = getFileName(path) + const lastDot = fileName.lastIndexOf('.') + return lastDot > 0 ? fileName.substring(0, lastDot) : fileName +} + +/** + * 规范化路径分隔符(统一为正斜杠) + * @param {string} path - 文件路径 + * @returns {string} 规范化后的路径 + */ +export const normalizePathSeparators = (path) => { + if (!path) return '' + return path.replace(/\\/g, '/') +} + +/** + * 连接路径片段 + * @param {...string} parts - 路径片段 + * @returns {string} 连接后的路径 + * @example + * joinPath('C:', 'Users', 'file.txt') // 'C:/Users/file.txt' + */ +export const joinPath = (...parts) => { + return parts + .filter(part => part && part !== '') + .map(part => part.replace(/[\/\\]+$/, '').replace(/^[\/\\]+/, '')) + .join('/') +} diff --git a/web/src/views/db-cli/index.vue.tmp b/web/src/views/db-cli/index.vue.tmp deleted file mode 100644 index ab93b95..0000000 --- a/web/src/views/db-cli/index.vue.tmp +++ /dev/null @@ -1,6 +0,0 @@ -// 新架构:使用单例 Store(事件驱动) -const structureStore = useStructureStore() -// 直接使用 Store 的状态(无需计算属性,无需 watch) -// 状态是只读的,通过 Store 方法修改 - -// 表结构编辑状态 \ No newline at end of file diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 0000000..4fbda84 --- /dev/null +++ b/web/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "jsx": "preserve", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "allowJs": true, + "strict": false, + "esModuleInterop": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "useDefineForClassFields": true, + "sourceMap": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "src/**/*.vue", + "src/**/*.d.ts" + ], + "exclude": [ + "node_modules", + "dist" + ] +} diff --git a/web/vite.config.js b/web/vite.config.js index 09764ae..185dcd8 100644 --- a/web/vite.config.js +++ b/web/vite.config.js @@ -12,6 +12,7 @@ export default defineConfig({ build: { outDir: 'dist', emptyOutDir: true, + sourcemap: true, // 启用 source map 用于调试 rollupOptions: { output: { manualChunks: {