Private
Public Access
1
0

1 Commits
main ... v0.3.3

Author SHA1 Message Date
90695d71d1 发布:v0.3.3 版本历史模块 + 域名迁移 + 站点版本信息修正
- 版本号更新至 0.3.3(version.go/wails.json/README.md)
- 更新检查域名迁移 img.1216.top → c.1216.top
- 新增 views/version 版本历史 Tab 页面(时间线 UI)
- 设置面板新增版本历史入口按钮
- CHANGELOG 补全 0.3.3 全部 17 个提交记录
- 站点 HTML 修正(删除错误 v0.4.0,v0.3.3 为最新)
- 生成 last-version.json / versions.json 发布数据
2026-04-13 23:49:21 +08:00
30 changed files with 269 additions and 710 deletions

View File

@@ -1,34 +1,5 @@
# 更新日志
## [0.3.4] - 2026-04-22
### 新增 ✨
- **CodeMirror 搜索功能**: Ctrl+F / Ctrl+H 全局查找替换,`@codemirror/search` 集成
- **编辑器滚动位置恢复**: LRU 缓存最多5份/3分钟TTL切换文件不丢位置
- **文件列表列排序**: 图标/名称/时间/大小四列可排序,升序降序切换
- **文件搜索过滤**: 工具栏实时搜索框,按文件名过滤列表
- **Toolbar UI 重排**: 快捷访问内嵌面包屑左侧、历史记录改为图标+tooltip、Ctrl+H 快捷键
- **更新面板 Markdown 渲染**: changelog 用 `marked.parse()` 结构化渲染,替代纯文本
- **重命名零闪烁**: `updateFilePath()` 仅迁移路径引用+草稿key不重新加载内容
### 优化 🚀
- **路径安全重构**: `validateFilePath()` 提取统一函数,消除两处重复校验代码
- **requireUpdateAPI 模式**: 7 处重复 nil 检查收敛为 guard 方法
- **端口统一**: 文件服务器端口 18765→8073全局一致消除魔法数字分散
- **文件服务器 URL 动态获取**: 前端从后端 API 获取,不再硬编码
- **Tab 配置迁移扩展**: MigrateTabConfig 改为 map 驱动,覆盖 openclaw-manager→version 迁移
- **updateContent 简化**: 去掉时间窗口双重检查,仅保留版本号机制
### 安全修复 🔒
- **sentinel error 替代字符串匹配**: validateFilePath 错误用 `errors.Is()` 判断,消息变更不再静默失效
- **sanitizeHtml 防御远程 Markdown XSS**: 过滤 script/iframe/embed/on* 事件属性
### 修复 🐛
- **showHeader 默认值修正**: localStorage 无值时默认显示表头(兼容旧行为)
- **外层容器双重 scroll reset 移除**: 避免 CodeEditor 内部滚动恢复与外层 reset 冲突闪烁
---
## [0.3.3] - 2026-04-13
### 新增 ✨
@@ -43,15 +14,15 @@
### 优化 🚀
- MySQL 动态连接池重构 — 健康检查、性能权重、自适应扩缩容
- SQL 查询优化器 — 查询缓存、慢查询日志
- SQL 查询优化器 — 查询缓存、慢查询日志 (762 行)
- Redis Pipeline — 批量命令、事务 MULTI/EXEC 支持
- Office/CSV 预览增强 — 本地文件服务器获取文件
- Markdown 增强 — 本地文件链接支持、Shell 语法高亮
- HTML 预览 — 改用 iframe src 替代 srcdoc
- Wails 框架升级 + Mermaid 主题切换 + 代码高亮修复
- 文件列表 UI 重构 — 统一渲染逻辑,提升滚动性能
- FileListPanel 重写 (+511 行) — 删除 FileItemRow统一列表渲染逻辑
- CSV 编辑模式优化 + PDF 导出重构
- 拷贝功能优化
- 拷贝功能优化 — 新增 ClipboardCopy composable
### 修复 🐛
- Office 文件预览:修复类型检测与二进制误判
@@ -68,9 +39,10 @@
### 重构 🔧
- CodeMirror 架构优化 — 统一导出避免多实例问题
- 消除代码重复 — storage/connection_service 重构、useVisibleDatabases 抽取
- 大规模死代码清理,显著减小包体积
- **大规模死代码清理 (-1306 行)**: 删除废弃 storage 层(connection_service 279行)、audit_log、file_lock、recycle_bin、zip_helper、useFileEdit.js(-369行)、useFilePreview.js(-603行)、errorHandler.js(-63行)、DeviceTest 清理等
- 配置加载超时保护(最多重试 30 次)
- 正则表达式预编译、缓存读锁优化
- 正则表达式预编译query_optimizer
- 缓存读锁优化 + SHA-256 key hash
- 禁止 Ctrl+滚轮缩放
- Dockerfile 语法高亮支持
- 滚动条样式修复

View File

@@ -1,22 +1,10 @@
# U-Desk v0.3.4
# U-Desk v0.3.3
## 功能
- **文件管理** — 本地文件浏览、编辑CodeMirror 语法高亮+搜索)、预览(图片/视频/PDF/HTML/Markdown/Excel/Word/CSV
- **数据库客户端** — 多数据库连接管理、SQL 执行、查询历史、表结构管理
- **Markdown 编辑器** — 独立编辑页面、实时预览、PDF 导出
- **版本更新** — 自动检查更新、下载安装、changelog 渲染
- **系统信息** — CPU/内存/磁盘硬件信息查询
## 技术栈
- **后端**: Go + Wails v2 (桌面应用框架)
- **前端**: Vue 3 + Arco Design + CodeMirror 6 + Pinia
- **存储**: SQLite (GORM)
- **本地文件服务器**: `localhost:8073`CSS/JS 路径转换、HTML 预览)
## 开发
```bash
wails dev
```
- 数据库客户端
- Markdown编辑器
- PDF导出
## 更新
-文件服务器安全重构+编辑器增强+搜索排序+更新面板渲染
-MD编辑器完成
- ✅ PDF导出优化中

72
app.go
View File

