重构:Wails v3 迁移 + 前端目录规范化 + Sidebar滚动优化
- web/ → frontend/ 目录重命名(Wails v3 标准结构) - main.go: Middleware 修复 custom.js 404 + DevTools 延迟启动 - Sidebar: 收藏夹内部独立滚动 + 帮助区块固定底部 - useFavorites.ts: longPressTimer const→let 修复 TypeError - App.vue: Arco Tabs padding-top 覆盖 - build: config.yml / Taskfile.yml 对齐官方模板,devtools build tag - 新增 v3 bindings、vite.config.js、跨平台构建配置 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
163
app.go
163
app.go
@@ -10,6 +10,7 @@ import (
|
||||
"path/filepath"
|
||||
stdruntime "runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/windows/registry"
|
||||
@@ -20,17 +21,21 @@ import (
|
||||
"u-desk/internal/storage"
|
||||
"u-desk/internal/system"
|
||||
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
"github.com/wailsapp/wails/v3/pkg/w32"
|
||||
)
|
||||
|
||||
// App 应用结构体
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
updateAPI *api.UpdateAPI
|
||||
configAPI *api.ConfigAPI
|
||||
pdfAPI *api.PdfAPI
|
||||
filesystem *filesystem.FileSystemService
|
||||
isAlwaysOnTop bool
|
||||
ctx context.Context
|
||||
mainWindow *application.WebviewWindow
|
||||
updateAPI *api.UpdateAPI
|
||||
updateTicker *time.Ticker
|
||||
configAPI *api.ConfigAPI
|
||||
pdfAPI *api.PdfAPI
|
||||
filesystem *filesystem.FileSystemService
|
||||
isAlwaysOnTop bool
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// App 方法命名约定:
|
||||
@@ -42,21 +47,24 @@ func NewApp() *App {
|
||||
return &App{}
|
||||
}
|
||||
|
||||
// Startup 应用启动时调用
|
||||
func (a *App) Startup(ctx context.Context) {
|
||||
// SetMainWindow 设置主窗口引用(由 main.go 在创建窗口后调用)
|
||||
func (a *App) SetMainWindow(w *application.WebviewWindow) {
|
||||
a.mainWindow = w
|
||||
}
|
||||
|
||||
// ServiceStartup Wails v3 服务启动生命周期(替代 v2 的 Startup)
|
||||
func (a *App) ServiceStartup(ctx context.Context, _ application.ServiceOptions) error {
|
||||
a.ctx = ctx
|
||||
|
||||
// 1. 核心初始化:SQLite(必须同步,很快)
|
||||
sqliteDB, err := storage.InitFast()
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("SQLite 初始化失败,应用无法启动: %v", err))
|
||||
if _, err := storage.InitFast(); err != nil {
|
||||
return fmt.Errorf("SQLite 初始化失败,应用无法启动: %w", err)
|
||||
}
|
||||
_ = sqliteDB // 全局 DB 已由 InitFast() 设置
|
||||
|
||||
// 2. 初始化配置服务
|
||||
configService, err := api.NewConfigAPI()
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("配置服务初始化失败: %v", err))
|
||||
return fmt.Errorf("配置服务初始化失败: %w", err)
|
||||
}
|
||||
a.configAPI = configService
|
||||
|
||||
@@ -68,7 +76,6 @@ func (a *App) Startup(ctx context.Context) {
|
||||
pdfAPI, err := api.NewPdfAPI()
|
||||
if err != nil {
|
||||
fmt.Printf("[启动] PDF导出API初始化失败: %v\n", err)
|
||||
// PDF导出失败不应影响应用启动,所以只警告不panic
|
||||
} else {
|
||||
a.pdfAPI = pdfAPI
|
||||
fmt.Println("[启动] PDF导出模块初始化完成")
|
||||
@@ -84,18 +91,27 @@ func (a *App) Startup(ctx context.Context) {
|
||||
|
||||
// 4. 根据配置初始化模块(条件初始化)
|
||||
if err := a.initModulesByConfig(visibleTabs); err != nil {
|
||||
panic(fmt.Sprintf("模块初始化失败: %v", err))
|
||||
return fmt.Errorf("模块初始化失败: %w", err)
|
||||
}
|
||||
|
||||
// 5. 异步初始化:UpdateAPI(涉及网络请求,完全异步)
|
||||
go func() {
|
||||
if updateAPI, err := api.NewUpdateAPI("https://c.1216.top/last-version.json"); err == nil {
|
||||
a.mu.Lock()
|
||||
a.updateAPI = updateAPI
|
||||
a.mu.Unlock()
|
||||
|
||||
a.updateAPI.SetContext(ctx)
|
||||
a.updateAPI.SetEventEmitter(func(name string, data ...any) {
|
||||
if a.mainWindow != nil {
|
||||
a.mainWindow.EmitEvent(name, data...)
|
||||
}
|
||||
})
|
||||
a.startAutoUpdateCheck()
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getVisibleTabs 获取配置中的可见 Tabs
|
||||
@@ -106,20 +122,17 @@ func (a *App) getVisibleTabs() []string {
|
||||
return common.DefaultVisibleTabs
|
||||
}
|
||||
|
||||
// 快速检查成功标识
|
||||
success, ok := config["success"].(bool)
|
||||
if !ok || !success {
|
||||
fmt.Printf("[启动] 配置读取失败,使用默认配置\n")
|
||||
return common.DefaultVisibleTabs
|
||||
}
|
||||
|
||||
// 提取 data
|
||||
data, ok := config["data"].(map[string]interface{})
|
||||
if !ok {
|
||||
return common.DefaultVisibleTabs
|
||||
}
|
||||
|
||||
// 提取 visibleTabs
|
||||
visibleTabsInterface, ok := data["visibleTabs"].([]interface{})
|
||||
if !ok {
|
||||
return common.DefaultVisibleTabs
|
||||
@@ -136,11 +149,9 @@ func (a *App) getVisibleTabs() []string {
|
||||
|
||||
// initModulesByConfig 根据配置初始化模块
|
||||
func (a *App) initModulesByConfig(visibleTabs []string) error {
|
||||
// 检查是否启用文件系统模块
|
||||
if common.Contains(visibleTabs, common.TabFileSystem) {
|
||||
fmt.Println("[启动] 初始化文件系统模块...")
|
||||
|
||||
// 初始化文件系统服务
|
||||
fsConfig := filesystem.DefaultConfig()
|
||||
var err error
|
||||
a.filesystem, err = filesystem.NewFileSystemService(fsConfig)
|
||||
@@ -148,7 +159,6 @@ func (a *App) initModulesByConfig(visibleTabs []string) error {
|
||||
return fmt.Errorf("文件系统服务初始化失败: %w", err)
|
||||
}
|
||||
|
||||
// 异步启动文件服务器
|
||||
go a.startFileServer()
|
||||
|
||||
fmt.Println("[启动] 文件系统模块初始化完成")
|
||||
@@ -161,22 +171,22 @@ func (a *App) initModulesByConfig(visibleTabs []string) error {
|
||||
|
||||
// startFileServer 启动文件服务器
|
||||
func (a *App) startFileServer() {
|
||||
// 启动独立的本地文件服务器(使用 filesystem 包中的实现)
|
||||
if _, err := filesystem.StartLocalFileServer(); err != nil {
|
||||
fmt.Printf("[文件服务器] 启动失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("[文件服务器] 启动在 http://localhost:8073")
|
||||
}
|
||||
|
||||
// Shutdown 应用关闭时调用
|
||||
func (a *App) Shutdown(ctx context.Context) {
|
||||
// 创建带超时的上下文(5秒超时)
|
||||
shutdownCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
// ServiceShutdown Wails v3 服务关闭生命周期(替代 v2 的 Shutdown)
|
||||
func (a *App) ServiceShutdown() error {
|
||||
if a.updateTicker != nil {
|
||||
a.updateTicker.Stop()
|
||||
}
|
||||
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 1. 关闭文件系统服务(优雅关闭,释放资源)
|
||||
if a.filesystem != nil {
|
||||
fmt.Println("[文件系统服务] 正在关闭...")
|
||||
if err := a.filesystem.Close(shutdownCtx); err != nil {
|
||||
@@ -186,13 +196,13 @@ func (a *App) Shutdown(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 停止文件服务器(使用全局服务器的关闭方法)
|
||||
fmt.Println("[文件服务器] 正在关闭...")
|
||||
if err := filesystem.ShutdownLocalFileServer(); err != nil {
|
||||
fmt.Printf("[文件服务器] 关闭失败: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("[文件服务器] 已关闭")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSystemInfo 获取系统信息
|
||||
@@ -307,7 +317,6 @@ func (a *App) ExtractFileFromZip(zipPath, filePath string) (string, error) {
|
||||
}
|
||||
|
||||
// ExtractFileFromZipToTemp 从 zip 文件中提取单个文件到临时目录
|
||||
// 返回临时文件的完整路径,适用于图片等二进制文件
|
||||
func (a *App) ExtractFileFromZipToTemp(zipPath, filePath string) (string, error) {
|
||||
return a.filesystem.ExtractFileFromZipToTemp(zipPath, filePath)
|
||||
}
|
||||
@@ -327,10 +336,8 @@ func (a *App) ResolveShortcut(lnkPath string) (map[string]interface{}, error) {
|
||||
}, err
|
||||
}
|
||||
|
||||
// 获取目标文件信息
|
||||
fileInfo, err := a.filesystem.GetFileInfo(targetPath)
|
||||
if err != nil {
|
||||
// 目标文件不存在或无法访问
|
||||
return map[string]interface{}{
|
||||
"success": true,
|
||||
"targetPath": targetPath,
|
||||
@@ -339,7 +346,6 @@ func (a *App) ResolveShortcut(lnkPath string) (map[string]interface{}, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 返回完整的目标信息
|
||||
return map[string]interface{}{
|
||||
"success": true,
|
||||
"targetPath": targetPath,
|
||||
@@ -350,11 +356,10 @@ func (a *App) ResolveShortcut(lnkPath string) (map[string]interface{}, error) {
|
||||
}
|
||||
|
||||
// getWindowsSpecialFolder 从注册表读取 Windows 特殊文件夹的真实路径
|
||||
// 用户可通过系统设置修改下载/桌面/文档等目录位置,注册表记录实际路径
|
||||
func getWindowsSpecialFolder(guid string, fallbackName string) string {
|
||||
key, err := registry.OpenKey(registry.CURRENT_USER,
|
||||
`Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders`,
|
||||
registry.READ)
|
||||
registry.READ)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
@@ -365,9 +370,7 @@ func getWindowsSpecialFolder(guid string, fallbackName string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 展开 %USERPROFILE% 等环境变量
|
||||
path := os.ExpandEnv(val)
|
||||
// 验证路径存在
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
return ""
|
||||
}
|
||||
@@ -385,7 +388,6 @@ func (a *App) GetCommonPaths() (map[string]string, error) {
|
||||
"home": homeDir,
|
||||
}
|
||||
|
||||
// Windows: 从注册表读取特殊文件夹真实路径(用户可能已修改位置)
|
||||
folderGUIDs := map[string]string{
|
||||
"desktop": "{B4BFCC3A-DB2C-424C-B029-7FE99A87C641}",
|
||||
"documents": "{D20B4C7F-5EA7-424C-B25E-039F6F1FCC8A}",
|
||||
@@ -395,12 +397,10 @@ func (a *App) GetCommonPaths() (map[string]string, error) {
|
||||
if p := getWindowsSpecialFolder(guid, name); p != "" {
|
||||
paths[name] = p
|
||||
} else {
|
||||
// folderGUIDs 的 key 均为 ASCII,无需 Unicode 处理
|
||||
paths[name] = filepath.Join(homeDir, strings.ToUpper(name[:1])+name[1:])
|
||||
}
|
||||
}
|
||||
|
||||
// Windows: 动态添加所有盘符
|
||||
if stdruntime.GOOS == "windows" {
|
||||
for _, drive := range "ABCDEFGHIJKLMNOPQRSTUVWXYZ" {
|
||||
path := string(drive) + ":\\"
|
||||
@@ -416,16 +416,15 @@ func (a *App) GetCommonPaths() (map[string]string, error) {
|
||||
|
||||
// Reload 重新加载窗口(用于菜单项)
|
||||
func (a *App) Reload() {
|
||||
if a.ctx != nil {
|
||||
runtime.WindowReload(a.ctx)
|
||||
if a.mainWindow != nil {
|
||||
a.mainWindow.Reload()
|
||||
}
|
||||
}
|
||||
|
||||
// ClearCache 清理本地缓存(用于菜单项)
|
||||
func (a *App) ClearCache() {
|
||||
if a.ctx != nil {
|
||||
// 发送事件到前端,让前端清理 localStorage
|
||||
runtime.EventsEmit(a.ctx, "clear-cache")
|
||||
if a.mainWindow != nil {
|
||||
a.mainWindow.EmitEvent("clear-cache")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -433,58 +432,72 @@ func (a *App) ClearCache() {
|
||||
|
||||
// WindowMinimize 最小化窗口
|
||||
func (a *App) WindowMinimize() {
|
||||
if a.ctx != nil {
|
||||
runtime.WindowMinimise(a.ctx)
|
||||
if a.mainWindow != nil {
|
||||
a.mainWindow.Minimise()
|
||||
}
|
||||
}
|
||||
|
||||
// WindowMaximize 最大化/还原窗口
|
||||
func (a *App) WindowMaximize() {
|
||||
if a.ctx != nil {
|
||||
if runtime.WindowIsMaximised(a.ctx) {
|
||||
runtime.WindowUnmaximise(a.ctx)
|
||||
} else {
|
||||
runtime.WindowMaximise(a.ctx)
|
||||
}
|
||||
if a.mainWindow == nil {
|
||||
return
|
||||
}
|
||||
if a.mainWindow.IsMaximised() {
|
||||
a.mainWindow.UnMaximise()
|
||||
} else {
|
||||
a.mainWindow.Maximise()
|
||||
}
|
||||
}
|
||||
|
||||
// WindowClose 关闭窗口
|
||||
func (a *App) WindowClose() {
|
||||
if a.ctx != nil {
|
||||
runtime.Quit(a.ctx)
|
||||
}
|
||||
application.Get().Quit()
|
||||
}
|
||||
|
||||
// WindowIsMaximized 检查窗口是否最大化
|
||||
func (a *App) WindowIsMaximized() bool {
|
||||
if a.ctx != nil {
|
||||
return runtime.WindowIsMaximised(a.ctx)
|
||||
if a.mainWindow != nil {
|
||||
return a.mainWindow.IsMaximised()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// WindowToggleAlwaysOnTop 切换窗口置顶
|
||||
func (a *App) WindowToggleAlwaysOnTop() bool {
|
||||
if a.ctx == nil {
|
||||
if a.mainWindow == nil {
|
||||
return false
|
||||
}
|
||||
a.isAlwaysOnTop = !a.isAlwaysOnTop
|
||||
runtime.WindowSetAlwaysOnTop(a.ctx, a.isAlwaysOnTop)
|
||||
a.mainWindow.SetAlwaysOnTop(a.isAlwaysOnTop)
|
||||
return a.isAlwaysOnTop
|
||||
}
|
||||
|
||||
// SetWindowTitleBarColor 设置原生标题栏颜色 + 主题模式(0x00BBGGRR 格式)
|
||||
func (a *App) SetWindowTitleBarColor(color uint32, isDark bool) {
|
||||
if a.mainWindow == nil || stdruntime.GOOS != "windows" {
|
||||
return
|
||||
}
|
||||
hwnd := uintptr(a.mainWindow.NativeWindow())
|
||||
if hwnd == 0 {
|
||||
return
|
||||
}
|
||||
w32.SetTheme(hwnd, isDark)
|
||||
w32.SetTitleBarColour(hwnd, color)
|
||||
}
|
||||
|
||||
// ========== 版本更新管理接口 ==========
|
||||
|
||||
// requireUpdateAPI 检查 updateAPI 是否已初始化,未初始化返回统一错误
|
||||
// requireUpdateAPI 检查 updateAPI 是否已初始化
|
||||
func (a *App) requireUpdateAPI() (*api.UpdateAPI, error) {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
if a.updateAPI == nil {
|
||||
return nil, fmt.Errorf("更新功能正在初始化中")
|
||||
}
|
||||
return a.updateAPI, nil
|
||||
}
|
||||
|
||||
// CheckUpdate 检查更新(UpdateAPI 可能尚未初始化完成)
|
||||
// CheckUpdate 检查更新
|
||||
func (a *App) CheckUpdate() (map[string]interface{}, error) {
|
||||
api, err := a.requireUpdateAPI()
|
||||
if err != nil {
|
||||
@@ -586,13 +599,11 @@ func (a *App) startAutoUpdateCheck() {
|
||||
interval = 5
|
||||
}
|
||||
|
||||
// 立即检查一次
|
||||
go a.checkUpdate()
|
||||
|
||||
// 启动定时器
|
||||
ticker := time.NewTicker(time.Duration(interval) * time.Minute)
|
||||
a.updateTicker = time.NewTicker(time.Duration(interval) * time.Minute)
|
||||
go func() {
|
||||
for range ticker.C {
|
||||
for range a.updateTicker.C {
|
||||
a.checkUpdate()
|
||||
}
|
||||
}()
|
||||
@@ -606,11 +617,15 @@ func (a *App) checkUpdate() {
|
||||
}
|
||||
}()
|
||||
|
||||
if a.updateAPI == nil {
|
||||
a.mu.Lock()
|
||||
api := a.updateAPI
|
||||
a.mu.Unlock()
|
||||
|
||||
if api == nil {
|
||||
return
|
||||
}
|
||||
|
||||
result, err := a.updateAPI.CheckUpdate()
|
||||
result, err := api.CheckUpdate()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -626,8 +641,8 @@ func (a *App) checkUpdate() {
|
||||
}
|
||||
|
||||
hasUpdate, ok := data["has_update"].(bool)
|
||||
if ok && hasUpdate && a.ctx != nil {
|
||||
runtime.EventsEmit(a.ctx, "update-available", data)
|
||||
if ok && hasUpdate && a.mainWindow != nil {
|
||||
a.mainWindow.EmitEvent("update-available", data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -645,7 +660,7 @@ func (a *App) GetFileServerURL() string {
|
||||
return "http://localhost:8073"
|
||||
}
|
||||
|
||||
// DetectFileTypeByContent 通过文件内容检测文件类型(用于小文件)
|
||||
// DetectFileTypeByContent 通过文件内容检测文件类型
|
||||
func (a *App) DetectFileTypeByContent(path string) (map[string]interface{}, error) {
|
||||
return filesystem.DetectFileTypeByContentSimple(path)
|
||||
}
|
||||
@@ -695,7 +710,6 @@ func (a *App) SaveAppConfig(req SaveAppConfigRequest) (map[string]interface{}, e
|
||||
return nil, fmt.Errorf("配置服务正在初始化中")
|
||||
}
|
||||
|
||||
// 保存前检查是否有新启用的模块,需要动态初始化
|
||||
oldConfig, _ := a.configAPI.GetAppConfig()
|
||||
var oldVisibleTabs []string
|
||||
if success, ok := oldConfig["success"].(bool); ok && success {
|
||||
@@ -717,7 +731,6 @@ func (a *App) SaveAppConfig(req SaveAppConfigRequest) (map[string]interface{}, e
|
||||
return result, err
|
||||
}
|
||||
|
||||
// 保存成功后,检查是否有新启用的模块需要初始化
|
||||
if success, ok := result["success"].(bool); ok && success {
|
||||
a.handleNewlyEnabledModules(oldVisibleTabs, req.VisibleTabs)
|
||||
}
|
||||
@@ -762,7 +775,6 @@ func (a *App) initFilesystemModule() {
|
||||
return
|
||||
}
|
||||
|
||||
// 启动文件服务器
|
||||
go a.startFileServer()
|
||||
|
||||
fmt.Println("[模块] 文件系统模块初始化完成")
|
||||
@@ -810,4 +822,3 @@ func (a *App) SelectPDFSaveDirectory() (string, error) {
|
||||
|
||||
return a.pdfAPI.SelectDirectory()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user