diff --git a/app.go b/app.go index f307971..005eea1 100644 --- a/app.go +++ b/app.go @@ -5,6 +5,7 @@ package main import ( "context" + "encoding/json" "fmt" "os" "path/filepath" @@ -16,6 +17,7 @@ import ( "u-desk/internal/api" "u-desk/internal/common" + "u-desk/internal/filewatch" "u-desk/internal/filesystem" "u-desk/internal/hotkey" osssvc "u-desk/internal/ossdrv" @@ -44,6 +46,7 @@ type App struct { sftpService *sftp.Service ossService *osssvc.Service profileSvc *service.ProfileService + fileWatcher *filewatch.Watcher isAlwaysOnTop bool mu sync.Mutex unregisterHotkey func() @@ -61,6 +64,11 @@ func NewApp() *App { // SetMainWindow 设置主窗口引用(由 main.go 在创建窗口后调用) func (a *App) SetMainWindow(w *application.WebviewWindow) { a.mainWindow = w + a.fileWatcher = filewatch.NewWatcher(func(name string, data ...any) { + if a.mainWindow != nil { + a.mainWindow.EmitEvent(name, data...) + } + }) } // RegisterGlobalHotkey 注册 Ctrl+Shift+B 全局热键(需在窗口创建后调用) @@ -69,11 +77,10 @@ func (a *App) RegisterGlobalHotkey() { return } a.mu.Lock() + defer a.mu.Unlock() if a.unregisterHotkey != nil { - a.mu.Unlock() return } - a.mu.Unlock() hwnd := uintptr(a.mainWindow.NativeWindow()) if hwnd == 0 { fmt.Println("[全局热键] HWND 为 0,注册跳过") @@ -85,9 +92,7 @@ func (a *App) RegisterGlobalHotkey() { return } fmt.Println("[全局热键] Ctrl+Shift+B 已注册") - a.mu.Lock() a.unregisterHotkey = func() { hotkey.Unregister(hwnd, id) } - a.mu.Unlock() } // HandleHotkey 处理全局热键回调:切换 BgmBar 显示/隐藏 @@ -102,6 +107,9 @@ func (a *App) HandleHotkey() { func (a *App) ServiceStartup(ctx context.Context, _ application.ServiceOptions) error { a.ctx = ctx + // dev 模式打开 DevTools + openDevTools(a.mainWindow) + // 1. 核心初始化:SQLite(必须同步,很快) if _, err := storage.InitFast(); err != nil { return fmt.Errorf("SQLite 初始化失败,应用无法启动: %w", err) @@ -310,6 +318,21 @@ func (a *App) ReadFile(path string) (string, error) { return a.filesystem.ReadFile(path) } +// WatchFile 开始监听指定文件的变化,变化时发送 file-changed 事件 +func (a *App) WatchFile(path string) error { + if a.fileWatcher == nil { + return fmt.Errorf("文件监听器未初始化") + } + return a.fileWatcher.WatchFile(path) +} + +// UnwatchFile 停止监听文件变化 +func (a *App) UnwatchFile() { + if a.fileWatcher != nil { + a.fileWatcher.UnwatchFile() + } +} + // WriteFileRequest 写入文件请求结构体 type WriteFileRequest struct { Path string `json:"path"` @@ -435,6 +458,7 @@ func (a *App) ResolveShortcut(lnkPath string) (map[string]interface{}, error) { }, nil } + // getWindowsSpecialFolder 从注册表读取 Windows 特殊文件夹的真实路径 func getWindowsSpecialFolder(guid string, fallbackName string) string { key, err := registry.OpenKey(registry.CURRENT_USER, @@ -1194,28 +1218,9 @@ func (a *App) LoadConnectionProfiles() ([]map[string]interface{}, error) { if err != nil { return nil, err } - result := make([]map[string]interface{}, len(list)) - for i, p := range list { - result[i] = map[string]interface{}{ - "id": float64(p.ID), - "name": p.Name, - "host": p.Host, - "port": p.Port, - "username": p.Username, - "password": p.Password, - "keyPath": p.KeyPath, - "type": p.Type, - "provider": p.Provider, - "token": p.Token, - "accessKey": p.AccessKey, - "secretKey": p.SecretKey, - "bucket": p.Bucket, - "region": p.Region, - "endpoint": p.Endpoint, - "lastConnected": p.LastConnected, - "sortOrder": float64(p.SortOrder), - } - } + var result []map[string]interface{} + data, _ := json.Marshal(list) + json.Unmarshal(data, &result) return result, nil } diff --git a/build/windows/Taskfile.yml b/build/windows/Taskfile.yml index 99c603a..d94318b 100644 --- a/build/windows/Taskfile.yml +++ b/build/windows/Taskfile.yml @@ -48,7 +48,7 @@ tasks: - cmd: rm -f *.syso platforms: [linux, darwin] vars: - BUILD_FLAGS: '{{if eq .DEV "true"}}{{if .EXTRA_TAGS}}-tags {{.EXTRA_TAGS}} {{end}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production,devtools -trimpath -buildvcs=false -ldflags="-w -s -H windowsgui"{{end}}' + BUILD_FLAGS: '{{if eq .DEV "true"}}{{if .EXTRA_TAGS}}-tags {{.EXTRA_TAGS}} {{end}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production -trimpath -buildvcs=false -ldflags="-w -s -H windowsgui"{{end}}' env: GOOS: windows CGO_ENABLED: '{{.CGO_ENABLED | default "0"}}' diff --git a/build/windows/info.json b/build/windows/info.json index 09b3af3..83a1c0d 100644 --- a/build/windows/info.json +++ b/build/windows/info.json @@ -1,15 +1,15 @@ { "fixed": { - "file_version": "0.1.0" + "file_version": "0.4.0" }, "info": { "0000": { - "ProductVersion": "0.1.0", - "CompanyName": "My Company", - "FileDescription": "A u-desk application", - "LegalCopyright": "© 2026, My Company", + "ProductVersion": "0.4.0", + "CompanyName": "1216.top", + "FileDescription": "U-Desk 桌面文件管理器", + "LegalCopyright": "© 2026, 1216.top", "ProductName": "U-Desk", - "Comments": "This is a comment" + "Comments": "桌面文件管理器" } } } \ No newline at end of file diff --git a/cmd/dbread/main.go b/cmd/dbread/main.go new file mode 100644 index 0000000..4cdcdda --- /dev/null +++ b/cmd/dbread/main.go @@ -0,0 +1,53 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + + "github.com/glebarez/sqlite" + "gorm.io/gorm" +) + +func main() { + home, _ := os.UserHomeDir() + dbPath := filepath.Join(home, ".u-desk", "app.db") + + db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{}) + if err != nil { + fmt.Fprintln(os.Stderr, "open db:", err) + os.Exit(1) + } + + // 所有列 + type Profile struct { + ID uint `gorm:"primaryKey"` + Name string `gorm:"column:name"` + Type string `gorm:"column:type"` + Host string `gorm:"column:host"` + Port int `gorm:"column:port"` + Username string `gorm:"column:username"` + Provider string `gorm:"column:provider"` + Token string `gorm:"column:token"` + AccessKey string `gorm:"column:access_key"` + SecretKey string `gorm:"column:secret_key"` + Bucket string `gorm:"column:bucket"` + Region string `gorm:"column:region"` + Endpoint string `gorm:"column:endpoint"` + } + + var profiles []Profile + db.Table("connection_profiles").Find(&profiles) + + // 脱敏 secret_key + for i := range profiles { + if len(profiles[i].SecretKey) > 8 { + profiles[i].SecretKey = profiles[i].SecretKey[:4] + "****" + } + } + + b, _ := json.MarshalIndent(profiles, "", " ") + fmt.Println("=== Connection Profiles ===") + fmt.Println(string(b)) +} diff --git a/devtools.go b/devtools.go new file mode 100644 index 0000000..ca8db88 --- /dev/null +++ b/devtools.go @@ -0,0 +1,16 @@ +//go:build !production + +package main + +import ( + "time" + + "github.com/wailsapp/wails/v3/pkg/application" +) + +func openDevTools(window *application.WebviewWindow) { + go func() { + time.Sleep(2 * time.Second) + window.OpenDevTools() + }() +} diff --git a/devtools_prod.go b/devtools_prod.go new file mode 100644 index 0000000..dd588b7 --- /dev/null +++ b/devtools_prod.go @@ -0,0 +1,7 @@ +//go:build production + +package main + +import "github.com/wailsapp/wails/v3/pkg/application" + +func openDevTools(window *application.WebviewWindow) {} diff --git a/docs/代码改进清单.md b/docs/代码改进清单.md new file mode 100644 index 0000000..12406cb --- /dev/null +++ b/docs/代码改进清单.md @@ -0,0 +1,23 @@ +# 代码改进清单 + +> 基于 Wails 开发文档 vs 项目现状审查,2026-05-16 + +## 已完成 + +- [x] 🔴① `main.go` 添加 `SingleInstance` 单实例锁(`top.1216.udesk`) +- [x] 🔴② `app.go:74-97` RegisterGlobalHotkey 竞态修复(合并为单一 `defer mu.Unlock()`) +- [x] 🔴③ `build/windows/info.json` 版本号 0.1.0→0.4.0,公司名→1216.top +- [x] 🟡⑤ 删除 `frontend/src/wailsjs/wailsjs/` v2 遗留绑定目录 +- [x] 🟡⑥ `LoadConnectionProfiles` 手动 map 转换改用 `json.Marshal/Unmarshal` +- [x] 🟡⑨ `App.vue` onMounted 添加 `contextmenu` 事件拦截(禁用浏览器默认右键菜单) +- [x] 🟡⑫ `main.go` Run() 错误输出改用 `fmt.Fprintf(os.Stderr, ...)` +- [x] Sidebar 设置按钮 `···` 点击无响应修复:移除 `.stop` + Teleport 重构 + 增大点击区域 + +## 待处理 + +- [ ] ④ App 结构体拆分 — 1344 行,应拆为 FilesystemService / ProfileService / BgmService / UpdateService 等 v3 Service +- [ ] ⑦ `internal/api/pdf_api.go:371` SelectDirectory 改用 Wails 原生对话框 `application.Get().Dialog.OpenFile()` +- [ ] ⑧ `app.go:176-189` HWND 轮询改事件驱动 — v3 alpha 暂无对应 API,后续跟进 +- [ ] ⑩ `app.go:29` Windows 专用导入 `golang.org/x/sys/windows/registry` 加构建标签拆到 `*_windows.go` +- [ ] ⑪ 全局结构化日志 — `fmt.Println` 替换为 `log/slog`,按优先级分批替换 +- [ ] ⑬ `app.go:158` 更新检查 URL `https://c.1216.top/last-version.json` 移入配置 diff --git a/frontend/bindings/u-desk/app.ts b/frontend/bindings/u-desk/app.ts index f0189eb..edbdcd5 100644 --- a/frontend/bindings/u-desk/app.ts +++ b/frontend/bindings/u-desk/app.ts @@ -674,6 +674,13 @@ export function SftpWriteFile(req: $models.SftpWriteFileRequest): $CancellablePr return $Call.ByID(2401472593, req); } +/** + * UnwatchFile 停止监听文件变化 + */ +export function UnwatchFile(): $CancellablePromise { + return $Call.ByID(3006906623); +} + /** * VerifyUpdateFile 验证更新文件哈希值 */ @@ -683,6 +690,13 @@ export function VerifyUpdateFile(filePath: string, expectedHash: string, hashTyp }); } +/** + * WatchFile 开始监听指定文件的变化,变化时发送 file-changed 事件 + */ +export function WatchFile(path: string): $CancellablePromise { + return $Call.ByID(325055910, path); +} + /** * WindowClose 关闭窗口 */ diff --git a/frontend/src/App.vue b/frontend/src/App.vue index b32aad0..2061310 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -176,6 +176,9 @@ onMounted(() => { // 禁止 Ctrl+滚轮缩放 document.addEventListener('wheel', preventZoom, { passive: false }) + // 禁用浏览器默认右键菜单(桌面应用体验) + document.addEventListener('contextmenu', e => e.preventDefault()) + // 延迟检查更新(启动后 3 秒,静默模式) setTimeout(() => { updateStore.checkForUpdates(true) diff --git a/frontend/src/api/connection-manager.ts b/frontend/src/api/connection-manager.ts index cdf4da9..584ad64 100644 --- a/frontend/src/api/connection-manager.ts +++ b/frontend/src/api/connection-manager.ts @@ -91,11 +91,14 @@ class ConnectionManagerImpl { try { const list = await LoadConnectionProfiles() if (list && list.length > 0) { - this._profiles = list.map((p: any) => ({ - ...p, - id: String(p.id), - lastConnected: p.lastConnected || p.last_connected ? new Date(p.lastConnected || p.last_connected).getTime() : undefined, - })) + this._profiles = list.map((p: any) => { + const camel = snakeToCamel(p) + return { + ...camel, + id: String(p.id), + lastConnected: camel.lastConnected ? new Date(camel.lastConnected).getTime() : undefined, + } + }) const hasLocal = this._profiles.some(p => p.type === 'local') if (!hasLocal) { this._profiles.unshift({ @@ -105,17 +108,20 @@ class ConnectionManagerImpl { } } catch { /* 首次使用 */ } this.notifyChange() - this._profiles.forEach(p => { - if (p.type === 'local') { this.fetchSystemInfo(p.id).catch(() => {}) } - }) - const autoConnect = localStorage.getItem('desk:autoConnect') - if (autoConnect !== 'false') { - for (const p of this._profiles) { - if (p.type !== 'local') { - this.buildAndPool(String(p.id), p).catch(() => {}) + // 延迟执行系统信息采集和自动连接,不阻塞首屏渲染 + setTimeout(() => { + this._profiles.forEach(p => { + if (p.type === 'local') { this.fetchSystemInfo(p.id).catch(() => {}) } + }) + const autoConnect = localStorage.getItem('desk:autoConnect') + if (autoConnect !== 'false') { + for (const p of this._profiles) { + if (p.type !== 'local') { + this.buildAndPool(String(p.id), p).catch(() => {}) + } } } - } + }, 500) } /** 保存/更新单个 profile 到 SQLite */ @@ -269,9 +275,16 @@ class ConnectionManagerImpl { return } - // 新建连接并入池(成功后再设 activeId) - await this.buildAndPool(profileId, profile) + // 先设 activeId(确保回调读到正确的 activeProfile),失败时回退 + const prevActiveId = this._activeId this._activeId = profileId + try { + await this.buildAndPool(profileId, profile) + } catch (err) { + this._activeId = prevActiveId + this.notifyChange() + throw err + } } /** 断开指定 profile 并从池移除 */ diff --git a/frontend/src/api/system.ts b/frontend/src/api/system.ts index 648e5ec..38933c2 100644 --- a/frontend/src/api/system.ts +++ b/frontend/src/api/system.ts @@ -108,3 +108,20 @@ export async function detectFileTypeByContent(path: string) { export async function getCommonPaths() { return t().getCommonPaths() } + +// 文件监听(仅本地模式,直接调用 Wails 绑定) +export async function watchFile(path: string): Promise { + if (connectionManager.isRemote()) return + try { + const { WatchFile } = await import('../wailsjs/v3-bindings/u-desk/app') + await WatchFile(path) + } catch { /* 忽略绑定未生成的场景 */ } +} + +export async function unwatchFile(): Promise { + if (connectionManager.isRemote()) return + try { + const { UnwatchFile } = await import('../wailsjs/v3-bindings/u-desk/app') + await UnwatchFile() + } catch { /* 忽略 */ } +} diff --git a/frontend/src/assets/logo.png b/frontend/src/assets/logo.png new file mode 100644 index 0000000..3e081d9 Binary files /dev/null and b/frontend/src/assets/logo.png differ diff --git a/frontend/src/components/FileSystem/components/ConnectionIndicator.vue b/frontend/src/components/FileSystem/components/ConnectionIndicator.vue index 939087c..5ac8c79 100644 --- a/frontend/src/components/FileSystem/components/ConnectionIndicator.vue +++ b/frontend/src/components/FileSystem/components/ConnectionIndicator.vue @@ -75,7 +75,7 @@ diff --git a/frontend/src/components/FileSystem/components/Sidebar.vue b/frontend/src/components/FileSystem/components/Sidebar.vue index 2adffb4..bf3d1f2 100644 --- a/frontend/src/components/FileSystem/components/Sidebar.vue +++ b/frontend/src/components/FileSystem/components/Sidebar.vue @@ -3,7 +3,7 @@ + + + + +