@@ -3,6 +3,7 @@ package main
import (
"context"
"fmt"
"net/http"
"os"
"path/filepath"
stdruntime "runtime"
@@ -29,6 +30,7 @@ type App struct {
updateAPI *api.UpdateAPI
configAPI *api.ConfigAPI
pdfAPI *api.PdfAPI
fileServer *http.Server
filesystem *filesystem.FileSystemService
isAlwaysOnTop bool
}
@@ -192,7 +194,7 @@ func (a *App) startFileServer() {
return
}
fmt.Println("[文件服务器] 启动在 http://localhost:8073")
fmt.Println("[文件服务器] 启动在 http://localhost:18765")
}
// Shutdown 应用关闭时调用
@@ -413,7 +415,7 @@ func (a *App) GetCommonPaths() (map[string]string, error) {
// Windows: 从注册表读取特殊文件夹真实路径(用户可能已修改位置)
folderGUIDs := map[string]string{
"desktop": "{B4BFCC3A-DB2C-424C-B029-7FE99A87C641}",
"documents": "{D20B4C7F-5EA7-424C-B25E-039F6F1FCC8A}",
"documents": "{D20B4C7F-5EA7-40D4B25E-039F6F1FCC8A}",
"downloads": "{374DE290-123F-4565-9164-39C4925E467B}",
}
for name, guid := range folderGUIDs {
@@ -601,84 +603,68 @@ func (a *App) ListSqlTabs() ([]map[string]interface{}, error) {
// ========== 版本更新管理接口 ==========
// requireUpdateAPI 检查 updateAPI 是否已初始化,未初始化返回统一错误
func (a *App) requireUpdateAPI() (*api.UpdateAPI, error) {
// CheckUpdate 检查更新UpdateAPI 可能尚未初始化完成)
func (a *App) CheckUpdate() (map[string]interface{}, error) {
if a.updateAPI == nil {
return nil, fmt.Errorf("更新功能正在初始化中")
}
return a.updateAPI, nil
}
// CheckUpdate 检查更新UpdateAPI 可能尚未初始化完成)
func (a *App) CheckUpdate() (map[string]interface{}, error) {
api, err := a.requireUpdateAPI()
if err != nil {
return nil, err
}
return api.CheckUpdate()
return a.updateAPI.CheckUpdate()
}
// GetCurrentVersion 获取当前版本号
func (a *App) GetCurrentVersion() (map[string]interface{}, error) {
api, err := a.requireUpdateAPI()
if err != nil {
return nil, err
if a.updateAPI == nil {
return nil, fmt.Errorf("更新功能正在初始化中")
}
return api.GetCurrentVersion()
return a.updateAPI.GetCurrentVersion()
}
// GetUpdateConfig 获取更新配置
func (a *App) GetUpdateConfig() (map[string]interface{}, error) {
api, err := a.requireUpdateAPI()
if err != nil {
return nil, err
if a.updateAPI == nil {
return nil, fmt.Errorf("更新功能正在初始化中")
}
return api.GetUpdateConfig()
return a.updateAPI.GetUpdateConfig()
}
// SetUpdateConfig 设置更新配置
func (a *App) SetUpdateConfig(autoCheckEnabled bool, checkIntervalMinutes int, checkURL string) (map[string]interface{}, error) {
api, err := a.requireUpdateAPI()
if err != nil {
return nil, err
if a.updateAPI == nil {
return nil, fmt.Errorf("更新功能正在初始化中")
}
return api.SetUpdateConfig(autoCheckEnabled, checkIntervalMinutes, checkURL)
return a.updateAPI.SetUpdateConfig(autoCheckEnabled, checkIntervalMinutes, checkURL)
}
// DownloadUpdate 下载更新包
func (a *App) DownloadUpdate(downloadURL string) (map[string]interface{}, error) {
api, err := a.requireUpdateAPI()
if err != nil {
return nil, err
if a.updateAPI == nil {
return nil, fmt.Errorf("更新功能正在初始化中")
}
return api.DownloadUpdate(downloadURL)
return a.updateAPI.DownloadUpdate(downloadURL)
}
// InstallUpdate 安装更新包
func (a *App) InstallUpdate(installerPath string, autoRestart bool) (map[string]interface{}, error) {
api, err := a.requireUpdateAPI()
if err != nil {
return nil, err
if a.updateAPI == nil {
return nil, fmt.Errorf("更新功能正在初始化中")
}
return api.InstallUpdate(installerPath, autoRestart)
return a.updateAPI.InstallUpdate(installerPath, autoRestart)
}
// InstallUpdateWithHash 安装更新包(带哈希验证)
func (a *App) InstallUpdateWithHash(installerPath string, autoRestart bool, expectedHash string, hashType string) (map[string]interface{}, error) {
api, err := a.requireUpdateAPI()
if err != nil {
return nil, err
if a.updateAPI == nil {
return nil, fmt.Errorf("更新功能正在初始化中")
}
return api.InstallUpdateWithHash(installerPath, autoRestart, expectedHash, hashType)
return a.updateAPI.InstallUpdateWithHash(installerPath, autoRestart, expectedHash, hashType)
}
// VerifyUpdateFile 验证更新文件哈希值
func (a *App) VerifyUpdateFile(filePath string, expectedHash string, hashType string) (map[string]interface{}, error) {
api, err := a.requireUpdateAPI()
if err != nil {
return nil, err
if a.updateAPI == nil {
return nil, fmt.Errorf("更新功能正在初始化中")
}
return api.VerifyUpdateFile(filePath, expectedHash, hashType)
return a.updateAPI.VerifyUpdateFile(filePath, expectedHash, hashType)
}
// startAutoUpdateCheck 启动自动更新检查
@@ -767,7 +753,7 @@ func (a *App) GetAuditLogs(limit int) ([]map[string]interface{}, error) {
// GetFileServerURL 获取本地文件服务器的URL
func (a *App) GetFileServerURL() string {
return "http://localhost:8073"
return "http://localhost:18765"
}
// DetectFileTypeByContent 通过文件内容检测文件类型(用于小文件)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 130 KiB

View File

@@ -1 +1 @@
{"version": "0.3.3", "release_date": "2026-04-13", "changelog": "### 新增 ✨\n- Markdown 编辑器: 独立编辑页面、实时预览、字符/行数统计、Ctrl+S 保存、自动保存\n- PDF 导出: 浏览器打印 + 后端 gofpdf/chromedp 多种导出方式\n- 窗口置顶 + 收藏夹置顶\n- Excel/Word 文件预览支持\n- 数据库 UI 大幅改进: 查询历史、查询模板、SQL 工具栏、结果导出\n- 数据库可见性过滤与连接管理增强\n\n### 优化 🚀\n- MySQL 动态连接池重构(健康检查、性能权重、自适应扩缩容)\n- SQL 查询优化器(查询缓存、慢查询日志)\n- Redis Pipeline 支持\n- Wails 框架升级 + FileListPanel 重写\n- CSV 编辑模式优化 + 拷贝功能优化\n\n### 修复 🐛\n- Office 类型检测修复、CORS 跨域修复、大文件卡死修复\n\n### 安全修复 🔒\n- XSS 防护、PDF 路径穿越防护、HTML 注入防护\n\n### 重构 🔧\n- CodeMirror 架构优化、大规模死代码清理(-1306行)", "download_url": "https://c.1216.top/download/u-desk-0.3.3.exe", "file_size": 9801728, "sha256": "829c79a91c10277011159749110f4ebee5e3638a078e86850c03b1c9f09e184c", "force_update": false}
{"version": "0.3.3", "release_date": "2026-04-13", "changelog": "### 新增 ✨\n- Markdown 编辑器: 独立编辑页面、实时预览、字符/行数统计、Ctrl+S 保存、自动保存\n- PDF 导出: 浏览器打印 + 后端 gofpdf/chromedp 多种导出方式\n- 窗口置顶 + 收藏夹置顶\n- Excel/Word 文件预览支持\n- 数据库 UI 大幅改进: 查询历史、查询模板、SQL 工具栏、结果导出\n- 数据库可见性过滤与连接管理增强\n\n### 优化 🚀\n- MySQL 动态连接池重构(健康检查、性能权重、自适应扩缩容)\n- SQL 查询优化器(查询缓存、慢查询日志)\n- Redis Pipeline 支持\n- Wails 框架升级 + FileListPanel 重写\n- CSV 编辑模式优化 + 拷贝功能优化\n\n### 修复 🐛\n- Office 类型检测修复、CORS 跨域修复、大文件卡死修复\n\n### 安全修复 🔒\n- XSS 防护、PDF 路径穿越防护、HTML 注入防护\n\n### 重构 🔧\n- CodeMirror 架构优化、大规模死代码清理(-1306行)", "download_url": "https://c.1216.top/download/u-desk-0.3.3.exe", "file_size": 18396672, "sha256": "f0bdf8954b276f4bb45a69336f171bb2a481f7a7125fc3309aae5de2fbf0cf15", "force_update": false}

View File

@@ -1 +1 @@
{"updated_at": "2026-04-13T23:45:00+08:00", "versions": [{"version": "0.3.3", "release_date": "2026-04-13", "changelog": "### 新增 ✨\n- **Markdown 编辑器**: 独立编辑页面、实时预览、字符/行数统计、Ctrl+S 保存、自动保存\n- **Markdown 文件页面**: 独立的 Markdown 文件查看与编辑界面\n- **PDF 导出**: 浏览器打印 + 后端 gofpdf/chromedp 多种导出方式\n- **窗口置顶**: 支持窗口始终置顶\n- **收藏夹置顶**: 收藏项支持置顶排序\n- **文件预览**: Excel/Word 文件预览支持\n- **数据库 UI 大幅改进**: 查询历史面板、查询模板面板、SQL 工具栏、结果导出(CSV)、SQL 格式化器\n- **数据库可见性过滤**: 连接管理增强、ConnectionForm 重写、统一错误处理模块\n\n### 优化 🚀\n- MySQL 动态连接池重构 — 健康检查、性能权重、自适应扩缩容\n- SQL 查询优化器 — 查询缓存、慢查询日志 (762 行)\n- Redis Pipeline — 批量命令、事务 MULTI/EXEC 支持\n- Wails 框架升级 + Mermaid 主题切换 + 代码高亮修复\n- FileListPanel 重写 (+511 行) — 删除 FileItemRow统一列表渲染逻辑\n- CSV 编辑模式优化 + PDF 导出重构\n- 拷贝功能优化 — 新增 ClipboardCopy composable\n\n### 修复 🐛\n- Office 文件预览:修复类型检测与二进制误判\n- 本地文件服务器 CORS 跨域问题\n- 大文件点击卡死问题\n- 收藏夹 bug 修复\n\n### 安全修复 🔒\n- XSS 防护PdfExportButton、MarkdownPreview HTML 消毒)\n- PDF 导出路径穿越防护\n- PDF 导出标题 HTML 注入防护\n\n### 重构 🔧\n- CodeMirror 架构优化 — 统一导出避免多实例问题\n- 消除代码重复 — storage/connection_service 重构\n- **大规模死代码清理 (-1306 行)**: 删除废弃 storage 层、audit_log、file_lock、recycle_bin、useFileEdit.js(-369行)、useFilePreview.js(-603行) 等\n- 配置加载超时保护、正则表达式预编译、禁止 Ctrl+滚轮缩放", "download_url": "https://c.1216.top/download/u-desk-0.3.3.exe", "file_size": 9801728, "sha256": "829c79a91c10277011159749110f4ebee5e3638a078e86850c03b1c9f09e184c"}, {"version": "0.3.2", "release_date": "2026-02-05", "changelog": "### 重构 🔧\n- CodeMirror 架构优化 - 统一导出避免多实例问题\n- 语言加载器优化 - 从动态 import 改为静态导入\n- 动态主题切换 - 使用 Compartment 实现无损切换\n\n### 优化 🚀\n- 编辑器性能 - 添加内容更新防抖\n- 亮色主题 - 改进代码编辑器亮色模式样式", "download_url": "", "file_size": 0, "sha256": ""}, {"version": "0.3.0", "release_date": "2026-02-04", "changelog": "### 新增 ✨\n- Markdown 图表支持 - Mermaid 流程图、时序图、类图等\n- 代码语法高亮 - 支持 20+ 种常用编程语言\n- 文件列表优化 - 文件夹优先显示,同类型按名称排序", "download_url": "", "file_size": 0, "sha256": ""}, {"version": "0.2.0", "release_date": "2026-01-28", "changelog": "### 新增 ✨\n- 应用配置管理 - 全新设置面板,支持自定义显示模块和默认启动页\n- 智能更新提醒 - 新增版本更新通知组件\n- 模块重命名 - 应用更名为 u-desk", "download_url": "", "file_size": 0, "sha256": ""}, {"version": "0.1.5", "release_date": "2026-01-22", "changelog": "### 新增 ✨\n- 文件管理模块 - 文件浏览、编辑、操作功能\n- 版本更新管理 - 自动检查和下载更新\n- 系统信息查询 - CPU、内存、磁盘等硬件信息", "download_url": "", "file_size": 0, "sha256": ""}, {"version": "0.1.0", "release_date": "2026-01-18", "changelog": "### 新增 ✨\n- 数据库管理 - 支持多种数据库连接和查询功能", "download_url": "", "file_size": 0, "sha256": ""}]}
{"updated_at": "2026-04-13T23:45:00+08:00", "versions": [{"version": "0.3.3", "release_date": "2026-04-13", "changelog": "### 新增 ✨\n- **Markdown 编辑器**: 独立编辑页面、实时预览、字符/行数统计、Ctrl+S 保存、自动保存\n- **Markdown 文件页面**: 独立的 Markdown 文件查看与编辑界面\n- **PDF 导出**: 浏览器打印 + 后端 gofpdf/chromedp 多种导出方式\n- **窗口置顶**: 支持窗口始终置顶\n- **收藏夹置顶**: 收藏项支持置顶排序\n- **文件预览**: Excel/Word 文件预览支持\n- **数据库 UI 大幅改进**: 查询历史面板、查询模板面板、SQL 工具栏、结果导出(CSV)、SQL 格式化器\n- **数据库可见性过滤**: 连接管理增强、ConnectionForm 重写、统一错误处理模块\n\n### 优化 🚀\n- MySQL 动态连接池重构 — 健康检查、性能权重、自适应扩缩容\n- SQL 查询优化器 — 查询缓存、慢查询日志 (762 行)\n- Redis Pipeline — 批量命令、事务 MULTI/EXEC 支持\n- Wails 框架升级 + Mermaid 主题切换 + 代码高亮修复\n- FileListPanel 重写 (+511 行) — 删除 FileItemRow统一列表渲染逻辑\n- CSV 编辑模式优化 + PDF 导出重构\n- 拷贝功能优化 — 新增 ClipboardCopy composable\n\n### 修复 🐛\n- Office 文件预览:修复类型检测与二进制误判\n- 本地文件服务器 CORS 跨域问题\n- 大文件点击卡死问题\n- 收藏夹 bug 修复\n\n### 安全修复 🔒\n- XSS 防护PdfExportButton、MarkdownPreview HTML 消毒)\n- PDF 导出路径穿越防护\n- PDF 导出标题 HTML 注入防护\n\n### 重构 🔧\n- CodeMirror 架构优化 — 统一导出避免多实例问题\n- 消除代码重复 — storage/connection_service 重构\n- **大规模死代码清理 (-1306 行)**: 删除废弃 storage 层、audit_log、file_lock、recycle_bin、useFileEdit.js(-369行)、useFilePreview.js(-603行) 等\n- 配置加载超时保护、正则表达式预编译、禁止 Ctrl+滚轮缩放", "download_url": "https://c.1216.top/download/u-desk-0.3.3.exe", "file_size": 18396672, "sha256": "f0bdf8954b276f4bb45a69336f171bb2a481f7a7125fc3309aae5de2fbf0cf15"}, {"version": "0.3.2", "release_date": "2026-02-05", "changelog": "### 重构 🔧\n- CodeMirror 架构优化 - 统一导出避免多实例问题\n- 语言加载器优化 - 从动态 import 改为静态导入\n- 动态主题切换 - 使用 Compartment 实现无损切换\n\n### 优化 🚀\n- 编辑器性能 - 添加内容更新防抖\n- 亮色主题 - 改进代码编辑器亮色模式样式", "download_url": "", "file_size": 0, "sha256": ""}, {"version": "0.3.0", "release_date": "2026-02-04", "changelog": "### 新增 ✨\n- Markdown 图表支持 - Mermaid 流程图、时序图、类图等\n- 代码语法高亮 - 支持 20+ 种常用编程语言\n- 文件列表优化 - 文件夹优先显示,同类型按名称排序", "download_url": "", "file_size": 0, "sha256": ""}, {"version": "0.2.0", "release_date": "2026-01-28", "changelog": "### 新增 ✨\n- 应用配置管理 - 全新设置面板,支持自定义显示模块和默认启动页\n- 智能更新提醒 - 新增版本更新通知组件\n- 模块重命名 - 应用更名为 u-desk", "download_url": "", "file_size": 0, "sha256": ""}, {"version": "0.1.5", "release_date": "2026-01-22", "changelog": "### 新增 ✨\n- 文件管理模块 - 文件浏览、编辑、操作功能\n- 版本更新管理 - 自动检查和下载更新\n- 系统信息查询 - CPU、内存、磁盘等硬件信息", "download_url": "", "file_size": 0, "sha256": ""}, {"version": "0.1.0", "release_date": "2026-01-18", "changelog": "### 新增 ✨\n- 数据库管理 - 支持多种数据库连接和查询功能", "download_url": "", "file_size": 0, "sha256": ""}]}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

View File

@@ -1,44 +0,0 @@
Add-Type -AssemblyName System.Drawing
$srcPath = "E:\wk-lab\u-desk\build\windows\app-icon.png"
$icoPath = "E:\wk-lab\u-desk\build\windows\icon.ico"
$sizes = @(256, 128, 64, 48, 32, 16)
$src = [System.Drawing.Image]::FromFile($srcPath)
$fs = New-Object System.IO.FileStream($icoPath, [System.IO.FileMode]::Create)
$w = New-Object System.IO.BinaryWriter($fs)
$w.Write([uint16]0)
$w.Write([uint16]1)
$w.Write([uint16]$sizes.Count)
foreach ($sz in $sizes) {
$bmp = New-Object System.Drawing.Bitmap($sz, $sz, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
$g = [System.Drawing.Graphics]::FromImage($bmp)
$g.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::HighQuality
$g.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
$g.DrawImage($src, 0, 0, $sz, $sz)
$g.Dispose()
$ms = New-Object System.IO.MemoryStream
$bmp.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png)
$bytes = $ms.ToArray()
$ms.Dispose()
$bmp.Dispose()
$w.Write([uint32]40)
$w.Write([int32]$sz)
$w.Write([int32]$sz)
$w.Write([uint16]1)
$w.Write([uint32]32)
$w.Write([uint32]$bytes.Length)
$w.Write([uint32]22)
$w.Write($bytes)
}
$w.Close()
$fs.Close()
$src.Dispose()
$item = Get-Item $icoPath
Write-Output "ICO: $($item.Name) ($([math]::Round($item.Length / 1KB)) KB)"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

View File

@@ -136,66 +136,40 @@ func (api *ConfigAPI) SaveAppConfig(req SaveAppConfigRequest) (map[string]interf
}, nil
}
// MigrateTabConfig 迁移旧配置device 移除 + openclaw-manager 重命名)
// MigrateTabConfig 迁移旧配置
func (api *ConfigAPI) MigrateTabConfig() error {
config, _ := api.configService.GetTabConfig()
if config == nil {
return nil
}
needMigrate := false
// 检查是否包含需要迁移的旧 key
// 检查是否包含 device
hasDevice := false
for _, tab := range config.AvailableTabs {
if tab.Key == "device" || tab.Key == "openclaw-manager" {
needMigrate = true
if tab.Key == "device" {
hasDevice = true
break
}
}
if !needMigrate {
if !hasDevice {
return nil
}
// 映射:旧 key → 新 key不需要的移除
keyMap := map[string]string{
"openclaw-manager": "version",
// "device": "" // 直接过滤
}
// 过滤掉 device
newTabs := make([]service.TabDefinition, 0, len(config.AvailableTabs))
newVisible := make([]string, 0, len(config.VisibleTabs))
seenKeys := map[string]bool{}
for _, tab := range config.AvailableTabs {
newKey, shouldRename := keyMap[tab.Key]
if shouldRename {
if newKey == "" {
continue // 移除(如 device
}
if seenKeys[newKey] {
continue // 避免重复
}
seenKeys[newKey] = true
newTabs = append(newTabs, service.TabDefinition{Key: newKey, Title: tab.Title, Enabled: tab.Enabled})
} else {
if tab.Key != "device" {
newTabs = append(newTabs, tab)
}
}
for _, key := range config.VisibleTabs {
if newKey, ok := keyMap[key]; ok {
if newKey != "" && !seenKeys[newKey] {
newVisible = append(newVisible, newKey)
}
// newKey == "" 时跳过(如 device
} else {
if key != "device" {
newVisible = append(newVisible, key)
}
}
defaultTab := config.DefaultTab
if newKey, ok := keyMap[defaultTab]; ok && newKey != "" {
defaultTab = newKey
}
if defaultTab == "device" {
defaultTab = "file-system"
}

View File

@@ -2,7 +2,6 @@ package filesystem
import (
"context"
"errors"
"fmt"
"log"
"net/http"
@@ -49,35 +48,6 @@ var (
// HTML 属性正则缓存(避免 replaceHtmlTagAttribute 中重复编译)
var attrRegexCache sync.Map // map[string]*regexp.Regexp
// 路径校验 sentinel error用 errors.Is 匹配,不依赖字符串)
var (
ErrPathInvalidEncoding = fmt.Errorf("invalid path encoding")
ErrPathTraversal = fmt.Errorf("path traversal detected")
ErrPathUnsafe = fmt.Errorf("unsafe path")
)
// validateFilePath 校验文件路径安全性URL解码 + 路径遍历检测 + 安全检查)
// 返回清理后的绝对路径,或 sentinel error
func validateFilePath(rawPath string, logPrefix string) (string, error) {
decodedPath, err := url.QueryUnescape(rawPath)
if err != nil {
return "", ErrPathInvalidEncoding
}
if strings.Contains(decodedPath, "..") {
return "", ErrPathTraversal
}
filePath := strings.ReplaceAll(decodedPath, "/", "\\")
filePath = filepath.Clean(filePath)
if !isSafePath(filePath) {
return "", ErrPathUnsafe
}
return filePath, nil
}
// LocalFileServer 本地文件服务器(独立的 HTTP 服务器)
type LocalFileServer struct {
server *http.Server
@@ -105,7 +75,7 @@ func StartLocalFileServer() (string, error) {
// 创建服务器(固定端口)
server := &http.Server{
Addr: "localhost:8073",
Addr: "localhost:18765",
Handler: mux,
}
@@ -120,7 +90,7 @@ func StartLocalFileServer() (string, error) {
localFileServer = &LocalFileServer{
server: server,
addr: "localhost:8073",
addr: "localhost:18765",
}
log.Printf("[LocalFileServer] 已启动,监听: %s", localFileServer.addr)
@@ -155,6 +125,7 @@ func handleLocalFileRequest(w http.ResponseWriter, r *http.Request) {
// 从 URL 路径获取文件路径(移除 /localfs/ 前缀)
pathPart := strings.TrimPrefix(r.URL.Path, "/localfs/")
log.Printf("[LocalFileHandler] TrimPrefix 后: %s", pathPart)
if pathPart == "" || pathPart == r.URL.Path {
log.Printf("[LocalFileHandler] 路径前缀无效")
@@ -162,24 +133,34 @@ func handleLocalFileRequest(w http.ResponseWriter, r *http.Request) {
return
}
// 校验路径安全性(URL解码 + 路径遍历检测 + 安全检查)
filePath, err := validateFilePath(pathPart, "[LocalFileHandler]")
// 🔒 修复:先进行URL解码,防止路径遍历攻击
decodedPath, err := url.QueryUnescape(pathPart)
if err != nil {
log.Printf("[LocalFileHandler] 路径校验失败: %v (%s)", err, pathPart)
switch {
case errors.Is(err, ErrPathInvalidEncoding):
http.Error(w, "Invalid path encoding", http.StatusBadRequest)
case errors.Is(err, ErrPathTraversal):
http.Error(w, "Path traversal detected", http.StatusForbidden)
case errors.Is(err, ErrPathUnsafe):
http.Error(w, "Unsafe path", http.StatusForbidden)
default:
http.Error(w, err.Error(), http.StatusBadRequest)
}
log.Printf("[LocalFileHandler] URL解码失败: %v", err)
http.Error(w, "Invalid path encoding", http.StatusBadRequest)
return
}
log.Printf("[LocalFileHandler] URL解码后: %s", decodedPath)
// 🔒 修复:在路径转换前检查是否包含危险字符
if strings.Contains(decodedPath, "..") {
log.Printf("[LocalFileHandler] 检测到路径遍历尝试")
http.Error(w, "Path traversal detected", http.StatusForbidden)
return
}
// 路径转换(统一使用反斜杠)
filePath := strings.ReplaceAll(decodedPath, "/", "\\")
filePath = filepath.Clean(filePath)
log.Printf("[LocalFileHandler] 最终路径: %s", filePath)
// 安全检查
if !isSafePath(filePath) {
log.Printf("[LocalFileHandler] 路径未通过安全检查: %s", filePath)
http.Error(w, "Unsafe path", http.StatusForbidden)
return
}
// 🔒 文件类型白名单检查
ext := strings.ToLower(filepath.Ext(filePath))
if !isAllowedFileType(ext) {
@@ -478,30 +459,32 @@ func handleHtmlPreviewRequest(w http.ResponseWriter, r *http.Request) {
}
// 解析参数
rawPath := r.URL.Query().Get("path")
filePath := r.URL.Query().Get("path")
var err error
if filePath, err = url.QueryUnescape(filePath); err != nil {
http.Error(w, "Invalid path encoding", http.StatusBadRequest)
return
}
theme := r.URL.Query().Get("theme")
if theme == "" {
theme = "light"
}
// 校验路径安全性URL解码 + 路径遍历检测 + 安全检查)
filePath, err := validateFilePath(rawPath, "[HtmlPreview]")
if err != nil {
log.Printf("[HtmlPreview] 路径校验失败: %v (%s)", err, rawPath)
switch {
case errors.Is(err, ErrPathInvalidEncoding):
http.Error(w, "Invalid path encoding", http.StatusBadRequest)
case errors.Is(err, ErrPathTraversal):
http.Error(w, "Path traversal detected", http.StatusForbidden)
case errors.Is(err, ErrPathUnsafe):
http.Error(w, "Unsafe path", http.StatusForbidden)
default:
http.Error(w, err.Error(), http.StatusBadRequest)
}
log.Printf("[HtmlPreview] 收到请求: path=%s, theme=%s", filePath, theme)
// 安全检查
if !isSafePath(filePath) {
log.Printf("[HtmlPreview] 路径未通过安全检查: %s", filePath)
http.Error(w, "Unsafe path", http.StatusForbidden)
return
}
log.Printf("[HtmlPreview] 收到请求: path=%s, theme=%s", filePath, theme)
// 检查路径遍历攻击
if strings.Contains(filePath, "..") {
log.Printf("[HtmlPreview] 检测到路径遍历尝试: %s", filePath)
http.Error(w, "Path traversal detected", http.StatusForbidden)
return
}
// 读取文件
content, err := os.ReadFile(filePath)

View File

@@ -44,9 +44,9 @@ var defaultTabConfig = TabConfig{
{Key: "file-system", Title: "文件管理", Enabled: true},
{Key: "db-cli", Title: "数据库", Enabled: true},
{Key: "markdown-editor", Title: "Markdown", Enabled: true},
{Key: "version", Title: "版本历史", Enabled: true},
{Key: "openclaw-manager", Title: "OpenClaw", Enabled: true},
},
VisibleTabs: []string{"file-system", "db-cli", "markdown-editor", "version"},
VisibleTabs: []string{"file-system", "db-cli", "markdown-editor", "openclaw-manager"},
DefaultTab: "file-system",
}

12
web/package-lock.json generated
View File

@@ -25,7 +25,6 @@
"@codemirror/lang-yaml": "^6.1.2",
"@codemirror/language": "^6.12.1",
"@codemirror/legacy-modes": "^6.5.2",
"@codemirror/search": "^6.6.0",
"@codemirror/state": "^6.5.3",
"@codemirror/theme-one-dark": "^6.1.3",
"@codemirror/view": "^6.39.8",
@@ -415,17 +414,6 @@
"crelt": "^1.0.5"
}
},
"node_modules/@codemirror/search": {
"version": "6.6.0",
"resolved": "https://registry.npmmirror.com/@codemirror/search/-/search-6.6.0.tgz",
"integrity": "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.37.0",
"crelt": "^1.0.5"
}
},
"node_modules/@codemirror/state": {
"version": "6.5.3",
"resolved": "https://registry.npmmirror.com/@codemirror/state/-/state-6.5.3.tgz",

View File

@@ -25,7 +25,6 @@
"@codemirror/lang-yaml": "^6.1.2",
"@codemirror/language": "^6.12.1",
"@codemirror/legacy-modes": "^6.5.2",
"@codemirror/search": "^6.6.0",
"@codemirror/state": "^6.5.3",
"@codemirror/theme-one-dark": "^6.1.3",
"@codemirror/view": "^6.39.8",

View File

@@ -1 +1 @@
c0e9e27e045c6118704c87fcf34a03de
0e1fafcbb6b28922a38f6c5316932015

View File

@@ -101,13 +101,12 @@ import FileSystem from './components/FileSystem/index.vue'
import SettingsPanel from './components/SettingsPanel.vue'
import UpdateNotification from './components/UpdateNotification.vue'
import {useUpdateStore} from './stores/update'
import {useConfigStore, type AppConfig} from './stores/config'
import {useConfigStore} from './stores/config'
// 存储键
const ACTIVE_TAB_STORAGE_KEY = 'app-active-tab'
// 从 localStorage 恢复上次打开的区域,默认为 'file-system'
// 兼容旧版:'user' 是 v0.2.x 之前的 tab key已废弃需迁移
const savedTab = localStorage.getItem(ACTIVE_TAB_STORAGE_KEY)
const activeTab = ref((savedTab === 'user' ? 'file-system' : savedTab) || 'file-system')
const showSettings = ref(false)
@@ -126,7 +125,7 @@ const appConfig = computed(() => configStore.appConfig)
const visibleTabs = computed(() => configStore.visibleTabs)
// 保存配置
const handleSaveConfig = async (config: AppConfig) => {
const handleSaveConfig = async (config) => {
try {
await configStore.saveConfig(config)
showSettings.value = false
@@ -149,7 +148,7 @@ const loadConfig = async () => {
}
// 获取组件
const getComponent = (key: string) => {
const getComponent = (key) => {
const components = {
'file-system': FileSystem,
'db-cli': DbCli,
@@ -377,9 +376,4 @@ watch(activeTab, (newTab) => {
.arco-tooltip {
--wails-draggable: no-drag;
}
/* 桌面应用:禁止 html/body 级别滚动条,所有滚动由内部组件自行处理 */
html, body {
overflow: hidden !important;
}
</style>

View File

@@ -3,25 +3,34 @@
</template>
<script setup>
import { ref, onMounted, watch, onBeforeUnmount, nextTick } from 'vue'
import { ref, onMounted, watch, onBeforeUnmount, computed, nextTick } from 'vue'
import {
EditorView, lineNumbers, highlightActiveLineGutter, keymap,
EditorState, Compartment,
defaultKeymap, history,
bracketMatching, defaultHighlightStyle, syntaxHighlighting,
oneDark,
openSearchPanel, search
oneDark
} from '@/utils/codemirrorExports'
import { useThemeStore } from '@/stores/theme'
import { loadLanguageExtension, getLanguageFromExtension } from '@/utils/codeMirrorLoader'
// ==================== 主题定义 ====================
// 亮色主题的基础样式
const lightTheme = EditorView.theme({
'&': { backgroundColor: '#ffffff' },
'.cm-gutters': { backgroundColor: '#f7f7f7', color: '#999', border: 'none' },
'.cm-activeLineGutter': { backgroundColor: '#e8e8e8', color: '#333' },
'.cm-line': { caretColor: '#000' },
'.cm-selection': { backgroundColor: '#d9d9d9' },
'.cm-cursor': { borderLeftColor: '#000' }
})
// ==================== Props & Emits ====================
const props = defineProps({
modelValue: { type: String, required: true },
fileExtension: { type: String, default: '' },
filePath: { type: String, default: '' },
fileMtime: { type: String, default: '' }
fileExtension: { type: String, default: '' }
})
const emit = defineEmits(['update:modelValue'])
@@ -32,36 +41,6 @@ const themeStore = useThemeStore()
const editorContainer = ref(null)
let view = null
// 滚动位置缓存LRU 最多 5 份,每份 3 分钟过期
const MAX_SCROLL_CACHE = 5
const SCROLL_CACHE_TTL = 3 * 60 * 1000 // 3 分钟
const fileScrollPositions = new Map() // filePath → { scrollTop, anchor, timestamp }
let currentFilePath = ''
let saveScrollTimer = null
// 清理过期缓存 + LRU 淘汰,保持最多 MAX_SCROLL_CACHE 条
const cleanScrollCache = () => {
const now = Date.now()
// 清理过期的
for (const [key, val] of fileScrollPositions) {
if (now - val.timestamp > SCROLL_CACHE_TTL) {
fileScrollPositions.delete(key)
}
}
// LRU超出上限时删除最旧的
if (fileScrollPositions.size > MAX_SCROLL_CACHE) {
let oldestKey = null
let oldestTime = Infinity
for (const [key, val] of fileScrollPositions) {
if (val.timestamp < oldestTime) {
oldestTime = val.timestamp
oldestKey = key
}
}
if (oldestKey) fileScrollPositions.delete(oldestKey)
}
}
// 使用 Compartment 实现动态切换,避免重建编辑器
const themeCompartment = new Compartment()
const languageCompartment = new Compartment()
@@ -108,9 +87,6 @@ const createExtensions = () => {
keymap.of(defaultKeymap),
bracketMatching(),
// 查找替换Ctrl+F / Ctrl+H
search(),
// 内容更新监听(带防抖)
EditorView.updateListener.of((update) => {
if (update.docChanged) {
@@ -122,7 +98,7 @@ const createExtensions = () => {
EditorView.theme({
'&': { height: '100%', fontSize: '13px' },
'.cm-scroller': { overflow: 'auto', fontFamily: 'Consolas, Monaco, Courier New, monospace' },
'.cm-content': { padding: '8px' },
'.cm-content': { padding: '8px', minHeight: '100%' },
'.cm-line': { padding: '0 0' },
'&.cm-focused': { outline: 'none' }
}),
@@ -167,12 +143,6 @@ const createEditor = (docContent = '') => {
view = new EditorView({ state, parent: editorContainer.value })
// 滚动时防抖保存位置
view.scrollDOM.addEventListener('scroll', () => {
if (saveScrollTimer) clearTimeout(saveScrollTimer)
saveScrollTimer = setTimeout(saveScrollPosition, 200)
}, { passive: true })
// 初始化语言
initLanguage()
}
@@ -193,10 +163,8 @@ onMounted(() => {
})
onBeforeUnmount(() => {
if (emitTimeout) clearTimeout(emitTimeout)
if (saveScrollTimer) clearTimeout(saveScrollTimer)
if (view?.scrollDOM) {
view.scrollDOM.removeEventListener('scroll', saveScrollPosition)
if (emitTimeout) {
clearTimeout(emitTimeout)
}
view?.destroy()
view = null
@@ -204,64 +172,12 @@ onBeforeUnmount(() => {
// ==================== 监听器 ====================
// 保存当前文件滚动位置(防抖)
const saveScrollPosition = () => {
if (!view || !currentFilePath) return
const scroller = view.scrollDOM
if (!scroller) return
fileScrollPositions.set(currentFilePath, {
scrollTop: scroller.scrollTop,
anchor: view.state.selection.main.anchor,
timestamp: Date.now()
})
cleanScrollCache()
}
// 监听外部内容变化(切换文件/文件变更时触发)
watch([() => props.modelValue, () => props.fileMtime], ([newValue, newMtime], [oldValue, oldMtime]) => {
// 文件修改时间变了 → 说明磁盘内容有变更 → 强制刷新
const mtimeChanged = newMtime && oldMtime && newMtime !== oldMtime
if (view && (mtimeChanged || newValue !== view.state.doc.toString())) {
// 先保存旧文件的滚动位置
saveScrollPosition()
const newPath = props.filePath || ''
const isSameFile = currentFilePath && currentFilePath === newPath
// 监听外部内容变化
watch(() => props.modelValue, (newValue) => {
if (view && newValue !== view.state.doc.toString()) {
view.dispatch({
changes: { from: 0, to: view.state.doc.length, insert: newValue || '' },
selection: { anchor: 0 }
changes: { from: 0, to: view.state.doc.length, insert: newValue || '' }
})
currentFilePath = newPath
if (isSameFile && fileScrollPositions.has(newPath)) {
// 同一文件 → 检查是否过期,未过期则恢复位置
const saved = fileScrollPositions.get(newPath)
if (saved && Date.now() - saved.timestamp <= SCROLL_CACHE_TTL) {
nextTick(() => {
if (view) {
view.dispatch({
selection: { anchor: saved.anchor },
effects: EditorView.scrollIntoView(saved.anchor)
})
view.scrollDOM.scrollTop = saved.scrollTop
}
})
} else {
// 过期了 → 强制滚动到顶部
nextTick(() => {
if (view) view.scrollDOM.scrollTop = 0
})
}
} else {
// 不同文件 → 强制滚动到顶部scrollIntoView 不一定重置 DOM scrollTop
nextTick(() => {
if (view) {
view.scrollDOM.scrollTop = 0
}
})
}
}
})
@@ -298,6 +214,6 @@ watch(() => props.fileExtension, () => {
}
.codemirror-editor :deep(.cm-content) {
/* 不设 height让 CodeMirror 虚拟滚动自行计算文档高度 */
height: 100%;
}
</style>

View File

@@ -155,8 +155,6 @@
<AsyncCodeEditor
:model-value="config.fileContent"
:file-extension="config.currentFileExtension"
:file-path="config.currentFileFullPath"
:file-mtime="config.fileMtime"
@update:model-value="handleContentUpdate"
class="code-editor"
/>
@@ -220,8 +218,6 @@
<AsyncCodeEditor
:model-value="config.fileContent"
:file-extension="config.currentFileExtension"
:file-path="config.currentFileFullPath"
:file-mtime="config.fileMtime"
@update:model-value="handleContentUpdate"
class="code-editor"
/>
@@ -288,8 +284,6 @@
<AsyncCodeEditor
:model-value="config.fileContent"
:file-extension="config.currentFileExtension"
:file-path="config.currentFileFullPath"
:file-mtime="config.fileMtime"
@update:model-value="handleContentUpdate"
class="code-editor"
/>
@@ -343,8 +337,6 @@
<AsyncCodeEditor
:model-value="config.fileContent"
:file-extension="config.currentFileExtension"
:file-path="config.currentFileFullPath"
:file-mtime="config.fileMtime"
@update:model-value="handleContentUpdate"
class="code-editor"
/>
@@ -438,7 +430,7 @@ const htmlPreviewUrl = computed(() => {
return ''
}
const encodedPath = encodeURIComponent(props.config.currentFileFullPath)
return `http://localhost:8073/localfs/html-preview?path=${encodedPath}`
return `http://localhost:18765/localfs/html-preview?path=${encodedPath}`
})
// 计算属性:判断文件是否在当前目录
@@ -783,11 +775,11 @@ watch([markdownPreviewRef, () => props.config.isEditMode], ([refVal, isEditMode]
// 处理 HTML iframe 发送的消息(链接点击)
const handleHtmlIframeMessage = (event: MessageEvent) => {
// 安全检查:接受来自本地文件服务器或同源的消息
// Wails 应用的 origin 可能是 wails://...,而 iframe 来自 http://localhost:8073
// Wails 应用的 origin 可能是 wails://...,而 iframe 来自 http://localhost:18765
const allowedOrigins = [
window.location.origin,
'null', // about:blank 或 data: URL
'http://localhost:8073', // 本地文件服务器
'http://localhost:18765', // 本地文件服务器
]
if (!allowedOrigins.includes(event.origin)) {
return

View File

@@ -4,7 +4,7 @@
<span class="panel-title">📋 文件列表</span>
<div class="panel-header-right">
<span class="panel-count">{{ config.fileList.length }} </span>
<a-popover trigger="click" position="bottom" :content-style="{ padding: '4px', width: '170px' }">
<a-popover trigger="click" position="bottom" :content-style="{ padding: '4px', width: '150px' }">
<a-button size="mini" type="text" class="settings-btn">
<icon-more />
</a-button>
@@ -31,18 +31,6 @@
:disabled="col.key === 'name' && visibleCount <= 1"
@change="(val: boolean) => toggleColumn(col.key, val)"
>{{ col.label }}</a-checkbox>
<!-- 可排序列点击图标排序 -->
<span
v-if="colSortMap[col.key]"
class="col-sort-icon"
:class="{ 'col-sort-active': sortBy === colSortMap[col.key] }"
:title="sortBy === colSortMap[col.key] ? (sortOrder === 'asc' ? '升序 → 点击降序' : '降序 → 点击升序') : `按${col.label}排序`"
@click.stop="emit('sort', colSortMap[col.key])"
>
<IconSort v-if="sortBy !== colSortMap[col.key]" />
<IconSortAscending v-else-if="sortOrder === 'asc'" />
<IconSortDescending v-else />
</span>
</div>
</template>
</a-popover>
@@ -113,14 +101,6 @@ interface Emits {
const emit = defineEmits<Emits>()
// 列 key → 排序字段映射
const colSortMap: Record<string, string> = {
icon: 'type',
name: 'name',
time: 'modified_time',
size: 'size'
}
// ========== 列配置(支持显隐 + 排序) ==========
const COL_SETTINGS_KEY = STORAGE_KEYS.FILESYSTEM.COL_SETTINGS
const SHOW_HEADER_KEY = STORAGE_KEYS.FILESYSTEM.SHOW_HEADER
@@ -159,7 +139,6 @@ function loadColSettings(): ColumnConfig[] {
}
const colSettings = ref<ColumnConfig[]>(loadColSettings())
// 默认显示表头localStorage 无值时兼容旧行为)
const showHeader = ref(localStorage.getItem(SHOW_HEADER_KEY) !== 'false')
// 手动持久化(避免 deep watch 频繁写入)
@@ -403,25 +382,6 @@ defineExpose({ focusEditingItem })
color: var(--color-text-2);
}
/* 列项排序图标 */
.col-sort-icon {
margin-left: auto;
font-size: 12px;
color: var(--color-text-4);
cursor: pointer;
padding: 2px;
border-radius: 3px;
flex-shrink: 0;
transition: all 0.15s;
}
.col-sort-icon:hover {
background: var(--color-fill-2);
color: var(--color-text-2);
}
.col-sort-active {
color: rgb(var(--primary-6));
}
/* 滚动容器 */
.file-list-wrapper {
flex: 1;

View File

@@ -27,22 +27,6 @@
</div>
<!-- 正常模式面包屑导航 -->
<div v-else class="path-breadcrumb-wrapper">
<!-- 快捷访问仅图标面包屑前 -->
<a-dropdown>
<a-button size="mini" type="text">
<template #icon><icon-forward /></template>
</a-button>
<template #content>
<a-doption
v-for="shortcut in config.commonPaths"
:key="shortcut.path"
@click="handleGoToPath(shortcut.path)"
>
<template #icon>{{ (shortcut.name || '').split(' ')[0] }}</template>
{{ (shortcut.name || '').substring(2) }}
</a-doption>
</template>
</a-dropdown>
<PathBreadcrumb
:path="config.filePath"
@navigate="handleGoToPath"
@@ -65,17 +49,45 @@
</div>
<div class="toolbar-right">
<!-- 搜索框 -->
<a-input-search
:model-value="config.searchKeyword"
placeholder="搜索文件..."
size="small"
class="toolbar-search"
allow-clear
@search="handleSearch"
@update:model-value="handleSearchInput"
@keyup.escape="handleClearSearch"
/>
<!-- 快捷路径下拉 -->
<a-dropdown v-if="!config.isBrowsingZip">
<a-button size="small">
<template #icon>
<icon-forward />
</template>
快捷访问
</a-button>
<template #content>
<a-doption
v-for="shortcut in config.commonPaths"
:key="shortcut.path"
@click="handleGoToPath(shortcut.path)"
>
<template #icon>{{ (shortcut.name || '').split(' ')[0] }}</template>
{{ (shortcut.name || '').substring(2) }}
</a-doption>
</template>
</a-dropdown>
<!-- 历史记录下拉 -->
<a-dropdown>
<a-button size="small">
<template #icon>
<icon-history />
</template>
历史
</a-button>
<template #content>
<a-doption
v-for="path in config.pathHistory.slice(0, 10)"
:key="path"
@click="handleGoToPath(path)"
>
{{ path }}
</a-doption>
<a-doption v-if="config.pathHistory.length === 0" disabled>暂无历史</a-doption>
</template>
</a-dropdown>
<!-- 刷新按钮 -->
<a-button
@@ -89,29 +101,6 @@
刷新
</a-button>
<!-- 历史记录下拉仅图标Ctrl+H -->
<a-dropdown
v-model:popup-visible="historyPopupVisible"
>
<a-tooltip content="历史记录 (Ctrl+H)" position="left">
<a-button size="small">
<template #icon><icon-history /></template>
</a-button>
</a-tooltip>
<template #content>
<div class="history-dropdown-content">
<a-doption
v-for="path in config.pathHistory.slice(0, 10)"
:key="path"
@click="handleGoToPath(path)"
>
<span class="history-path-text">{{ path }}</span>
</a-doption>
<a-doption v-if="config.pathHistory.length === 0" disabled>暂无历史</a-doption>
</div>
</template>
</a-dropdown>
<!-- 切换侧边栏 -->
<a-button
size="small"
@@ -143,7 +132,6 @@ const props = defineProps<Props>()
interface Emits {
(e: 'update:filePath', path: string): void
(e: 'update:showSidebar', show: boolean): void
(e: 'update:searchKeyword', keyword: string): void
(e: 'refresh'): void
(e: 'exitZip'): void
(e: 'goToPath', path: string): void
@@ -153,9 +141,6 @@ interface Emits {
const emit = defineEmits<Emits>()
// 历史记录下拉显隐(供父组件 Ctrl+H 调用)
const historyPopupVisible = ref(false)
// 事件处理
const handleGoToPath = (path: string) => {
emit('goToPath', path)
@@ -185,28 +170,8 @@ const handleToggleSidebar = () => {
emit('update:showSidebar', !props.config.showSidebar)
}
const handleSearch = (keyword: string) => {
emit('update:searchKeyword', keyword)
}
const handleSearchInput = (keyword: string) => {
emit('update:searchKeyword', keyword)
}
const handleClearSearch = () => {
emit('update:searchKeyword', '')
}
// 切换历史记录下拉面板(供父组件 Ctrl+H 调用)
const toggleHistoryDropdown = () => {
historyPopupVisible.value = !historyPopupVisible.value
}
const { copied, copy: copyPath } = useClipboardCopy()
// 暴露方法给父组件
defineExpose({ toggleHistoryDropdown })
const handleCopyPath = async () => {
await copyPath(props.config.filePath)
}
@@ -237,11 +202,6 @@ const handleCopyPath = async () => {
flex-shrink: 0;
}
.toolbar-search {
width: 180px;
flex-shrink: 0;
}
.path-input-wrapper {
flex: 1;
min-width: 200px;
@@ -320,19 +280,4 @@ const handleCopyPath = async () => {
overflow: hidden;
text-overflow: ellipsis;
}
/* 历史记录下拉 */
.history-dropdown-content {
max-width: 420px;
max-height: 300px;
overflow-y: auto;
}
.history-path-text {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 380px;
}
</style>

View File

@@ -13,11 +13,10 @@ import {
isTextEditable, isConfigFile
} from '@/utils/fileTypeHelpers'
import { useFileOperations } from './useFileOperations'
import type { FileItem } from '@/types/file-system'
export interface UseFileEditOptions {
currentFilePath?: import('vue').Ref<FileItem | null>
currentDirectory?: import('vue').Ref<string>
currentFilePath?: any
currentDirectory?: any
}
// 文件大小限制5MB
@@ -47,6 +46,9 @@ export function useFileEdit(options: UseFileEditOptions = {}) {
// 文件版本跟踪(用于防止切换文件后的过期更新)
const fileVersion = ref(0)
// 最后一次文件加载的时间戳,用于过滤过期更新
const lastLoadTime = ref(0)
// 使用文件操作 composable
const { readFile, writeFile } = useFileOperations({
onSuccess: (operation, data) => {
@@ -79,7 +81,7 @@ export function useFileEdit(options: UseFileEditOptions = {}) {
* 判断是否为二进制文件(基于扩展名)
* 返回值: true=已知二进制, false=已知非二进制(文本/媒体/Office), null=未知需检测
*/
const isBinaryFileByExt = (filepath: string | FileItem): boolean | null => {
const isBinaryFileByExt = (filepath: any): boolean | null => {
const path = getFilePath(filepath)
const ext = getExt(path)
if (!ext) return null // 无扩展名返回 null表示需要进一步检测
@@ -183,6 +185,9 @@ export function useFileEdit(options: UseFileEditOptions = {}) {
// 增加文件版本号,使之前的过期更新失效
fileVersion.value++
// 记录加载时间戳,用于过滤过期更新
lastLoadTime.value = Date.now()
// 注意:不再清空内容,避免 HTML 预览切换时闪烁
// 新内容加载完成后会直接替换旧内容
@@ -451,33 +456,6 @@ ${ext ? `文件类型: ${fileTypeDesc}\n` : ''}
}
}
/**
* 仅更新文件路径(重命名场景:内容不变,只切换路径关联)
* 迁移草稿 key更新 currentFilePathRef
*/
const updateFilePath = (newPath: string) => {
const oldPath = currentFilePathRef.value
// 迁移草稿(旧 key → 新 key
if (draftKey.value && oldPath !== newPath) {
try {
const draft = localStorage.getItem(draftKey.value)
if (draft) {
const newKey = `${STORAGE_KEYS.FILESYSTEM.FILE_DRAFT}-${newPath}`
localStorage.setItem(newKey, draft)
localStorage.removeItem(draftKey.value)
draftKey.value = newKey
}
} catch (error) {
console.warn('[useFileEdit] 草稿迁移失败:', error)
}
}
// 只更新内部路径字符串引用,不触碰 currentFilePath它是 FileItem 对象,由父组件管理)
// 这样不会触发 watch → clearDraft
currentFilePathRef.value = newPath
}
/**
* 重置文件内容
*/
@@ -523,10 +501,14 @@ ${ext ? `文件类型: ${fileTypeDesc}\n` : ''}
}
/**
* 更新文件内容(仅版本号匹配时接受,防止快速切换文件时旧更新覆盖新内容)
* 更新文件内容
* 注意:需要确保更新后 fileContent 和 originalContent 保持正确的同步关系
*/
const updateContent = (content: string, expectedVersion?: number) => {
// 如果提供了期望的版本号,检查是否匹配
// 这用于防止快速切换文件时,旧文件的防抖更新覆盖新文件的内容
if (expectedVersion !== undefined && expectedVersion !== fileVersion.value) {
// 版本不匹配,这是一个过期的更新,忽略它
console.debug('[useFileEdit] 忽略过期更新(版本不匹配):', {
expected: expectedVersion,
current: fileVersion.value,
@@ -535,9 +517,25 @@ ${ext ? `文件类型: ${fileTypeDesc}\n` : ''}
return
}
// 额外检查:如果更新是在文件加载后的短时间内,可能是过期更新
// 防抖时间是 150ms我们使用 300ms 的安全边际
const timeSinceLoad = Date.now() - lastLoadTime.value
if (timeSinceLoad < 300) {
console.debug('[useFileEdit] 忽略过期更新(时间窗口内):', {
timeSinceLoad,
content: content.substring(0, 50)
})
return
}
// 确保只有在内容真正改变时才更新
if (fileContent.value !== content) {
fileContent.value = content
}
// 自动保存草稿(防抖)
// 实际实现应该使用防抖函数
// saveDraft()
}
/**
@@ -558,6 +556,12 @@ ${ext ? `文件类型: ${fileTypeDesc}\n` : ''}
return filePath.startsWith(currentDirectory.value)
}
// 监听文件内容变化,自动保存草稿
watch(fileContent, () => {
// 实际实现应该使用防抖
// saveDraft()
}, { deep: true })
// 监听文件路径变化,清除草稿
watch(currentFilePath, (newPath, oldPath) => {
if (newPath !== oldPath) {
@@ -600,7 +604,6 @@ ${ext ? `文件类型: ${fileTypeDesc}\n` : ''}
// 其他
resetContent,
clearContent,
updateFilePath,
setEditorHeight,
// 文件类型检查

View File

@@ -29,17 +29,8 @@ export interface UseFilePreviewOptions {
export function useFilePreview(options: UseFilePreviewOptions = {}) {
const { filePath = ref(''), isBrowsingZip = ref(false) } = options
// 文件服务器 URL优先从后端获取,降级到默认值
let _fileServerURL = 'http://localhost:8073'
const initFileServerURL = async () => {
try {
const url = await window.go.main.App.GetFileServerURL()
if (url) _fileServerURL = url
} catch { /* 使用默认值 */ }
}
initFileServerURL()
const getFileServerURL = () => _fileServerURL
// 文件服务器 URL硬编码,与旧版本保持一致
const fileServerURL = 'http://localhost:18765'
// 预览 URL
const previewUrl = ref('')
@@ -54,7 +45,7 @@ export function useFilePreview(options: UseFilePreviewOptions = {}) {
const getPreviewUrl = (path: string): string => {
if (!path) return ''
// 使用与旧版本相同的 URL 格式:/localfs/ 路径 + 规范化路径
return `${getFileServerURL()}/localfs/${normalizeFilePath(path, true)}`
return `${fileServerURL}/localfs/${normalizeFilePath(path, true)}`
}
/**
@@ -197,6 +188,12 @@ export function useFilePreview(options: UseFilePreviewOptions = {}) {
// 文件类型判断(同步,基于扩展名)
getFileType,
isImageFile,
isVideoFile,
isAudioFile,
isPdfFile,
isHtmlFile,
isMarkdownFile,
isPreviewable,
isEditable,

View File

@@ -2,7 +2,6 @@
<div class="file-system-container">
<!-- 顶部工具栏 -->
<Toolbar
ref="toolbarRef"
:config="toolbarConfig"
@update:file-path="handleFilePathUpdate"
@update:show-sidebar="handleSidebarToggle"
@@ -11,7 +10,6 @@
@go-to-path="handleGoToPath"
@open-file="handleOpenFile"
@navigate-to-zip-directory="handleNavigateToZipDirectory"
@update:search-keyword="handleSearchKeywordUpdate"
@show-message="handleShowMessage"
/>
@@ -154,7 +152,6 @@ const isEditableWithPreview = (filename: string): boolean => {
const fileList = ref<FileItem[]>([])
const fileLoading = ref(false)
const selectedFileItem = ref<FileItem | null>(null)
const toolbarRef = ref<InstanceType<typeof import('./components/Toolbar.vue').default> | null>(null)
// 排序状态(带 localStorage 持久化)
const SORT_STORAGE_KEY = STORAGE_KEYS.FILESYSTEM.SORT
@@ -190,18 +187,6 @@ const editingFileName = ref('')
// 侧边栏
const showSidebar = ref(true)
// 搜索
const searchKeyword = ref('')
// 过滤后的文件列表(基于搜索关键词)
const filteredFileList = computed(() => {
const keyword = searchKeyword.value.trim().toLowerCase()
if (!keyword) return fileList.value
return fileList.value.filter(item =>
item.name.toLowerCase().includes(keyword)
)
})
// 面板宽度(带 localStorage 持久化)
const restorePanelWidth = (): { left: number; right: number } => {
try {
@@ -286,7 +271,7 @@ const { previewUrl, updatePreviewUrl, imageLoading, currentImageDimensions, dete
})
// 文件编辑
const { fileContent, originalContent, isEditMode, fileContentHeight, contentChanged, canSaveFile, canResetContent, loadFile, saveFile, resetContent, clearContent, updateFilePath, toggleEditMode, updateContent, setEditorHeight, isBinaryFile: isBinaryFileRef, fileVersion } =
const { fileContent, originalContent, isEditMode, fileContentHeight, contentChanged, canSaveFile, canResetContent, loadFile, saveFile, resetContent, clearContent, toggleEditMode, updateContent, setEditorHeight, isBinaryFile: isBinaryFileRef, fileVersion } =
useFileEdit({
currentFilePath: selectedFileItem,
currentDirectory: filePath
@@ -310,8 +295,7 @@ const toolbarConfig = computed(() => ({
fileLoading: fileLoading.value,
showSidebar: showSidebar.value,
sortBy: sortBy.value,
sortOrder: sortOrder.value,
searchKeyword: searchKeyword.value
sortOrder: sortOrder.value
}))
// 侧边栏配置
@@ -323,7 +307,7 @@ const sidebarConfig = computed(() => ({
// 文件列表面板配置
const fileListPanelConfig = computed(() => ({
fileList: filteredFileList.value,
fileList: fileList.value,
fileLoading: fileLoading.value,
selectedFileItem: selectedFileItem.value,
editingFilePath: editingFilePath.value,
@@ -379,8 +363,7 @@ const fileEditorPanelConfig = computed(() => {
imageLoading: imageLoading.value,
currentImageDimensions: currentImageDimensions.value,
currentFileExtension,
isBinaryFile: isBinaryFileRef.value,
fileMtime: selectedFileItem.value?.modified_time || ''
isBinaryFile: isBinaryFileRef.value
}
})
@@ -399,10 +382,6 @@ const handleRefresh = async () => {
await loadDirectory(filePath.value)
}
const handleSearchKeywordUpdate = (keyword: string) => {
searchKeyword.value = keyword
}
const handleGoToPath = async (path: string) => {
await navigate(path)
}
@@ -640,12 +619,24 @@ const handleSaveEditing = async (oldPath: string, newName: string) => {
}
try {
// 标记是否需要重命名后仅更新路径(内容不变,零闪烁)
let needUpdatePath = false
// 如果重命名的是当前打开的文件,先关闭编辑器和预览
if (selectedFileItem.value?.path === oldPath) {
// 如果是文件(不是文件夹),才需要关闭编辑器
if (!selectedFileItem.value.isDir) {
// 清空编辑器内容
await clearContent()
// 如果重命名的是当前打开的文件
if (selectedFileItem.value?.path === oldPath && !selectedFileItem.value.isDir) {
needUpdatePath = true
// 清空预览URL
if (previewUrl.value) {
previewUrl.value = ''
}
}
// 取消选中状态
selectedFileItem.value = null
// 等待文件句柄释放(文件需要更长时间)
await new Promise(resolve => setTimeout(resolve, 300))
}
const renamedFile = await fileOps.rename(oldPath, trimmedName)
@@ -659,13 +650,6 @@ const handleSaveEditing = async (oldPath: string, newName: string) => {
}
Message.success(`✓ 重命名成功: ${trimmedName}`)
// 仅更新路径关联,不重新加载内容(编辑器内容不变,零闪烁)
if (needUpdatePath && !renamedFile.isDir) {
selectedFileItem.value = renamedFile
updateFilePath(newPath)
updatePreviewUrl(newPath)
}
} catch (error: any) {
// 提取错误信息
let errorMsg = error?.message || error?.toString() || '未知错误'
@@ -1253,26 +1237,15 @@ const handleKeyDown = async (event: KeyboardEvent) => {
return
}
// F5 刷新文件列表 + 重载当前预览文件
// F5 刷新文件列表
if (event.key === 'F5') {
event.preventDefault()
if (filePath.value) {
await loadDirectory(filePath.value)
// 如果有正在预览的文件,同时重新加载其内容(类似重新点击一次)
if (selectedFileItem.value && !selectedFileItem.value.isDir) {
await loadFileContent(selectedFileItem.value.path)
}
loadDirectory(filePath.value)
}
return
}
// Ctrl+H 打开历史记录面板
if ((event.ctrlKey || event.metaKey) && event.key === 'h') {
event.preventDefault()
toolbarRef.value?.toggleHistoryDropdown?.()
return
}
// Ctrl+Shift+C/D/E/F/G/H 快速打开对应盘符
if ((event.ctrlKey || event.metaKey) && event.shiftKey) {
const driveLetter = event.key.toUpperCase()

View File

@@ -6,8 +6,6 @@
import { computed, watch, onMounted, onUnmounted, h, nextTick } from 'vue'
import { Modal, Message, Progress } from '@arco-design/web-vue'
import { useUpdateStore } from '../stores/update'
import { marked } from '../utils/markedExtensions'
import { sanitizeHtml } from '@/utils/fileUtils'
const props = defineProps({
modelValue: {
@@ -68,8 +66,8 @@ const showUpdateModal = () => {
title: forceUpdate.value ? '重要更新' : '发现新版本',
content: () => {
const elements = [
h('div', { style: { marginBottom: '8px' } }, [
h('span', { style: { fontSize: '13px', color: 'var(--color-text-2)' } }, '版本:'),
h('div', { style: { marginBottom: '12px' } }, [
h('span', { style: { fontSize: '14px', color: 'var(--color-text-2)' } }, '版本:'),
h('span', { style: { fontSize: '14px', color: 'var(--color-text-1)', marginLeft: '8px' } }, currentVersion.value),
h('span', { style: { fontSize: '14px', color: 'var(--color-text-2)', marginLeft: '12px', marginRight: '12px' } }, '→'),
h('span', { style: { fontSize: '16px', color: 'rgb(var(--primary-6))', fontWeight: '500' } }, latestVersion.value)
@@ -78,23 +76,20 @@ const showUpdateModal = () => {
// 更新日志
if (changelog.value) {
const changelogHtml = (() => { try { return sanitizeHtml(String(marked.parse(changelog.value))) } catch { return changelog.value } })()
elements.push(
h('div', { style: { marginBottom: '8px' } }, [
h('div', { style: { fontSize: '12px', color: 'var(--color-text-2)', marginBottom: '4px' } }, '更新内容:'),
h('div', { style: { marginBottom: '12px' } }, [
h('div', { style: { fontSize: '13px', color: 'var(--color-text-2)', marginBottom: '8px' } }, '更新内容:'),
h('div', {
style: {
fontSize: '12px',
fontSize: '13px',
color: 'var(--color-text-2)',
lineHeight: '1.6',
padding: '10px 12px',
lineHeight: '1.8',
padding: '12px',
background: 'var(--color-fill-1)',
borderRadius: '4px',
maxHeight: '240px',
overflowY: 'auto'
},
innerHTML: changelogHtml
})
whiteSpace: 'pre-wrap'
}
}, changelog.value)
])
)
}
@@ -109,7 +104,7 @@ const showUpdateModal = () => {
}
if (metadata.length > 0) {
elements.push(
h('div', { style: { marginBottom: '4px', fontSize: '12px', color: 'var(--color-text-3)' } }, metadata.join(' · '))
h('div', { style: { marginBottom: '12px', fontSize: '13px', color: 'var(--color-text-3)' } }, metadata.join(' · '))
)
}

View File

@@ -30,7 +30,7 @@
<div class="changelog-title">
{{ updateInfo.has_update ? '最新版本更新内容' : '当前版本更新内容' }}
</div>
<div class="changelog" v-html="renderChangelog(updateInfo.changelog)" />
<div class="changelog">{{ updateInfo.changelog }}</div>
</div>
</a-card>
@@ -92,8 +92,8 @@
:status="downloadStatus"
/>
<div class="progress-info">
<span>{{ updateStore.formatFileSize(progressInfo.downloaded) }} / {{ updateStore.formatFileSize(progressInfo.total) }}</span>
<span v-if="progressInfo.speed > 0">{{ updateStore.formatSpeed(progressInfo.speed) }}</span>
<span>{{ formatFileSize(progressInfo.downloaded) }} / {{ formatFileSize(progressInfo.total) }}</span>
<span v-if="progressInfo.speed > 0">{{ formatSpeed(progressInfo.speed) }}</span>
</div>
</div>
@@ -118,8 +118,6 @@ import { Message, Modal } from '@arco-design/web-vue'
import { storeToRefs } from 'pinia'
import { IconHistory } from '@arco-design/web-vue/es/icon'
import { useUpdateStore } from '../stores/update'
import { marked } from '../utils/markedExtensions'
import { sanitizeHtml } from '@/utils/fileUtils'
// Emits
defineEmits(['open-version-history'])
@@ -136,10 +134,13 @@ const lastCheckTime = ref('-')
const installResult = ref(null)
const downloadedFile = ref(null)
/** 渲染 changelogMarkdown → HTML */
function renderChangelog(text: string): string {
if (!text) return ''
try { return sanitizeHtml(marked.parse(text) as string) } catch { return text }
// 工具函数
const formatFileSize = (bytes) => {
return updateStore.formatFileSize(bytes)
}
const formatSpeed = (bytesPerSecond) => {
return updateStore.formatSpeed(bytesPerSecond)
}
// 加载当前版本
@@ -282,70 +283,29 @@ onUnmounted(() => {
}
.changelog-section {
margin-top: 12px;
margin-top: 16px;
}
.changelog-title {
font-size: 13px;
font-size: 14px;
font-weight: 500;
color: var(--color-text-2);
margin-bottom: 6px;
color: var(--color-text-1);
margin-bottom: 8px;
}
.changelog {
background: var(--color-fill-1);
padding: 10px 12px;
background: var(--color-fill-2);
padding: 12px;
border-radius: 4px;
margin: 0;
max-height: 280px;
white-space: pre-wrap;
margin: 8px 0;
max-height: 200px;
overflow-y: auto;
font-size: 12px;
line-height: 1.65;
font-size: 13px;
line-height: 1.6;
color: var(--color-text-2);
}
.changelog :deep(h4) {
font-size: 12px;
font-weight: 600;
color: var(--color-text-1);
margin: 8px 0 3px;
}
.changelog :deep(h4:first-child) {
margin-top: 0;
}
.changelog :deep(ul) {
list-style: none;
padding: 0;
margin: 1px 0;
}
.changelog :deep(li) {
position: relative;
padding-left: 14px;
margin: 1px 0;
}
.changelog :deep(li::before) {
content: '·';
position: absolute;
left: 4px;
color: var(--color-text-4);
font-weight: bold;
}
.changelog :deep(code) {
background: var(--color-fill-3);
padding: 0 4px;
border-radius: 3px;
font-size: 11px;
}
.changelog :deep(p) {
margin: 2px 0;
}
.download-progress {
margin-top: 16px;
padding: 16px;

View File

@@ -15,7 +15,7 @@ interface TabConfig {
/**
* 应用配置类型
*/
export interface AppConfig {
interface AppConfig {
tabs: TabConfig[]
visibleTabs: string[]
defaultTab: string

View File

@@ -115,8 +115,6 @@ export interface ToolbarConfig {
sortBy: string
/** 排序方向 */
sortOrder: string
/** 搜索关键词 */
searchKeyword: string
}
/**
@@ -201,8 +199,6 @@ export interface FileEditorPanelConfig {
currentFileExtension: string
/** 是否为二进制文件 */
isBinaryFile: boolean
/** 文件修改时间(用于检测外部变更) */
fileMtime: string
}
/**

View File

@@ -10,7 +10,4 @@ export { defaultKeymap, history, historyKeymap } from '@codemirror/commands'
export { bracketMatching, defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language'
export { oneDark } from '@codemirror/theme-one-dark'
// 查找替换
export { openSearchPanel, closeSearchPanel, search, searchKeymap, SearchQuery } from '@codemirror/search'
// 语言包通过 codeMirrorLoader 动态导入,避免全量打包

View File

@@ -38,21 +38,6 @@ export const escapeHtml = (str) => {
.replace(/'/g, '&#039;')
}
/**
* 轻量 HTML 消毒(用于渲染远程 Markdown 等不可信 HTML 片段)
* 移除 script/iframe/object/embed 标签和 on* 事件属性
*/
export const sanitizeHtml = (html) => {
if (!html) return ''
return String(html)
.replace(/<script\b[^<]*(?:<\/script>|$)/gi, '')
.replace(/<iframe\b[^<]*(?:<\/iframe>|$)/gi, '')
.replace(/<object\b[^<]*(?:<\/object>|$)/gi, '')
.replace(/<embed\b[^>]*\/?>/gi, '')
.replace(/\s+on\w+\s*=\s*["'][^"']*["']/gi, '')
.replace(/\s+on\w+\s*=\s*[^\s>]*/gi, '')
}
/**
* 获取文件扩展名(路径安全)
* @param {string} path - 文件路径