Private
Public Access
1
0

重构: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:
2026-05-01 11:03:53 +08:00
parent 44847e0d40
commit f54bf1c28d
185 changed files with 7768 additions and 914 deletions

163
app.go
View File

@@ -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()
}