feat(query-editor): 支持查询重命名导出与保存快捷键

- 支持已保存查询重命名并同步当前标签标题

- 新增 SQL 文件导出接口、Wails 绑定和浏览器 mock

- 补充 Ctrl/Cmd+S 保存查询与 Ctrl+, 快捷键入口修复

- 覆盖 SQL 编辑器保存、导出和快捷键回归测试
This commit is contained in:
Syngnat
2026-05-31 22:32:48 +08:00
parent e687ae2819
commit 63db9fecb3
11 changed files with 583 additions and 35 deletions

View File

@@ -163,6 +163,67 @@ func writeSQLFileByPath(filePath string, content string) connection.QueryResult
return connection.QueryResult{Success: true, Data: map[string]interface{}{"filePath": target}}
}
func normalizeSQLExportDefaultFilename(rawName string) string {
name := strings.TrimSpace(rawName)
if name == "" {
name = "query"
}
if idx := strings.LastIndexAny(name, `/\`); idx >= 0 {
name = name[idx+1:]
}
if name == "." || name == string(filepath.Separator) {
name = "query"
}
name = strings.NewReplacer(
"/", "_",
"\\", "_",
":", "_",
"*", "_",
"?", "_",
"\"", "_",
"<", "_",
">", "_",
"|", "_",
).Replace(strings.TrimSpace(name))
if name == "" {
name = "query"
}
if !strings.EqualFold(filepath.Ext(name), ".sql") {
name += ".sql"
}
return name
}
func normalizeSQLExportTargetPath(filePath string) string {
target := strings.TrimSpace(filePath)
if target == "" {
return ""
}
if !strings.EqualFold(filepath.Ext(target), ".sql") {
target += ".sql"
}
if abs, err := filepath.Abs(target); err == nil {
target = abs
}
return target
}
func writeExportedSQLFileByPath(filePath string, content string) connection.QueryResult {
target := normalizeSQLExportTargetPath(filePath)
if target == "" {
return connection.QueryResult{Success: false, Message: "文件路径不能为空"}
}
if info, err := os.Stat(target); err == nil && info.IsDir() {
return connection.QueryResult{Success: false, Message: "所选路径不是 SQL 文件"}
} else if err != nil && !os.IsNotExist(err) {
return connection.QueryResult{Success: false, Message: fmt.Sprintf("无法读取文件信息: %v", err)}
}
if err := os.WriteFile(target, []byte(content), 0o644); err != nil {
return connection.QueryResult{Success: false, Message: fmt.Sprintf("无法写入 SQL 文件: %v", err)}
}
return connection.QueryResult{Success: true, Data: map[string]interface{}{"filePath": target}}
}
func buildSQLDirectoryEntries(directory string) ([]SQLDirectoryEntry, error) {
entries, err := os.ReadDir(directory)
if err != nil {
@@ -286,6 +347,31 @@ func (a *App) WriteSQLFile(filePath string, content string) connection.QueryResu
return writeSQLFileByPath(filePath, content)
}
func (a *App) ExportSQLFile(defaultName string, content string) connection.QueryResult {
filename, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
Title: "导出 SQL 文件",
DefaultFilename: normalizeSQLExportDefaultFilename(defaultName),
Filters: []runtime.FileFilter{
{
DisplayName: "SQL Files (*.sql)",
Pattern: "*.sql",
},
{
DisplayName: "All Files (*.*)",
Pattern: "*.*",
},
},
})
if err != nil || strings.TrimSpace(filename) == "" {
return connection.QueryResult{Success: false, Message: "已取消"}
}
result := writeExportedSQLFileByPath(filename, content)
if result.Success {
result.Message = "SQL 文件已导出"
}
return result
}
func normalizeSQLFileExecutionOptions(options sqlFileExecutionOptions) sqlFileExecutionOptions {
if options.BatchMaxStatements <= 0 {
options.BatchMaxStatements = sqlFileBatchMaxStatements

View File

@@ -75,6 +75,45 @@ func TestWriteSQLFileByPathRejectsEmptyPath(t *testing.T) {
}
}
func TestNormalizeSQLExportDefaultFilename(t *testing.T) {
tests := []struct {
name string
raw string
want string
}{
{name: "blank", raw: " ", want: "query.sql"},
{name: "appends extension", raw: "daily report", want: "daily report.sql"},
{name: "keeps sql extension", raw: "report.SQL", want: "report.SQL"},
{name: "uses base name", raw: filepath.Join("folder", "report.sql"), want: "report.sql"},
{name: "replaces invalid chars", raw: `a:b*c?d"e<f>g|h`, want: "a_b_c_d_e_f_g_h.sql"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := normalizeSQLExportDefaultFilename(tt.raw); got != tt.want {
t.Fatalf("expected %q, got %q", tt.want, got)
}
})
}
}
func TestWriteExportedSQLFileByPathCreatesNewSQLFile(t *testing.T) {
filePath := filepath.Join(t.TempDir(), "exported")
result := writeExportedSQLFileByPath(filePath, "select 42;\n")
if !result.Success {
t.Fatalf("expected sql export to succeed, got %#v", result)
}
data, err := os.ReadFile(filePath + ".sql")
if err != nil {
t.Fatalf("ReadFile returned error: %v", err)
}
if string(data) != "select 42;\n" {
t.Fatalf("expected exported sql content, got %q", string(data))
}
}
func TestReadSQLFileByPathReturnsLargeFileMetadata(t *testing.T) {
filePath := filepath.Join(t.TempDir(), "big.sql")
file, err := os.Create(filePath)