diff --git a/app.go b/app.go index 5b07bf6..e3192fc 100644 --- a/app.go +++ b/app.go @@ -6,16 +6,13 @@ import ( "net/http" "os" "path/filepath" - "regexp" stdruntime "runtime" "strings" "time" - "github.com/jung-kurt/gofpdf" "golang.org/x/sys/windows/registry" "u-desk/internal/api" "u-desk/internal/common" - "u-desk/internal/database" "u-desk/internal/filesystem" "u-desk/internal/service" "u-desk/internal/storage" @@ -24,13 +21,9 @@ import ( "github.com/wailsapp/wails/v2/pkg/runtime" ) -// PDF 有序列表正则(包级变量,避免循环内重复编译) -var orderedListRe = regexp.MustCompile(`^\d+\.\s+`) - // App 应用结构体 type App struct { ctx context.Context - db *database.DB connectionAPI *api.ConnectionAPI sqlAPI *api.SqlAPI tabAPI *api.TabAPI @@ -229,36 +222,6 @@ func (a *App) Shutdown(ctx context.Context) { } } -// QueryUsers 查询用户列表 -func (a *App) QueryUsers(keyword string, status int, role int, organid int, page int, pageSize int, sortField string, sortOrder string) (map[string]interface{}, error) { - db, err := a.getDB() - if err != nil { - return nil, err - } - return db.QueryUsers(keyword, status, role, organid, page, pageSize, sortField, sortOrder) -} - -// getDB 获取数据库连接(延迟加载,按需初始化) -func (a *App) getDB() (*database.DB, error) { - if a.db != nil { - return a.db, nil - } - - // 首次调用时才连接数据库 - db, err := database.Init() - if err != nil { - return nil, fmt.Errorf("数据库连接失败: %v", err) - } - - a.db = db - return db, nil -} - -// Greet 测试方法 -func (a *App) Greet(name string) string { - return "Hello " + name + ", It's show time!" -} - // GetSystemInfo 获取系统信息 func (a *App) GetSystemInfo() (map[string]interface{}, error) { return system.GetSystemInfo() @@ -992,70 +955,3 @@ func (a *App) SelectPDFSaveDirectory() (string, error) { return a.pdfAPI.SelectDirectory() } -// ExportMarkdownToPDF 使用gofpdf导出Markdown为PDF -func (a *App) ExportMarkdownToPDF(markdownContent string) (string, error) { - // 1. 弹出保存对话框 - savePath, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{ - Title: "保存 PDF", - DefaultFilename: "document.pdf", - Filters: []runtime.FileFilter{ - {DisplayName: "PDF 文件", Pattern: "*.pdf"}, - }, - }) - if err != nil || savePath == "" { - return "", err - } - - // 2. 创建PDF - pdf := gofpdf.New("P", "mm", "A4", "") - pdf.AddPage() - pdf.SetAutoPageBreak(true, 15) - - // 3. 解析Markdown并写入PDF - lines := strings.Split(markdownContent, "\n") - for _, line := range lines { - if strings.HasPrefix(line, "# ") { - // H1 标题 - pdf.SetFont("Arial", "B", 24) - pdf.Cell(40, 10, strings.TrimPrefix(line, "# ")) - pdf.Ln(12) - } else if strings.HasPrefix(line, "## ") { - // H2 标题 - pdf.SetFont("Arial", "B", 18) - pdf.Cell(40, 10, strings.TrimPrefix(line, "## ")) - pdf.Ln(10) - } else if strings.HasPrefix(line, "### ") { - // H3 标题 - pdf.SetFont("Arial", "B", 14) - pdf.Cell(40, 10, strings.TrimPrefix(line, "### ")) - pdf.Ln(8) - } else if strings.HasPrefix(line, "- ") || strings.HasPrefix(line, "* ") { - // 无序列表 - pdf.SetFont("Arial", "", 12) - pdf.Cell(10, 7, "•") - pdf.Cell(0, 7, strings.TrimPrefix(line, "- ")) - pdf.Ln(7) - } else if orderedListRe.MatchString(line) { - // 有序列表 - pdf.SetFont("Arial", "", 12) - pdf.Cell(10, 7, strings.TrimSpace(strings.SplitN(line, ".", 2)[0]) + ".") - pdf.Cell(0, 7, strings.TrimSpace(strings.SplitN(line, ".", 2)[1])) - pdf.Ln(7) - } else if line == "" { - // 空行 - pdf.Ln(7) - } else { - // 普通文本 - pdf.SetFont("Arial", "", 12) - pdf.MultiCell(190, 7, line, "", "", false) - } - } - - // 4. 保存文件 - err = pdf.OutputFileAndClose(savePath) - if err != nil { - return "", fmt.Errorf("保存PDF文件失败: %v", err) - } - - return savePath, nil -} diff --git a/go.mod b/go.mod index 5336583..9472fd1 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,6 @@ require ( github.com/chromedp/chromedp v0.14.2 github.com/glebarez/sqlite v1.11.0 github.com/go-sql-driver/mysql v1.9.3 - github.com/jung-kurt/gofpdf v1.16.2 github.com/redis/go-redis/v9 v9.17.3 github.com/shirou/gopsutil/v3 v3.24.5 github.com/wailsapp/wails/v2 v2.12.0 diff --git a/go.sum b/go.sum index 509ec70..ef26d5a 100644 --- a/go.sum +++ b/go.sum @@ -4,7 +4,6 @@ git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc= github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= -github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= @@ -17,7 +16,6 @@ github.com/chromedp/chromedp v0.14.2 h1:r3b/WtwM50RsBZHMUm9fsNhhzRStTHrKdr2zmwbZ github.com/chromedp/chromedp v0.14.2/go.mod h1:rHzAv60xDE7VNy/MYtTUrYreSc0ujt2O1/C3bzctYBo= github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM= github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= @@ -59,9 +57,6 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= -github.com/jung-kurt/gofpdf v1.16.2 h1:jgbatWHfRlPYiK85qgevsZTHviWXKwB1TTiKdz5PtRc= -github.com/jung-kurt/gofpdf v1.16.2/go.mod h1:1hl7y57EsiPAkLbOwzpzqgx1A30nQCk/YmFV8S2vmK0= github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/labstack/echo/v4 v4.15.0 h1:hoRTKWcnR5STXZFe9BmYun9AMTNeSbjHi2vtDuADJ24= @@ -93,10 +88,8 @@ github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOF github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= -github.com/phpdave11/gofpdi v1.0.7/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -110,7 +103,6 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qq github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw= github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= @@ -119,7 +111,6 @@ github.com/shoenig/go-m1cpu v0.1.7 h1:C76Yd0ObKR82W4vhfjZiCp0HxcSZ8Nqd84v+HZ0qyI github.com/shoenig/go-m1cpu v0.1.7/go.mod h1:KkDOw6m3ZJQAPHbrzkZki4hnx+pDRR1Lo+ldA56wD5w= github.com/shoenig/test v1.7.0 h1:eWcHtTXa6QLnBvm0jgEabMRN/uJ4DMV3M8xUGgRkZmk= github.com/shoenig/test v1.7.0/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= @@ -159,7 +150,6 @@ golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU= golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU= -golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= diff --git a/internal/common/constants.go b/internal/common/constants.go index 29df0bb..a8fc91a 100644 --- a/internal/common/constants.go +++ b/internal/common/constants.go @@ -12,6 +12,3 @@ const ( // DefaultVisibleTabs 默认可见的 Tabs var DefaultVisibleTabs = []string{TabDatabase, TabFileSystem, TabDevice} - -// DefaultTab 默认打开的 Tab -const DefaultTab = TabDatabase diff --git a/internal/common/utils.go b/internal/common/utils.go index d4129c9..f9d6170 100644 --- a/internal/common/utils.go +++ b/internal/common/utils.go @@ -61,12 +61,3 @@ func IsWindows() bool { return runtime.GOOS == "windows" } -// IsMac 判断是否为Mac系统 -func IsMac() bool { - return runtime.GOOS == "darwin" -} - -// IsLinux 判断是否为Linux系统 -func IsLinux() bool { - return runtime.GOOS == "linux" -} diff --git a/internal/filesystem/asset_handler.go b/internal/filesystem/asset_handler.go index 544df87..5c9f32a 100644 --- a/internal/filesystem/asset_handler.go +++ b/internal/filesystem/asset_handler.go @@ -2,7 +2,6 @@ package filesystem import ( "context" - "encoding/base64" "fmt" "log" "net/http" @@ -303,52 +302,6 @@ func getContentType(ext string) string { return defaultFileTypeManager.GetMIMEType(ext) } -// ReadFileAsBase64 读取文件并返回 base64 编码的字符串 -// 用于读取从 ZIP 提取的临时图片文件 -func ReadFileAsBase64(filePath string) (string, error) { - log.Printf("[ReadFileAsBase64] 读取文件: %s", filePath) - - if !isSafePath(filePath) { - return "", fmt.Errorf("路径不安全") - } - - // 检查文件是否存在 - fileInfo, err := os.Stat(filePath) - if err != nil { - if os.IsNotExist(err) { - return "", fmt.Errorf("文件不存在: %s", filePath) - } - return "", fmt.Errorf("无法访问文件: %v", err) - } - - log.Printf("[ReadFileAsBase64] 文件大小: %d bytes", fileInfo.Size()) - - // 读取文件 - data, err := os.ReadFile(filePath) - if err != nil { - return "", fmt.Errorf("读取文件失败: %v", err) - } - - // 编码为 base64 - encoded := base64.StdEncoding.EncodeToString(data) - log.Printf("[ReadFileAsBase64] 编码成功: 原始=%d, base64=%d", len(data), len(encoded)) - - // 获取文件扩展名并确定 MIME 类型 - ext := strings.ToLower(filepath.Ext(filePath)) - mimeType := getContentType(ext) - - // 返回 data URI 格式: data:image/png;base64,iVBORw0KG... - return fmt.Sprintf("data:%s;base64,%s", mimeType, encoded), nil -} - -// HandleLocalFile 处理 /localfs/ 路由的 HTTP 请求 -// 前端可以请求 http://localhost:18765/localfs/C:/path/to/image.jpg -// 注意:此函数与 ServeHTTP 功能重复,建议统一使用 ServeHTTP -func HandleLocalFile(w http.ResponseWriter, r *http.Request) { - handler := NewLocalFileHandler() - handler.ServeHTTP(w, r) -} - // isAllowedFileType 检查文件类型是否在白名单中 func isAllowedFileType(ext string) bool { return defaultFileTypeManager.IsAllowed(ext) diff --git a/internal/filesystem/audit_log.go b/internal/filesystem/audit_log.go index 2ad5e5b..7ab97ba 100644 --- a/internal/filesystem/audit_log.go +++ b/internal/filesystem/audit_log.go @@ -220,37 +220,6 @@ func (a *AuditLogger) Close() error { return a.logFile.Close() } -// RotateLog 日志轮转(每天创建新文件) -func (a *AuditLogger) RotateLog() error { - a.mu.Lock() - defer a.mu.Unlock() - - // 刷新缓冲区 - if err := a.flush(); err != nil { - return err - } - - // 关闭当前文件 - if err := a.logFile.Close(); err != nil { - return err - } - - // 生成新的日志文件名 - timestamp := time.Now().Format("2006-01-02") - logPath := filepath.Join(filepath.Dir(a.logPath), fmt.Sprintf("audit_%s.log", timestamp)) - - // 打开新文件 - logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) - if err != nil { - return err - } - - a.logFile = logFile - a.logPath = logPath - - return nil -} - // GetRecentLogs 获取最近的审计日志 func GetRecentLogs(logDir string, limit int) ([]AuditLogEntry, error) { // 读取今天的日志文件 @@ -309,22 +278,8 @@ func parseLines(text string) []string { var globalAuditLogger *AuditLogger var auditLoggerOnce sync.Once -// InitAuditLogger 初始化全局审计日志记录器 -func InitAuditLogger(logDir string) error { - var err error - globalAuditLogger, err = NewAuditLogger(logDir) - return err -} - // GetAuditLogger 获取全局审计日志记录器 func GetAuditLogger() *AuditLogger { return globalAuditLogger } -// CloseAuditLogger 关闭全局审计日志记录器 -func CloseAuditLogger() error { - if globalAuditLogger != nil { - return globalAuditLogger.Close() - } - return nil -} diff --git a/internal/filesystem/constants.go b/internal/filesystem/constants.go index 38113e9..d3a6743 100644 --- a/internal/filesystem/constants.go +++ b/internal/filesystem/constants.go @@ -13,22 +13,13 @@ const ( // HTTP 文件服务大小限制 MaxHTTPFileSize = 500 * 1024 * 1024 // 500MB - HTTP 访问文件最大大小 - - // 删除操作限制 - MaxDeleteSizeGB = 1 * 1024 * 1024 * 1024 // 1GB - 单个文件删除大小限制 - MaxDeleteDirSizeGB = 1 * 1024 * 1024 * 1024 // 1GB - 目录删除大小限制 ) // 时间相关常量 const ( // 审计日志 - AuditFlushInterval = 5 * time.Second // 审计日志刷新间隔 AuditLogBufferSize = 100 // 审计日志缓冲区大小 - // 回收站 - RecycleBinRetentionDays = 30 // 回收站文件保留天数(天) - RecycleBinRetentionPeriod = 30 * 24 * time.Hour // 回收站文件保留期 - // 临时文件 TempFileCleanupAge = 24 * time.Hour // 临时文件清理周期 TempFileDir = "u-desk-zip" // 临时文件目录名 @@ -36,7 +27,6 @@ const ( // 数量限制常量 const ( - MaxDirectoryDepth = 15 // 最大目录深度 MaxFileCount = 1000 // 最大文件数量(目录) ) @@ -48,15 +38,9 @@ const ( // 随机字符串相关常量 const ( - RandomStringCharset = "abcdefghijklmnopqrstuvwxyz0123456789" RandomStringDefaultLength = 6 // 回收站文件名随机后缀长度 ) -// 文件路径相关常量 -const ( - WindowsDriveLength = 2 // Windows 盘符长度 (C:) -) - // 路径遍历检测字符串 const ( PathTraversalPattern = ".." // 路径遍历特征字符串 @@ -69,17 +53,5 @@ const ( FileTypeAudio = "audio" FileTypeDocument = "document" FileTypeText = "text" - FileTypeArchive = "archive" FileTypeApplication = "application" ) - -// 安全相关常量 -const ( - // ZIP 安全 - MinValidZipSize = 22 // ZIP 文件最小有效大小(文件头) - ZipFileHeaderSignature = 0x504B // "PK" - ZIP 文件头签名 - - // 文件锁 - LockCheckMaxRetries = 3 // 文件锁检查最大重试次数 - LockCheckRetryInterval = 100 * time.Millisecond // 文件锁检查重试间隔 -) diff --git a/internal/filesystem/errors.go b/internal/filesystem/errors.go index c262603..a8b42ab 100644 --- a/internal/filesystem/errors.go +++ b/internal/filesystem/errors.go @@ -6,130 +6,6 @@ import ( "runtime" ) -// ErrorCode 错误码类型 -type ErrorCode string - -const ( - // 通用错误 - ErrCodeGeneral ErrorCode = "GENERAL_ERROR" - ErrCodeInvalid ErrorCode = "INVALID_ARGUMENT" - ErrCodeNotFound ErrorCode = "NOT_FOUND" - ErrCodePermission ErrorCode = "PERMISSION_DENIED" - ErrCodeIO ErrorCode = "IO_ERROR" - - // 路径相关错误 - ErrCodePathTraversal ErrorCode = "PATH_TRAVERSAL" - ErrCodeInvalidPath ErrorCode = "INVALID_PATH" - ErrCodeSensitivePath ErrorCode = "SENSITIVE_PATH" - - // 文件操作错误 - ErrCodeFileNotFound ErrorCode = "FILE_NOT_FOUND" - ErrCodeFileExists ErrorCode = "FILE_EXISTS" - ErrCodeDirectoryNotEmpty ErrorCode = "DIRECTORY_NOT_EMPTY" - - // 安全相关错误 - ErrCodeSecurityViolation ErrorCode = "SECURITY_VIOLATION" - ErrCodeSizeLimit ErrorCode = "SIZE_LIMIT_EXCEEDED" - ErrCodeFileLocked ErrorCode = "FILE_LOCKED" - - // ZIP相关错误 - ErrCodeZipInvalid ErrorCode = "ZIP_INVALID" - ErrCodeZipBomb ErrorCode = "ZIP_BOMB" - ErrCodeZipExtract ErrorCode = "ZIP_EXTRACT_FAILED" -) - -// FileError 文件系统专用错误类型 -// 包含详细的错误上下文信息,便于调试和用户提示 -type FileNotFoundError struct { - Path string - Err error -} - -func (e *FileNotFoundError) Error() string { - return fmt.Sprintf("文件不存在: %s", e.Path) -} - -func (e *FileNotFoundError) Unwrap() error { - return e.Err -} - -// PathValidationError 路径验证错误 -type PathValidationError struct { - Path string - Reason string - IsSensitive bool -} - -func (e *PathValidationError) Error() string { - return fmt.Sprintf("路径验证失败: %s - %s", e.Path, e.Reason) -} - -// SecurityViolationError 安全违规错误 -type SecurityViolationError struct { - Path string - Violation string - Suggestion string -} - -func (e *SecurityViolationError) Error() string { - msg := fmt.Sprintf("安全违规: %s - %s", e.Path, e.Violation) - if e.Suggestion != "" { - msg += fmt.Sprintf("\n建议: %s", e.Suggestion) - } - return msg -} - -// SizeLimitError 大小限制错误 -type SizeLimitError struct { - Path string - ActualSize int64 - MaxSize int64 - SizeType string // "file" or "directory" -} - -func (e *SizeLimitError) Error() string { - return fmt.Sprintf("%s大小超限: %s (实际: %.2f GB, 限制: %.2f GB)", - e.SizeType, e.Path, - float64(e.ActualSize)/(1024*1024*1024), - float64(e.MaxSize)/(1024*1024*1024), - ) -} - -// FileLockedError 文件锁定错误 -type FileLockedError struct { - Path string - ProcessInfo string -} - -func (e *FileLockedError) Error() string { - msg := fmt.Sprintf("文件被占用: %s", e.Path) - if e.ProcessInfo != "" { - msg += fmt.Sprintf("\n占用程序: %s", e.ProcessInfo) - } - return msg -} - -// WrapError 错误包装函数 -// 添加上下文信息到错误中 -func WrapError(operation string, path string, err error) error { - return fmt.Errorf("%s 失败: %s - %w", operation, path, err) -} - -// WrapErrorf 格式化错误包装 -func WrapErrorf(format string, args ...interface{}) error { - return fmt.Errorf(format, args...) -} - -// GetStackTrace 获取堆栈跟踪(用于调试) -func GetStackTrace(skip int) string { - buf := make([]byte, 4096) - n := runtime.Stack(buf, false) - if n > 0 { - return string(buf[:n]) - } - return "" -} - // DeleteRestrictionWarning 删除限制警告 // 用于在删除受限文件时提供详细的警告信息 type DeleteRestrictionWarning struct { @@ -141,3 +17,13 @@ type DeleteRestrictionWarning struct { func (w *DeleteRestrictionWarning) Error() string { return fmt.Sprintf("删除限制警告: %s\n%s", w.Path, w.Details) } + +// GetStackTrace 获取堆栈跟踪(用于调试) +func GetStackTrace(skip int) string { + buf := make([]byte, 4096) + n := runtime.Stack(buf, false) + if n > 0 { + return string(buf[:n]) + } + return "" +} diff --git a/internal/filesystem/file_lock.go b/internal/filesystem/file_lock.go index 9772da1..c1e3cf3 100644 --- a/internal/filesystem/file_lock.go +++ b/internal/filesystem/file_lock.go @@ -4,14 +4,6 @@ import ( "fmt" "os" "syscall" - "time" -) - -// Windows API 锁相关函数和常量 -var ( - modkernel32 = syscall.NewLazyDLL("kernel32.dll") - procGetLastError = modkernel32.NewProc("GetLastError") - procGetProcessId = modkernel32.NewProc("GetProcessId") ) // FileLockChecker 文件锁检查器 @@ -102,37 +94,6 @@ func (c *FileLockChecker) getProcessInfo(path string) (string, error) { return "文件正被其他程序使用", nil } -// CheckFileWithRetry 带重试的文件锁检查 -func (c *FileLockChecker) CheckFileWithRetry(path string, maxRetries int, retryInterval time.Duration) error { - for i := 0; i < maxRetries; i++ { - locked, processInfo, err := c.IsFileLocked(path) - if err != nil && !locked { - // 非锁相关的错误,直接返回 - return err - } - - if !locked { - // 文件未被锁定,可以操作 - return nil - } - - // 文件被锁定 - if i < maxRetries-1 { - // 还有重试机会,等待后重试 - time.Sleep(retryInterval) - continue - } - - // 最后一次重试失败,返回错误 - if processInfo != "" { - return fmt.Errorf("文件被占用: %s", processInfo) - } - return fmt.Errorf("文件被其他程序占用,请关闭相关程序后重试") - } - - return fmt.Errorf("文件检查超时") -} - // SafeDeleteWithLockCheck 带锁检查的安全删除 func (c *FileLockChecker) SafeDeleteWithLockCheck(path string) error { // 检查文件是否被锁定 @@ -158,20 +119,6 @@ const ( ERROR_SHARING_VIOLATION = 32 // syscall.Errno(32) ) -// BY_HANDLE_FILE_INFORMATION 文件信息结构体 -type BY_HANDLE_FILE_INFORMATION struct { - FileAttributes uint32 - CreationTime syscall.Filetime - LastAccessTime syscall.Filetime - LastWriteTime syscall.Filetime - VolumeSerialNumber uint32 - FileSizeHigh uint32 - FileSizeLow uint32 - NumberOfLinks uint32 - FileIndexHigh uint32 - FileIndexLow uint32 -} - // contains 检查字符串是否包含子串(不区分大小写) func contains(str, substr string) bool { return len(str) >= len(substr) && (str == substr || len(substr) == 0 || @@ -203,18 +150,3 @@ func containsIgnoreCase(str, substr string) bool { return false } -// 全局文件锁检查器 -var globalLockChecker *FileLockChecker - -// InitFileLockChecker 初始化全局文件锁检查器 -func InitFileLockChecker() { - globalLockChecker = NewFileLockChecker() -} - -// GetFileLockChecker 获取全局文件锁检查器 -func GetFileLockChecker() *FileLockChecker { - if globalLockChecker == nil { - globalLockChecker = NewFileLockChecker() - } - return globalLockChecker -} diff --git a/internal/filesystem/logger.go b/internal/filesystem/logger.go index 618b494..edcfc93 100644 --- a/internal/filesystem/logger.go +++ b/internal/filesystem/logger.go @@ -70,11 +70,6 @@ func (l *Logger) Info(format string, args ...interface{}) { l.log(LogLevelInfo, "INFO", format, args...) } -// Warn 记录警告日志 -func (l *Logger) Warn(format string, args ...interface{}) { - l.log(LogLevelWarn, "WARN", format, args...) -} - // Error 记录错误日志 func (l *Logger) Error(format string, args ...interface{}) { l.log(LogLevelError, "ERROR", format, args...) @@ -141,37 +136,10 @@ func LogError(operation string, path string, err error) { var ( globalLogger *Logger - loggerOnce sync.Once ) -// InitLogger 初始化全局日志记录器 -func InitLogger(logDir string, minLevel LogLevel) error { - var initErr error - loggerOnce.Do(func() { - timestamp := time.Now().Format("2006-01-02") - logPath := filepath.Join(logDir, fmt.Sprintf("filesystem_%s.log", timestamp)) - - logger, err := NewLogger(logPath, minLevel) - if err != nil { - initErr = err - return - } - - globalLogger = logger - log.Printf("[日志系统] 已启动,日志文件: %s", logPath) - }) - return initErr -} - // GetGlobalLogger 获取全局日志记录器 func GetGlobalLogger() *Logger { return globalLogger } -// CloseLogger 关闭全局日志记录器 -func CloseLogger() error { - if globalLogger != nil { - return globalLogger.Close() - } - return nil -} diff --git a/internal/filesystem/recycle_bin.go b/internal/filesystem/recycle_bin.go index b0eac95..379c7e4 100644 --- a/internal/filesystem/recycle_bin.go +++ b/internal/filesystem/recycle_bin.go @@ -376,16 +376,6 @@ func generateRandomString(length int) string { // 全局回收站实例 var globalRecycleBin *RecycleBin -// InitRecycleBin 初始化全局回收站 -func InitRecycleBin(binPath string) error { - bin, err := NewRecycleBin(binPath) - if err != nil { - return err - } - globalRecycleBin = bin - return nil -} - // GetRecycleBin 获取全局回收站实例 func GetRecycleBin() *RecycleBin { return globalRecycleBin diff --git a/internal/filesystem/service.go b/internal/filesystem/service.go index 040056e..a5da202 100644 --- a/internal/filesystem/service.go +++ b/internal/filesystem/service.go @@ -119,11 +119,6 @@ func (s *FileSystemService) initRecycleBin() error { // ========== 核心文件操作 ========== -// Read 读取文件内容(实现 FileService 接口) -func (s *FileSystemService) Read(path string) (string, error) { - return s.ReadFile(path) -} - // ReadFile 读取文件内容(限制最大 10MB) func (s *FileSystemService) ReadFile(path string) (string, error) { // 路径验证 @@ -151,10 +146,6 @@ func (s *FileSystemService) ReadFile(path string) (string, error) { } // Write 写入文件内容(实现 FileService 接口) -func (s *FileSystemService) Write(path, content string) error { - return s.WriteFile(path, content) -} - // writeFile 内部写入实现(路径验证+大小检查+写入+日志) func (s *FileSystemService) writeFileWithLog(path string, data []byte) error { if err := s.validatePath(path); err != nil { @@ -192,31 +183,6 @@ func (s *FileSystemService) SaveBase64File(path, base64Content string) error { return s.writeFileWithLog(path, data) } -// List 列出目录内容(实现 FileService 接口) -func (s *FileSystemService) List(path string) ([]map[string]interface{}, error) { - return s.ListDir(path) -} - -// Open 打开文件(实现 FileService 接口) -func (s *FileSystemService) Open(path string) error { - // 使用系统默认程序打开文件 - var cmd *exec.Cmd - switch runtime.GOOS { - case "windows": - cmd = exec.Command("cmd", "/c", "start", "", path) - case "darwin": - cmd = exec.Command("open", path) - default: - cmd = exec.Command("xdg-open", path) - } - return cmd.Start() -} - -// Delete 删除文件或目录(实现 FileService 接口) -func (s *FileSystemService) Delete(path string) (*FileOperationResult, error) { - return s.DeletePathWithContext(context.Background(), path) -} - // DeletePath 删除文件或目录 func (s *FileSystemService) DeletePath(path string) (*FileOperationResult, error) { return s.DeletePathWithContext(context.Background(), path) @@ -430,11 +396,6 @@ func (s *FileSystemService) CreateFile(path string) (*FileOperationResult, error }, nil } -// GetInfo 获取文件信息(实现 FileService 接口) -func (s *FileSystemService) GetInfo(path string) (map[string]interface{}, error) { - return s.GetFileInfo(path) -} - // GetFileInfo 获取文件信息 func (s *FileSystemService) GetFileInfo(path string) (map[string]interface{}, error) { if err := s.validatePath(path); err != nil { @@ -519,31 +480,16 @@ func (s *FileSystemService) RenamePath(oldPath, newPath string) (*FileOperationR // ========== ZIP操作接口 ========== -// ListZip 列出ZIP文件内容 -func (s *FileSystemService) ListZip(zipPath string) ([]map[string]interface{}, error) { - return ListZipContents(zipPath) -} - // ListZipContents 列出ZIP文件内容(别名,保持向后兼容) func (s *FileSystemService) ListZipContents(zipPath string) ([]map[string]interface{}, error) { return ListZipContents(zipPath) } -// ExtractZipFile 从ZIP提取文件内容 -func (s *FileSystemService) ExtractZipFile(zipPath, filePath string) (string, error) { - return ExtractFileFromZip(zipPath, filePath) -} - // ExtractFileFromZip 从ZIP提取文件内容(别名,保持向后兼容) func (s *FileSystemService) ExtractFileFromZip(zipPath, filePath string) (string, error) { return ExtractFileFromZip(zipPath, filePath) } -// ExtractZipFileToTemp 从ZIP提取文件到临时目录 -func (s *FileSystemService) ExtractZipFileToTemp(zipPath, filePath string) (string, error) { - return ExtractFileFromZipToTemp(zipPath, filePath) -} - // ExtractFileFromZipToTemp 从ZIP提取文件到临时目录(别名,保持向后兼容) func (s *FileSystemService) ExtractFileFromZipToTemp(zipPath, filePath string) (string, error) { return ExtractFileFromZipToTemp(zipPath, filePath) @@ -564,7 +510,9 @@ func getCurrentTimestamp() time.Time { // isInRecycleBin 检查路径是否在回收站中 func isInRecycleBin(path string) bool { recycleBinPath := filepath.Join(common.GetUserDataDir(), "recycle_bin") - return filepath.HasPrefix(filepath.Clean(path), filepath.Clean(recycleBinPath)) + cleanPath := filepath.Clean(path) + cleanBinPath := filepath.Clean(recycleBinPath) + return len(cleanPath) >= len(cleanBinPath) && cleanPath[:len(cleanBinPath)] == cleanBinPath } // ========== 辅助方法 ========== @@ -787,16 +735,3 @@ func GetGlobalService() (*FileSystemService, error) { return globalService, initErr } -// InitGlobalFileSystem 初始化全局文件系统(兼容旧代码) -func InitGlobalFileSystem() error { - _, err := GetGlobalService() - return err -} - -// CloseGlobalFileSystem 关闭全局文件系统 -func CloseGlobalFileSystem(ctx context.Context) error { - if globalService != nil { - return globalService.Close(ctx) - } - return nil -} diff --git a/internal/filesystem/zip.go b/internal/filesystem/zip.go index 7fe8368..c1154fd 100644 --- a/internal/filesystem/zip.go +++ b/internal/filesystem/zip.go @@ -346,46 +346,4 @@ func GetZipFileInfo(zipPath, filePath string) (map[string]interface{}, error) { return result.(map[string]interface{}), nil } -// validateZipFileBasic 验证ZIP文件的基本信息(提取自ListZipContents) -func validateZipFileBasic(zipPath string) error { - if err := validateZipPath(zipPath); err != nil { - return err - } - - fileInfo, err := os.Stat(zipPath) - if err != nil { - return fmt.Errorf("无法访问文件: %v", err) - } - - if fileInfo.Size() < MinValidZipSize { - return fmt.Errorf("文件太小 (%d bytes)", fileInfo.Size()) - } - - if fileInfo.Size() > MaxZipSize { - return fmt.Errorf("ZIP文件过大 (%d bytes)", fileInfo.Size()) - } - - return checkZipFileHeader(zipPath) -} - -// checkZipFileHeader 检查ZIP文件头签名 -func checkZipFileHeader(zipPath string) error { - file, err := os.Open(zipPath) - if err != nil { - return fmt.Errorf("无法打开文件: %v", err) - } - defer file.Close() - - header := make([]byte, 4) - n, err := file.Read(header) - if err != nil || n != 4 { - return fmt.Errorf("无法读取文件头") - } - - if header[0] != 0x50 || header[1] != 0x4B { - return fmt.Errorf("不是有效的 ZIP 文件") - } - - return nil -} diff --git a/internal/filesystem/zip_helper.go b/internal/filesystem/zip_helper.go index a82e460..f817a10 100644 --- a/internal/filesystem/zip_helper.go +++ b/internal/filesystem/zip_helper.go @@ -65,25 +65,6 @@ func isMatchFile(file *zip.File, targetPath string) bool { filepath.Clean(file.Name) == filepath.Clean(targetPath) } -// openZipFileInReader 在ZIP reader中打开指定文件 -// 用于读取文件内容的辅助函数 -func openZipFileInReader(reader *zip.ReadCloser, filePath string) (io.ReadCloser, *zip.File, error) { - for _, file := range reader.File { - if isMatchFile(file, filePath) { - if file.Mode().IsDir() { - return nil, nil, fmt.Errorf("不能读取目录") - } - - rc, err := file.Open() - if err != nil { - return nil, nil, fmt.Errorf("打开 zip 中的文件失败: %v", err) - } - return rc, file, nil - } - } - return nil, nil, fmt.Errorf("文件在 zip 中不存在: %s", filePath) -} - // readAllFromFile 从文件读取所有内容 // 辅助函数,避免重复的 io.ReadAll 调用 func readAllFromFile(rc io.ReadCloser) ([]byte, error) { diff --git a/internal/service/tab_service.go b/internal/service/tab_service.go index 831d828..1e4f491 100644 --- a/internal/service/tab_service.go +++ b/internal/service/tab_service.go @@ -29,8 +29,3 @@ func (s *TabService) SaveTabs(tabs []models.SqlTab) error { func (s *TabService) ListTabs() ([]models.SqlTab, error) { return s.repo.FindAll() } - -// DeleteTab 删除标签页 -func (s *TabService) DeleteTab(id uint) error { - return s.repo.Delete(id) -} diff --git a/internal/service/update_config.go b/internal/service/update_config.go index 5692172..94264dc 100644 --- a/internal/service/update_config.go +++ b/internal/service/update_config.go @@ -102,22 +102,6 @@ func SaveUpdateConfig(config *UpdateConfig) error { return nil } -// ShouldCheckUpdate 判断是否应该检查更新 -func (c *UpdateConfig) ShouldCheckUpdate() bool { - if !c.AutoCheckEnabled { - return false - } - - // 如果从未检查过,应该检查 - if c.LastCheckTime.IsZero() { - return true - } - - // 检查是否超过间隔分钟数 - minutesSinceLastCheck := time.Since(c.LastCheckTime).Minutes() - return minutesSinceLastCheck >= float64(c.CheckIntervalMinutes) -} - // UpdateLastCheckTime 更新最后检查时间 func (c *UpdateConfig) UpdateLastCheckTime() error { c.LastCheckTime = time.Now() diff --git a/internal/service/version.go b/internal/service/version.go index 939f033..6c8a1b9 100644 --- a/internal/service/version.go +++ b/internal/service/version.go @@ -64,11 +64,6 @@ func ParseVersion(versionStr string) (*Version, error) { return &Version{Major: major, Minor: minor, Patch: patch}, nil } -// String 返回版本号字符串(格式:v1.0.0) -func (v *Version) String() string { - return fmt.Sprintf("v%d.%d.%d", v.Major, v.Minor, v.Patch) -} - // Compare 比较版本号 // 返回值:-1 表示当前版本小于目标版本,0 表示相等,1 表示大于 func (v *Version) Compare(other *Version) int { @@ -100,11 +95,6 @@ func (v *Version) IsNewerThan(other *Version) bool { return v.Compare(other) > 0 } -// IsOlderThan 判断是否比目标版本旧 -func (v *Version) IsOlderThan(other *Version) bool { - return v.Compare(other) < 0 -} - // ==================== 版本号获取 ==================== // GetCurrentVersion 获取当前版本号(带缓存) diff --git a/internal/storage/connection_service.go b/internal/storage/connection_service.go deleted file mode 100644 index 63098a0..0000000 --- a/internal/storage/connection_service.go +++ /dev/null @@ -1,279 +0,0 @@ -package storage - -import ( - "context" - "encoding/json" - "fmt" - "u-desk/internal/crypto" - "u-desk/internal/dbclient" - "u-desk/internal/storage/models" - - "gorm.io/gorm" -) - -// ConnectionService 连接管理服务 -type ConnectionService struct { - db *gorm.DB -} - -// NewConnectionService 创建连接服务 -func NewConnectionService() (*ConnectionService, error) { - db := GetDB() - if db == nil { - // 尝试重新初始化 - var err error - db, err = Init() - if err != nil { - return nil, fmt.Errorf("数据库初始化失败: %v", err) - } - } - return &ConnectionService{db: db}, nil -} - -// SaveConnection 保存连接配置 -func (s *ConnectionService) SaveConnection(conn *models.DbConnection) error { - if conn.Name == "" { - return fmt.Errorf("连接名称不能为空") - } - if conn.Type == "" { - return fmt.Errorf("数据库类型不能为空") - } - if conn.Host == "" { - return fmt.Errorf("主机地址不能为空") - } - - // 检查名称是否重复(排除当前记录) - var count int64 - query := s.db.Model(&models.DbConnection{}).Where("name = ?", conn.Name) - if conn.ID > 0 { - query = query.Where("id != ?", conn.ID) - } - query.Count(&count) - if count > 0 { - return fmt.Errorf("连接名称已存在") - } - - if conn.ID > 0 { - // 更新模式 - updateData := map[string]interface{}{ - "name": conn.Name, - "type": conn.Type, - "host": conn.Host, - "port": conn.Port, - "username": conn.Username, - "database": conn.Database, - "options": conn.Options, - "visible_databases": conn.VisibleDatabases, - } - - // 如果提供了新密码,加密后更新 - if conn.Password != "" { - encrypted, err := crypto.EncryptPassword(conn.Password) - if err != nil { - return fmt.Errorf("密码加密失败: %v", err) - } - updateData["password"] = encrypted - } - // 如果密码为空,不更新密码字段(保留原密码) - - return s.db.Model(&models.DbConnection{}).Where("id = ?", conn.ID).Updates(updateData).Error - } - - // 新增模式 - 必须提供密码 - if conn.Password == "" { - return fmt.Errorf("新增连接时密码不能为空") - } - - // 加密密码 - encrypted, err := crypto.EncryptPassword(conn.Password) - if err != nil { - return fmt.Errorf("密码加密失败: %v", err) - } - conn.Password = encrypted - - return s.db.Create(conn).Error -} - -// ListConnections 获取连接列表 -func (s *ConnectionService) ListConnections() ([]models.DbConnection, error) { - var connections []models.DbConnection - err := s.db.Order("created_at DESC").Find(&connections).Error - return connections, err -} - -// GetConnection 获取连接详情 -func (s *ConnectionService) GetConnection(id uint) (*models.DbConnection, error) { - var conn models.DbConnection - err := s.db.First(&conn, id).Error - if err != nil { - return nil, err - } - return &conn, nil -} - -// DeleteConnection 删除连接配置 -func (s *ConnectionService) DeleteConnection(id uint) error { - var conn models.DbConnection - if err := s.db.First(&conn, id).Error; err != nil { - return nil // 连接不存在视为成功 - } - - // 使用事务删除 - return s.db.Transaction(func(tx *gorm.DB) error { - // 清理关联数据 - tx.Where("connection_id = ?", id).Delete(&models.SqlResultHistory{}) - tx.Where("connection_id = ?", id).Delete(&models.SqlTab{}) - - // 删除连接 - if err := tx.Delete(&conn).Error; err != nil { - return err - } - - // 关闭连接池 - dbclient.GetPool().CloseConnection(id, conn.Type) - return nil - }) -} - -// resolvePassword 解析密码(编辑模式下从已保存连接中获取) -func (s *ConnectionService) resolvePassword(id uint, password string) (string, error) { - if id > 0 && password == "" { - conn, err := s.GetConnection(id) - if err != nil { - return "", fmt.Errorf("获取连接信息失败: %v", err) - } - decryptPassword, err := crypto.DecryptPassword(conn.Password) - if err != nil { - return "", fmt.Errorf("密码解密失败: %v", err) - } - return decryptPassword, nil - } - return password, nil -} - -// parseMongoOptions 解析 MongoDB 连接选项 -func parseMongoOptions(options string) (authSource, authMechanism string) { - if options == "" { - return "", "" - } - var opts map[string]interface{} - if err := json.Unmarshal([]byte(options), &opts); err != nil { - return "", "" - } - authSource, _ = opts["authSource"].(string) - authMechanism, _ = opts["authMechanism"].(string) - return authSource, authMechanism -} - -// TestConnection 测试连接(需要根据类型调用不同的测试方法) -func (s *ConnectionService) TestConnection(conn *models.DbConnection) error { - password, err := crypto.DecryptPassword(conn.Password) - if err != nil { - return fmt.Errorf("密码解密失败: %v", err) - } - - authSource, authMechanism := parseMongoOptions(conn.Options) - - return s.testConnectionByType(conn.Type, conn.Host, conn.Port, conn.Username, password, conn.Database, authSource, authMechanism) -} - -// testConnectionByType 根据类型调用对应的测试方法 -func (s *ConnectionService) testConnectionByType(dbType, host string, port int, username, password, database, authSource, authMechanism string) error { - switch dbType { - case "mysql": - return testMySQLConnection(host, port, username, password, database) - case "redis": - return testRedisConnection(host, port, password) - case "mongo": - return testMongoConnection(host, port, username, password, database, authSource, authMechanism) - default: - return fmt.Errorf("不支持的数据库类型: %s", dbType) - } -} - -// testMySQLConnection 测试 MySQL 连接 -func testMySQLConnection(host string, port int, username, password, database string) error { - return dbclient.TestMySQLConnection(host, port, username, password, database) -} - -// testRedisConnection 测试 Redis 连接 -func testRedisConnection(host string, port int, password string) error { - return dbclient.TestRedisConnection(host, port, password) -} - -// testMongoConnection 测试 MongoDB 连接 -func testMongoConnection(host string, port int, username, password, database, authSource, authMechanism string) error { - return dbclient.TestMongoConnectionWithOptions(host, port, username, password, database, authSource, authMechanism) -} - -// TestConnectionWithParams 使用参数测试连接(不保存数据) -func (s *ConnectionService) TestConnectionWithParams(dbType, host string, port int, username, password, database, options string, id uint) error { - password, err := s.resolvePassword(id, password) - if err != nil { - return err - } - - authSource, authMechanism := parseMongoOptions(options) - return s.testConnectionByType(dbType, host, port, username, password, database, authSource, authMechanism) -} - -// LoadAllDatabases 加载全部数据库列表 -func (s *ConnectionService) LoadAllDatabases(dbType, host string, port int, username, password, database, options string, id uint) ([]string, error) { - password, err := s.resolvePassword(id, password) - if err != nil { - return nil, err - } - - authSource, authMechanism := parseMongoOptions(options) - - // 根据类型加载数据库列表 - switch dbType { - case "mysql": - return loadMySQLDatabases(host, port, username, password, database) - case "mongo": - return loadMongoDatabasesWithOptions(host, port, username, password, database, authSource, authMechanism) - case "redis": - // Redis 没有数据库概念,返回空列表 - return []string{}, nil - default: - return nil, fmt.Errorf("不支持的数据库类型: %s", dbType) - } -} - -// loadMySQLDatabases 加载 MySQL 数据库列表 -func loadMySQLDatabases(host string, port int, username, password, defaultDatabase string) ([]string, error) { - config := &dbclient.MySQLConfig{ - Host: host, - Port: port, - Username: username, - Password: password, - Database: defaultDatabase, - } - client, err := dbclient.NewMySQLClient(config) - if err != nil { - return nil, err - } - defer client.Close() - - return client.ListDatabases(context.Background()) -} - -// loadMongoDatabasesWithOptions 加载 MongoDB 数据库列表(使用解析后的选项) -func loadMongoDatabasesWithOptions(host string, port int, username, password, defaultDatabase, authSource, authMechanism string) ([]string, error) { - mongoConfig := &dbclient.MongoConfig{ - Host: host, - Port: port, - Username: username, - Password: password, - Database: defaultDatabase, - AuthSource: authSource, - AuthMechanism: authMechanism, - } - client, err := dbclient.NewMongoClient(mongoConfig) - if err != nil { - return nil, err - } - defer client.Close() - - return client.ListDatabases(context.Background()) -} diff --git a/internal/storage/models/file.go b/internal/storage/models/file.go deleted file mode 100644 index b104ebf..0000000 --- a/internal/storage/models/file.go +++ /dev/null @@ -1,20 +0,0 @@ -package models - -import ( - "time" -) - -// SqlFile SQL 文件记录 -type SqlFile struct { - ID uint `gorm:"primaryKey" json:"id"` - Name string `gorm:"type:varchar(200);not null" json:"name"` // 文件名 - Path string `gorm:"type:varchar(500);not null;uniqueIndex" json:"path"` // 文件路径 - Content string `gorm:"type:text" json:"content"` // 文件内容 - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -// TableName 指定表名 -func (SqlFile) TableName() string { - return "sql_file" -} diff --git a/internal/storage/models/version.go b/internal/storage/models/version.go deleted file mode 100644 index 6ef0a94..0000000 --- a/internal/storage/models/version.go +++ /dev/null @@ -1,20 +0,0 @@ -package models - -import "time" - -// Version 版本信息 -type Version struct { - ID int `gorm:"primaryKey" json:"id"` // 主键ID - Version string `gorm:"type:varchar(20);not null;uniqueIndex" json:"version"` // 版本号(语义化版本,如1.0.0) - DownloadURL string `gorm:"type:varchar(500)" json:"download_url"` // 下载地址(更新包下载URL) - Changelog string `gorm:"type:text" json:"changelog"` // 更新日志(Markdown格式) - ForceUpdate int `gorm:"type:tinyint;not null;default:0" json:"force_update"` // 是否强制更新(1:是 0:否) - ReleaseDate *time.Time `gorm:"type:date" json:"release_date"` // 发布日期 - CreatedAt time.Time `gorm:"autoCreateTime:false" json:"created_at"` // 创建时间(由程序设置) - UpdatedAt time.Time `gorm:"autoUpdateTime:false" json:"updated_at"` // 更新时间(由程序设置) -} - -// TableName 指定表名 -func (Version) TableName() string { - return "sys_version" -} diff --git a/internal/storage/repository/result_repo.go b/internal/storage/repository/result_repo.go index 08b47eb..e9ee2ac 100644 --- a/internal/storage/repository/result_repo.go +++ b/internal/storage/repository/result_repo.go @@ -5,17 +5,13 @@ import ( "u-desk/internal/storage" "u-desk/internal/storage/models" "gorm.io/gorm" - "time" ) type ResultRepository interface { Save(connectionID uint, database, sql string, resultType string, data interface{}, columns []string, rowsAffected int, executionTime int64) (*models.SqlResultHistory, error) FindByID(id uint) (*models.SqlResultHistory, error) - FindByConnection(connectionID uint, limit int) ([]models.SqlResultHistory, error) Search(connectionID *uint, keyword string, limit, offset int) ([]models.SqlResultHistory, int64, error) Delete(id uint) error - DeleteByConnection(connectionID uint) error - DeleteOld(keepDays int) error } type resultRepository struct { @@ -61,15 +57,6 @@ func (r *resultRepository) FindByID(id uint) (*models.SqlResultHistory, error) { return &history, err } -func (r *resultRepository) FindByConnection(connectionID uint, limit int) ([]models.SqlResultHistory, error) { - var histories []models.SqlResultHistory - query := r.db.Where("connection_id = ?", connectionID).Order("created_at DESC") - if limit > 0 { - query = query.Limit(limit) - } - return histories, query.Find(&histories).Error -} - func (r *resultRepository) Search(connectionID *uint, keyword string, limit, offset int) ([]models.SqlResultHistory, int64, error) { query := r.db.Model(&models.SqlResultHistory{}) @@ -101,10 +88,3 @@ func (r *resultRepository) Delete(id uint) error { return r.db.Delete(&models.SqlResultHistory{}, id).Error } -func (r *resultRepository) DeleteByConnection(connectionID uint) error { - return r.db.Where("connection_id = ?", connectionID).Delete(&models.SqlResultHistory{}).Error -} - -func (r *resultRepository) DeleteOld(keepDays int) error { - return r.db.Where("created_at < ?", time.Now().AddDate(0, 0, -keepDays)).Delete(&models.SqlResultHistory{}).Error -} diff --git a/internal/storage/repository/tab_repo.go b/internal/storage/repository/tab_repo.go index 3873daf..2100106 100644 --- a/internal/storage/repository/tab_repo.go +++ b/internal/storage/repository/tab_repo.go @@ -10,7 +10,6 @@ type TabRepository interface { SaveAll(tabs []models.SqlTab) error FindAll() ([]models.SqlTab, error) Delete(id uint) error - DeleteAll() error } type tabRepository struct { @@ -50,6 +49,3 @@ func (r *tabRepository) Delete(id uint) error { return r.db.Delete(&models.SqlTab{}, id).Error } -func (r *tabRepository) DeleteAll() error { - return r.db.Where("1=1").Delete(&models.SqlTab{}).Error -} diff --git a/web/src/components/FileSystem/components/FileEditorPanel.vue b/web/src/components/FileSystem/components/FileEditorPanel.vue index 60a829c..76d66f3 100644 --- a/web/src/components/FileSystem/components/FileEditorPanel.vue +++ b/web/src/components/FileSystem/components/FileEditorPanel.vue @@ -29,16 +29,22 @@ {{ config.currentFileName }} - + + + + + + -
+
{{ config.fileContent }}
@@ -271,7 +277,7 @@
-
+
@@ -346,8 +352,9 @@ diff --git a/web/src/components/FileSystem/components/FileListPanel.vue b/web/src/components/FileSystem/components/FileListPanel.vue index b71ca6a..835dde5 100644 --- a/web/src/components/FileSystem/components/FileListPanel.vue +++ b/web/src/components/FileSystem/components/FileListPanel.vue @@ -55,7 +55,6 @@ :scroll="{ y: 'auto' }" class="file-table" @row-click="handleRowClick" - @row-dblclick="handleRowDoubleClick" @row-contextmenu="handleRowContextMenu" /> diff --git a/web/src/components/FileSystem/components/Toolbar.vue b/web/src/components/FileSystem/components/Toolbar.vue index c084739..b82c255 100644 --- a/web/src/components/FileSystem/components/Toolbar.vue +++ b/web/src/components/FileSystem/components/Toolbar.vue @@ -32,10 +32,17 @@ @navigate="handleGoToPath" @openFile="handleOpenFile" /> - -
- -
+ + + + +
@@ -109,9 +116,10 @@ @@ -233,22 +228,8 @@ const handleCopyPath = async () => { border-color: var(--color-border-2); } -.copy-icon-wrapper { - display: flex; - align-items: center; - justify-content: center; - padding: 4px 8px; - cursor: pointer; - color: var(--color-text-3); - font-size: 14px; - transition: all 0.2s; - border-radius: 4px; - flex-shrink: 0; -} - -.copy-icon-wrapper:hover { - color: rgb(var(--primary-6)); - background: var(--color-fill-2); +.toolbar-copy-btn { + padding: 2px 4px; } .zip-breadcrumb { diff --git a/web/src/components/FileSystem/composables/useClipboardCopy.ts b/web/src/components/FileSystem/composables/useClipboardCopy.ts new file mode 100644 index 0000000..63f1f4c --- /dev/null +++ b/web/src/components/FileSystem/composables/useClipboardCopy.ts @@ -0,0 +1,46 @@ +import { ref } from 'vue' +import { Message } from '@arco-design/web-vue' + +/** + * 拷贝路径 composable(3-tier fallback: Wails native → clipboard API → execCommand) + */ +export function useClipboardCopy() { + const copied = ref(false) + let copyTimer: ReturnType | null = null + + const copy = async (path: string) => { + if (!path || copied.value) return + + try { + if (window.runtime?.ClipboardSetText) { + await window.runtime.ClipboardSetText(path) + } else { + await navigator.clipboard.writeText(path) + } + copied.value = true + } catch { + try { + const input = document.createElement('input') + input.style.position = 'fixed' + input.style.opacity = '0' + input.value = path + document.body.appendChild(input) + input.select() + document.execCommand('copy') + document.body.removeChild(input) + copied.value = true + } catch { + Message.error('复制失败') + } + } + + if (copyTimer) clearTimeout(copyTimer) + copyTimer = setTimeout(() => { copied.value = false }, 2000) + } + + const cleanup = () => { + if (copyTimer) { clearTimeout(copyTimer); copyTimer = null } + } + + return { copied, copy, cleanup } +} diff --git a/web/src/components/MarkdownEditor.vue b/web/src/components/MarkdownEditor.vue index c33acc9..a496e74 100644 --- a/web/src/components/MarkdownEditor.vue +++ b/web/src/components/MarkdownEditor.vue @@ -154,7 +154,6 @@ export default { const isFullscreen = ref(false) const isEditorExpanded = ref(false) const isPreviewExpanded = ref(false) - const showPreview = ref(true) const editorWidthPercent = ref(50) const editorContentRef = ref(null) const previewRef = ref(null) @@ -231,13 +230,10 @@ export default { // 切换功能 const togglePreview = () => { - showPreview.value = !showPreview.value - if (showPreview.value) { - // 恢复预览时重新调整大小 - nextTick(() => { - adjustTextareaHeight() - }) - } + // 预览面板始终显示,保留快捷键兼容性 + nextTick(() => { + adjustTextareaHeight() + }) } const toggleFullscreen = () => { @@ -323,16 +319,6 @@ export default { } }) - // 导出方法 - const getMarkdownContent = () => { - return markdownContent.value - } - - const setMarkdownContent = (content) => { - markdownContent.value = content - hasChanges.value = content !== lastSavedContent.value - } - return { markdownContent, textarea, @@ -343,13 +329,10 @@ export default { isFullscreen, isEditorExpanded, isPreviewExpanded, - showPreview, handleInput, handleKeydown, saveContent, onExportComplete, - getMarkdownContent, - setMarkdownContent, togglePreview, toggleFullscreen, toggleEditorExpand, diff --git a/web/src/stores/theme.ts b/web/src/stores/theme.ts index bb08120..10e97c9 100644 --- a/web/src/stores/theme.ts +++ b/web/src/stores/theme.ts @@ -16,7 +16,6 @@ export const useThemeStore = defineStore('theme', () => { // ==================== 计算属性 ==================== const isDark = computed(() => theme.value === 'dark') - const isLight = computed(() => theme.value === 'light') const tooltipText = computed(() => isDark.value ? '切换到亮色主题' : '切换到夜间主题' ) @@ -53,20 +52,6 @@ export const useThemeStore = defineStore('theme', () => { applyTheme(newTheme) } - /** - * 设置为亮色主题 - */ - const setLightTheme = () => { - applyTheme('light') - } - - /** - * 设置为暗色主题 - */ - const setDarkTheme = () => { - applyTheme('dark') - } - /** * 初始化主题(应用启动时调用) */ @@ -96,16 +81,6 @@ export const useThemeStore = defineStore('theme', () => { systemThemeListener = () => mediaQuery.removeEventListener('change', handleChange) } - /** - * 清理系统主题监听器 - */ - const removeSystemThemeListener = () => { - if (systemThemeListener) { - systemThemeListener() - systemThemeListener = null - } - } - // ==================== 返回 ==================== return { // 状态 @@ -113,14 +88,10 @@ export const useThemeStore = defineStore('theme', () => { // 计算属性 isDark, - isLight, tooltipText, // 方法 toggleTheme, - setLightTheme, - setDarkTheme, - initTheme, - removeSystemThemeListener + initTheme } }) diff --git a/web/src/stores/update.ts b/web/src/stores/update.ts index d8aeaec..c14a436 100644 --- a/web/src/stores/update.ts +++ b/web/src/stores/update.ts @@ -1,6 +1,7 @@ import { defineStore } from 'pinia' -import { ref, reactive } from 'vue' +import { ref } from 'vue' import { Message } from '@arco-design/web-vue' +import { formatBytes as formatFileSize } from '@/utils/fileUtils' /** * 更新管理 Store @@ -38,14 +39,6 @@ export const useUpdateStore = defineStore('update', () => { } } - const formatFileSize = (bytes: number): string => { - if (!bytes || bytes < 0) return '0 B' - const k = 1024 - const sizes = ['B', 'KB', 'MB', 'GB'] - const i = Math.floor(Math.log(bytes) / Math.log(k)) - return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i] - } - const formatSpeed = (bytesPerSecond: number): string => { return formatFileSize(bytesPerSecond) + '/s' } @@ -211,6 +204,8 @@ export const useUpdateStore = defineStore('update', () => { downloadProgress.value = 100 downloadStatus.value = 'success' + const fileSize = (data.file_size as number) || 0 + // 系统通知:下载完成 try { window.runtime?.SendNotification?.({ @@ -218,7 +213,6 @@ export const useUpdateStore = defineStore('update', () => { body: `更新包下载完成 (${formatFileSize(fileSize)}),正在安装...` }) } catch { /* 通知不可用时忽略 */ } - const fileSize = (data.file_size as number) || 0 progressInfo.value = { speed: 0, downloaded: fileSize, @@ -263,13 +257,6 @@ export const useUpdateStore = defineStore('update', () => { window.runtime.EventsOff('download-complete') } - /** - * 关闭更新提示 - */ - const closeUpdateNotification = () => { - showUpdate.value = false - } - // ==================== 返回 ==================== return { // 状态 @@ -288,7 +275,6 @@ export const useUpdateStore = defineStore('update', () => { installUpdate, setupEventListeners, removeEventListeners, - closeUpdateNotification, formatFileSize, formatSpeed } diff --git a/web/src/style.css b/web/src/style.css index 7d6cf9c..443ddf6 100644 --- a/web/src/style.css +++ b/web/src/style.css @@ -50,6 +50,29 @@ body { scrollbar-color: var(--color-border-2, rgba(0, 0, 0, 0.1)) transparent; } +/* 暗色细滚动条(用于编辑器/预览区) */ +.thin-dark-scrollbar { + scrollbar-width: thin; + scrollbar-color: var(--color-border-3) transparent; +} + +.thin-dark-scrollbar::-webkit-scrollbar { + width: 6px; +} + +.thin-dark-scrollbar::-webkit-scrollbar-track { + background: transparent; +} + +.thin-dark-scrollbar::-webkit-scrollbar-thumb { + background: var(--color-border-3); + border-radius: 3px; +} + +.thin-dark-scrollbar::-webkit-scrollbar-thumb:hover { + background: var(--color-border-2); +} + /* Highlight.js CSS */ .hljs { display: block; diff --git a/web/src/utils/codemirrorExports.js b/web/src/utils/codemirrorExports.js index 7977929..3b648b6 100644 --- a/web/src/utils/codemirrorExports.js +++ b/web/src/utils/codemirrorExports.js @@ -4,10 +4,10 @@ */ // Core -export { EditorView, lineNumbers, highlightActiveLineGutter, keymap, drawSelection, dropCursor } from '@codemirror/view' -export { EditorState, Compartment, Facet, StateEffect, StateField } from '@codemirror/state' +export { EditorView, lineNumbers, highlightActiveLineGutter, keymap } from '@codemirror/view' +export { EditorState, Compartment } from '@codemirror/state' export { defaultKeymap, history, historyKeymap } from '@codemirror/commands' -export { bracketMatching, defaultHighlightStyle, syntaxHighlighting, StreamLanguage } from '@codemirror/language' +export { bracketMatching, defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language' export { oneDark } from '@codemirror/theme-one-dark' // 语言包通过 codeMirrorLoader 动态导入,避免全量打包 diff --git a/web/src/utils/constants.js b/web/src/utils/constants.js index 77ccfd5..d62a6a0 100644 --- a/web/src/utils/constants.js +++ b/web/src/utils/constants.js @@ -61,8 +61,6 @@ export const FILE_EXTENSIONS = { // 视频文件 VIDEO_BROWSER: ['mp4', 'webm', 'ogg', 'mov', 'm4v'], // 浏览器原生支持 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'], @@ -96,9 +94,6 @@ export const FILE_EXTENSIONS = { // 纯文本文件 TEXT: ['txt', 'text', 'log', 'md', 'markdown', 'rst', 'adoc', 'tex', 'msg', 'csv', 'tsv'], - // 标记语言文件(用于特殊预览) - MARKUP: ['html', 'htm', 'md', 'markdown'], - // 数据库文件 DATABASE: ['db', 'sqlite', 'mdb', 'accdb'], diff --git a/web/src/utils/fileUtils.js b/web/src/utils/fileUtils.js index 5a23724..3ef5484 100644 --- a/web/src/utils/fileUtils.js +++ b/web/src/utils/fileUtils.js @@ -11,7 +11,7 @@ import { FILE_ICON_MAP, FILE_ICONS, BYTE_UNITS, FILE_SIZE_FORMAT } from './const * 路径分隔符正则(匹配 Windows 和 Unix 风格) * @type {RegExp} */ -export const PATH_SEPARATOR_REGEX = /[/\\]/ +const PATH_SEPARATOR_REGEX = /[/\\]/ /** * 规范化路径分隔符(统一为正斜杠) @@ -93,26 +93,6 @@ export function getFileName(path) { return parts[parts.length - 1] || path } -/** - * 分割路径为多个部分 - * @param {string} path - 文件路径 - * @returns {string[]} 路径数组 - */ -export const splitPath = (path) => { - if (!path) return [] - return path.split(PATH_SEPARATOR_REGEX) -} - -/** - * 获取文件名(不含扩展名) - * @param {string} path - 文件路径 - * @returns {string} 文件名(不含扩展名) - */ -export const getFileNameWithoutExt = (path) => { - const fileName = getFileName(path) - const lastDot = fileName.lastIndexOf('.') - return lastDot > 0 ? fileName.substring(0, lastDot) : fileName -} /** * 根据文件信息获取对应的图标 @@ -177,89 +157,6 @@ export function normalizeFilePath(path, encode = false) { return normalized } -/** - * 获取文件类型的友好名称 - * @param {string} path - 文件路径 - * @returns {string} 文件类型名称 - * - * @example - * getFileTypeName('image.png') // "PNG图片" - * getFileTypeName('document.pdf') // "PDF文档" - * getFileTypeName('unknown.xyz') // "XYZ文件" - */ -export function getFileTypeName(path) { - const ext = getExt(path) - const extUpper = ext.toUpperCase() - - // 图片 - if (['JPG', 'JPEG', 'PNG', 'GIF', 'BMP', 'SVG', 'WEBP'].includes(extUpper)) { - return `${extUpper}图片` - } - - // 视频 - if (['MP4', 'WEBM', 'AVI', 'MKV'].includes(extUpper)) { - return `${extUpper}视频` - } - - // 音频 - if (['MP3', 'WAV', 'FLAC', 'AAC'].includes(extUpper)) { - return `${extUpper}音频` - } - - // PDF - if (extUpper === 'PDF') { - return 'PDF文档' - } - - // 文档 - if (['DOC', 'DOCX', 'XLS', 'XLSX', 'PPT', 'PPTX'].includes(extUpper)) { - return `${extUpper}文档` - } - - // 代码 - if (['JS', 'TS', 'PY', 'JAVA', 'GO', 'RS', 'CPP'].includes(extUpper)) { - return `${extUpper}代码` - } - - // 默认返回扩展名 - return ext ? `${extUpper}文件` : '文件' -} - -/** - * 检查路径是否为绝对路径 - * @param {string} path - 文件路径 - * @returns {boolean} 是否为绝对路径 - * - * @example - * isAbsolutePath('C:\\Users') // true (Windows) - * isAbsolutePath('/home/user') // true (Unix) - * isAbsolutePath('folder/file') // false - */ -export function isAbsolutePath(path) { - if (!path) return false - - // Windows路径:盘符开头 - if (/^[A-Za-z]:\\/.test(path)) return true - - // Unix路径:以 / 开头 - if (path.startsWith('/')) return true - - return false -} - -/** - * 拼接路径片段 - * @param {...string} parts - 路径片段 - * @returns {string} 拼接后的路径 - * - * @example - * joinPaths('/home', 'user', 'docs') // "/home/user/docs" - * joinPaths('C:\\Users', 'user') // "C:\\Users\\user" - */ -export function joinPaths(...parts) { - return parts.join('/').replace(/\/+/g, '/') -} - /** * 获取路径使用的分隔符(Windows 反斜杠或 Unix 正斜杠) * @param {string} path - 文件路径 @@ -301,24 +198,6 @@ export function getParentPath(path) { return parentPath || '/' } -/** - * 清理文件名,移除非法字符 - * @param {string} filename - 原始文件名 - * @param {string} [replacement='_'] - 替换字符 - * @returns {string} 清理后的文件名 - * - * @example - * sanitizeFileName('file/name.txt') // "file_name.txt" - * sanitizeFileName('file:name.txt', '-') // "file-name.txt" - */ -export function sanitizeFileName(filename, replacement = '_') { - if (!filename) return '' - - // Windows不允许的字符: < > : " / \ | ? * - const illegalChars = /[<>:"/\\|?*]/g - - return filename.replace(illegalChars, replacement) -} /** * 文件列表排序:文件夹优先,支持多字段排序 diff --git a/web/src/views/db-cli/components/ConnectionTree.vue b/web/src/views/db-cli/components/ConnectionTree.vue index 6b0bcd0..71d4a94 100644 --- a/web/src/views/db-cli/components/ConnectionTree.vue +++ b/web/src/views/db-cli/components/ConnectionTree.vue @@ -66,7 +66,6 @@ :block-node="true" :default-expand-all="false" :show-line="false" - :load-more="handleLoadMore" :selected-keys="selectedKeys" :expanded-keys="expandedKeys" @select="handleTreeSelect" @@ -586,7 +585,7 @@ const filteredTreeData = computed(() => { }) // 搜索历史管理 -const MAX_HISTORY = 100 // 最多保存10条历史 +const MAX_HISTORY = 100 // 最多保存100条历史 const saveSearchHistory = (text: string) => { if (!text.trim()) return @@ -694,22 +693,29 @@ const handleTreeSelect = (keys, info) => { // 触发节点选择相关事件 emitNodeSelectEvents(nodeData, conn) + + // 选中连接/数据库时自动展开加载子节点(仅在未加载时触发) + if (nodeData.type === 'connection' || nodeData.type === 'database') { + if (!expandedKeys.value.includes(key)) { + expandedKeys.value = [...expandedKeys.value, key] + saveToStorage(STORAGE_KEYS.TREE_EXPANDED_KEYS, expandedKeys.value) + } + // 仅在子节点未加载时才触发展开加载,避免快速切换时的重复请求 + if (!nodeData.children || nodeData.children.length === 0) { + handleNodeExpand(nodeData, true) + } + } } // 树节点展开 const handleTreeExpand = (keys: string[], info) => { - // Arco Design Tree 的 expand 事件参数格式 - // keys: 展开的节点 key 数组 - // info: { expanded: boolean, node: TreeNodeData, e: Event } - // 更新展开状态 expandedKeys.value = keys - + // 保存到localStorage saveToStorage(STORAGE_KEYS.TREE_EXPANDED_KEYS, keys) - + if (!info || !info.node) { - // 如果没有 info.node,尝试从 keys 中获取 if (keys && keys.length > 0) { const lastExpandedKey = keys[keys.length - 1] const nodeData = findNodeByKey(treeData.value, lastExpandedKey) @@ -720,10 +726,10 @@ const handleTreeExpand = (keys: string[], info) => { } return } - + const nodeData = info.node - const isExpanded = info.expanded !== false // 默认为展开 - + const isExpanded = info.expanded !== false + if (isExpanded) { handleNodeExpand(nodeData, true) } @@ -732,14 +738,12 @@ const handleTreeExpand = (keys: string[], info) => { // 处理节点展开逻辑 const handleNodeExpand = (nodeData, isExpanded) => { if (!isExpanded) return - + if (nodeData.type === 'connection') { - // 连接节点:加载数据库列表 if (!nodeData.children || nodeData.children.length === 0) { loadDatabases(nodeData) } } else if (nodeData.type === 'database') { - // 数据库节点:加载表列表 if (!nodeData.children || nodeData.children.length === 0) { loadTables(nodeData) } @@ -759,10 +763,22 @@ const handleLoadMore = (nodeData) => { }) } -// 通用加载节点数据的错误处理包装器 +// 通用加载节点数据的错误处理包装器(带超时保护) +const NODE_LOAD_TIMEOUT = 15000 // 15秒超时 + const withLoadingNode = async (nodeKey: string, loader: () => Promise) => { if (loadingNodes.value.has(nodeKey)) return loadingNodes.value.add(nodeKey) + + // 超时保护:防止无限转圈 + const timer = setTimeout(() => { + if (loadingNodes.value.has(nodeKey)) { + loadingNodes.value.delete(nodeKey) + refreshTreeData() + console.warn(`[ConnectionTree] 节点 ${nodeKey} 加载超时 (${NODE_LOAD_TIMEOUT}ms)`) + } + }, NODE_LOAD_TIMEOUT) + try { await loader() // 强制触发响应式更新 @@ -772,7 +788,9 @@ const withLoadingNode = async (nodeKey: string, loader: () => Promise) => const loadType = nodeKey.startsWith('conn-') ? 'databases' : nodeKey.startsWith('db-') ? 'tables' : 'keys' Message.error(getLoadFailedTip(error, loadType)) + console.error(`[ConnectionTree] 节点 ${nodeKey} 加载失败:`, error) } finally { + clearTimeout(timer) loadingNodes.value.delete(nodeKey) } } diff --git a/web/src/views/db-cli/components/SqlEditor.vue b/web/src/views/db-cli/components/SqlEditor.vue index c69db45..0864372 100644 --- a/web/src/views/db-cli/components/SqlEditor.vue +++ b/web/src/views/db-cli/components/SqlEditor.vue @@ -23,7 +23,6 @@ import {Message} from '@arco-design/web-vue' import { EditorView, keymap, lineNumbers, EditorState, - sql, javascript, defaultKeymap, history, historyKeymap, defaultHighlightStyle, syntaxHighlighting } from '@/utils/codemirrorExports' @@ -55,22 +54,22 @@ const tabPersistence = useTabPersistence() // 数据库类型配置 const DB_CONFIG = { mysql: { - language: () => sql(), + language: async () => (await import('@codemirror/lang-sql')).sql(), defaultContent: 'select 1;', executeText: '执行' }, redis: { - language: () => javascript({ jsx: false, typescript: false }), + language: async () => (await import('@codemirror/lang-javascript')).javascript({ jsx: false, typescript: false }), defaultContent: 'GET key\nSET key value\nHGET hash field', executeText: '执行命令' }, mongo: { - language: () => javascript({ jsx: false, typescript: false }), + language: async () => (await import('@codemirror/lang-javascript')).javascript({ jsx: false, typescript: false }), defaultContent: 'db.collection.find({})\n// 示例:db.users.find({name: "John"})', executeText: '执行查询' }, mongodb: { - language: () => javascript({ jsx: false, typescript: false }), + language: async () => (await import('@codemirror/lang-javascript')).javascript({ jsx: false, typescript: false }), defaultContent: 'db.collection.find({})\n// 示例:db.users.find({name: "John"})', executeText: '执行查询' } @@ -79,7 +78,7 @@ const DB_CONFIG = { // ==================== 工具函数 ==================== const getDbType = () => props.currentConnection?.type?.toLowerCase() || 'mysql' const getDbConfig = (dbType = null) => DB_CONFIG[dbType || getDbType()] || DB_CONFIG.mysql -const getLanguageMode = (dbType = null) => getDbConfig(dbType).language() +const getLanguageMode = async (dbType = null) => getDbConfig(dbType).language() const getDefaultContent = (dbType = null) => getDbConfig(dbType).defaultContent const getExecuteButtonText = () => getDbConfig().executeText @@ -91,9 +90,9 @@ let saveTimer = null const lastExecutionTime = ref(null) // 创建编辑器扩展 -const createEditorExtensions = () => { +const createEditorExtensions = async () => { const dbType = getDbType() - const languageMode = getLanguageMode(dbType) + const languageMode = await getLanguageMode(dbType) return [ EditorState.lineSeparator.of('\n'), @@ -202,7 +201,7 @@ const initEditor = async () => { const state = EditorState.create({ doc: initialContent, - extensions: createEditorExtensions() + extensions: await createEditorExtensions() }) editorView = new EditorView({ diff --git a/web/src/views/db-cli/components/SqlPreviewDialog.vue b/web/src/views/db-cli/components/SqlPreviewDialog.vue index 3b93957..f6e0f9d 100644 --- a/web/src/views/db-cli/components/SqlPreviewDialog.vue +++ b/web/src/views/db-cli/components/SqlPreviewDialog.vue @@ -20,7 +20,6 @@ import { IconCopy } from '@arco-design/web-vue/es/icon' import { EditorView, lineNumbers, EditorState, - sql, defaultHighlightStyle, syntaxHighlighting } from '@/utils/codemirrorExports' @@ -68,6 +67,8 @@ const initEditor = async () => { // 检测是否为暗色主题 const isDark = document.body.hasAttribute('arco-theme') + const { sql } = await import('@codemirror/lang-sql') + const state = EditorState.create({ doc: sqlText, extensions: [ diff --git a/web/src/views/db-cli/index.vue b/web/src/views/db-cli/index.vue index e780ccc..1508fd6 100644 --- a/web/src/views/db-cli/index.vue +++ b/web/src/views/db-cli/index.vue @@ -264,7 +264,11 @@ const sqlPreviewInfo = ref<{ // 编辑器/结果区域高度调整 const loadEditorAreaHeight = (): number => { const saved = localStorage.getItem(STORAGE_KEYS.EDITOR_AREA_HEIGHT) - return saved ? Number(saved) : 50 + if (saved) { + const val = Number(saved) + if (Number.isFinite(val) && val > 0 && val <= 100) return val + } + return 50 } const editorAreaHeight = ref(loadEditorAreaHeight()) const editorAreaPixelHeight = ref(null) @@ -320,7 +324,7 @@ const handleEditorResultDividerMouseDown = (e: MouseEvent) => { if (!(mainLayoutEl instanceof HTMLElement)) return - const resizeHandler = createResizeHandler(mainLayoutEl, () => editorAreaHeight.value, { + const resizeHandler = createResizeHandler(() => mainLayoutEl, () => editorAreaHeight.value, { minPercent: 20, maxPercent: 80, minPixels: 150, diff --git a/web/src/views/db-cli/utils/resize.ts b/web/src/views/db-cli/utils/resize.ts index f05cfb1..be4bb1e 100644 --- a/web/src/views/db-cli/utils/resize.ts +++ b/web/src/views/db-cli/utils/resize.ts @@ -1,35 +1,2 @@ -export interface ResizeOptions { - minPercent?: number - maxPercent?: number - minPixels?: number - onResize?: (percentage: number) => void -} - -export function createResizeHandler( - container: HTMLElement | null, - getInitialPercentage: () => number, - options: ResizeOptions = {} -): (e: MouseEvent) => void { - const { minPercent = 20, maxPercent = 80, minPixels = 150, onResize } = options - - return (e: MouseEvent) => { - if (!container) return - - const handleMouseMove = (moveEvent: MouseEvent) => { - if (!container) return - const rect = container.getBoundingClientRect() - const percentage = ((moveEvent.clientY - rect.top) / rect.height) * 100 - const minPercentFromPixels = (minPixels / rect.height) * 100 - const clamped = Math.max(Math.max(minPercent, minPercentFromPixels), Math.min(maxPercent, percentage)) - onResize?.(clamped) - } - - const handleMouseUp = () => { - document.removeEventListener('mousemove', handleMouseMove) - document.removeEventListener('mouseup', handleMouseUp) - } - - document.addEventListener('mousemove', handleMouseMove) - document.addEventListener('mouseup', handleMouseUp) - } -} +// 保留向后兼容,内部使用通用工具 +export { createResizeHandler, type ResizeOptions } from '../../../utils/resize'