Private
Public Access
1
0
Files
u-desk/app.go
绝尘 f54bf1c28d 重构: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>
2026-05-01 11:03:53 +08:00

825 lines
21 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// [fs-only] 数据库客户端模块已移除feature/fs-only 分支)
// 保留模块:文件系统 | Markdown编辑器 | 版本历史(抽屉) | 系统信息 | 更新检查 | PDF导出
// 顶部Tab仅file-system数据库 db-cli 已删除)
package main
import (
"context"
"fmt"
"os"
"path/filepath"
stdruntime "runtime"
"strings"
"sync"
"time"
"golang.org/x/sys/windows/registry"
"u-desk/internal/api"
"u-desk/internal/common"
"u-desk/internal/filesystem"
"u-desk/internal/service"
"u-desk/internal/storage"
"u-desk/internal/system"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/w32"
)
// App 应用结构体
type App struct {
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 方法命名约定:
// - 多参数操作 → XxxRequest 结构体Wails 自动生成 TS 类型)
// - 单参数查询/简单操作 → 直接参数
// NewApp 创建新的应用实例
func NewApp() *App {
return &App{}
}
// 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必须同步很快
if _, err := storage.InitFast(); err != nil {
return fmt.Errorf("SQLite 初始化失败,应用无法启动: %w", err)
}
// 2. 初始化配置服务
configService, err := api.NewConfigAPI()
if err != nil {
return fmt.Errorf("配置服务初始化失败: %w", err)
}
a.configAPI = configService
// 2.5. 迁移旧配置
_ = a.configAPI.MigrateTabConfig()
// 2.6. 初始化PDF导出API
fmt.Println("[启动] 初始化PDF导出模块...")
pdfAPI, err := api.NewPdfAPI()
if err != nil {
fmt.Printf("[启动] PDF导出API初始化失败: %v\n", err)
} else {
a.pdfAPI = pdfAPI
fmt.Println("[启动] PDF导出模块初始化完成")
}
// 3. 初始化版本号(提前触发缓存,避免后续重复计算)
version := service.GetCurrentVersion()
fmt.Printf("[启动] 当前版本: %s\n", version)
// 4. 读取配置,获取可见的 Tabs
visibleTabs := a.getVisibleTabs()
fmt.Printf("[启动] 可用的模块: %v\n", visibleTabs)
// 4. 根据配置初始化模块(条件初始化)
if err := a.initModulesByConfig(visibleTabs); err != nil {
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
func (a *App) getVisibleTabs() []string {
config, err := a.configAPI.GetAppConfig()
if err != nil {
fmt.Printf("[启动] 读取配置失败,使用默认配置: %v\n", err)
return common.DefaultVisibleTabs
}
success, ok := config["success"].(bool)
if !ok || !success {
fmt.Printf("[启动] 配置读取失败,使用默认配置\n")
return common.DefaultVisibleTabs
}
data, ok := config["data"].(map[string]interface{})
if !ok {
return common.DefaultVisibleTabs
}
visibleTabsInterface, ok := data["visibleTabs"].([]interface{})
if !ok {
return common.DefaultVisibleTabs
}
visibleTabs := common.InterfaceSliceToStringSlice(visibleTabsInterface)
if len(visibleTabs) == 0 {
return common.DefaultVisibleTabs
}
return visibleTabs
}
// 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)
if err != nil {
return fmt.Errorf("文件系统服务初始化失败: %w", err)
}
go a.startFileServer()
fmt.Println("[启动] 文件系统模块初始化完成")
} else {
fmt.Println("[启动] 跳过文件系统模块(未启用)")
}
return nil
}
// startFileServer 启动文件服务器
func (a *App) startFileServer() {
if _, err := filesystem.StartLocalFileServer(); err != nil {
fmt.Printf("[文件服务器] 启动失败: %v\n", err)
return
}
fmt.Println("[文件服务器] 启动在 http://localhost:8073")
}
// 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()
if a.filesystem != nil {
fmt.Println("[文件系统服务] 正在关闭...")
if err := a.filesystem.Close(shutdownCtx); err != nil {
fmt.Printf("[文件系统服务] 关闭失败: %v\n", err)
} else {
fmt.Println("[文件系统服务] 已关闭")
}
}
fmt.Println("[文件服务器] 正在关闭...")
if err := filesystem.ShutdownLocalFileServer(); err != nil {
fmt.Printf("[文件服务器] 关闭失败: %v\n", err)
} else {
fmt.Println("[文件服务器] 已关闭")
}
return nil
}
// GetSystemInfo 获取系统信息
func (a *App) GetSystemInfo() (map[string]interface{}, error) {
return system.GetSystemInfo()
}
// GetCPUInfo 获取 CPU 信息
func (a *App) GetCPUInfo() (map[string]interface{}, error) {
return system.GetCPUInfo()
}
// GetMemoryInfo 获取内存信息
func (a *App) GetMemoryInfo() (map[string]interface{}, error) {
return system.GetMemoryInfo()
}
// GetDiskInfo 获取磁盘信息
func (a *App) GetDiskInfo() ([]map[string]interface{}, error) {
return system.GetDiskInfo()
}
// ReadFile 读取文件
func (a *App) ReadFile(path string) (string, error) {
return a.filesystem.ReadFile(path)
}
// WriteFileRequest 写入文件请求结构体
type WriteFileRequest struct {
Path string `json:"path"`
Content string `json:"content"`
}
// WriteFile 写入文件
func (a *App) WriteFile(req WriteFileRequest) error {
return a.filesystem.WriteFile(req.Path, req.Content)
}
// SaveBase64FileRequest 保存 Base64 编码的二进制文件
type SaveBase64FileRequest struct {
Path string `json:"path"`
Content string `json:"content"` // base64 编码的文件内容
}
// SaveBase64File 将 base64 内容解码后写入文件(用于图片等二进制数据)
func (a *App) SaveBase64File(req SaveBase64FileRequest) error {
return a.filesystem.SaveBase64File(req.Path, req.Content)
}
// ListDir 列出目录
func (a *App) ListDir(path string) ([]map[string]interface{}, error) {
return a.filesystem.ListDir(path)
}
// CreateDir 创建目录
func (a *App) CreateDir(path string) (*filesystem.FileOperationResult, error) {
return a.filesystem.CreateDir(path)
}
// CreateFile 创建文件
func (a *App) CreateFile(path string) (*filesystem.FileOperationResult, error) {
return a.filesystem.CreateFile(path)
}
// DeletePath 删除文件或目录
func (a *App) DeletePath(path string) (*filesystem.FileOperationResult, error) {
return a.filesystem.DeletePath(path)
}
// RenamePathRequest 重命名文件或目录请求结构体
type RenamePathRequest struct {
OldPath string `json:"oldPath"`
NewPath string `json:"newPath"`
}
// RenamePath 重命名文件或目录
func (a *App) RenamePath(req RenamePathRequest) (*filesystem.FileOperationResult, error) {
return a.filesystem.RenamePath(req.OldPath, req.NewPath)
}
// GetFileInfo 获取文件信息
func (a *App) GetFileInfo(path string) (map[string]interface{}, error) {
return a.filesystem.GetFileInfo(path)
}
// GetEnvVars 获取环境变量
func (a *App) GetEnvVars() (map[string]string, error) {
envVars := make(map[string]string)
for _, env := range os.Environ() {
if key, value, found := strings.Cut(env, "="); found {
envVars[key] = value
}
}
return envVars, nil
}
// OpenPath 使用系统默认程序打开文件或目录
func (a *App) OpenPath(path string) error {
return a.filesystem.OpenPath(path)
}
// ========== Zip 文件操作接口 ==========
// ListZipContents 列出 zip 文件内容
func (a *App) ListZipContents(zipPath string) ([]map[string]interface{}, error) {
return a.filesystem.ListZipContents(zipPath)
}
// ExtractFileFromZip 从 zip 文件中提取单个文件内容
func (a *App) ExtractFileFromZip(zipPath, filePath string) (string, error) {
return a.filesystem.ExtractFileFromZip(zipPath, filePath)
}
// ExtractFileFromZipToTemp 从 zip 文件中提取单个文件到临时目录
func (a *App) ExtractFileFromZipToTemp(zipPath, filePath string) (string, error) {
return a.filesystem.ExtractFileFromZipToTemp(zipPath, filePath)
}
// GetZipFileInfo 获取 zip 文件中特定文件的信息
func (a *App) GetZipFileInfo(zipPath, filePath string) (map[string]interface{}, error) {
return a.filesystem.GetZipFileInfo(zipPath, filePath)
}
// ResolveShortcut 解析快捷方式文件,返回目标路径信息
func (a *App) ResolveShortcut(lnkPath string) (map[string]interface{}, error) {
targetPath, err := a.filesystem.ResolveShortcut(lnkPath)
if err != nil {
return map[string]interface{}{
"success": false,
"message": err.Error(),
}, err
}
fileInfo, err := a.filesystem.GetFileInfo(targetPath)
if err != nil {
return map[string]interface{}{
"success": true,
"targetPath": targetPath,
"targetExists": false,
"targetAccessible": false,
}, nil
}
return map[string]interface{}{
"success": true,
"targetPath": targetPath,
"targetExists": true,
"targetAccessible": true,
"targetInfo": fileInfo,
}, nil
}
// 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)
if err != nil {
return ""
}
defer key.Close()
val, _, err := key.GetStringValue(guid)
if err != nil || val == "" {
return ""
}
path := os.ExpandEnv(val)
if _, err := os.Stat(path); err != nil {
return ""
}
return path
}
// GetCommonPaths 获取常用系统路径
func (a *App) GetCommonPaths() (map[string]string, error) {
homeDir, err := os.UserHomeDir()
if err != nil {
return nil, err
}
paths := map[string]string{
"home": homeDir,
}
folderGUIDs := map[string]string{
"desktop": "{B4BFCC3A-DB2C-424C-B029-7FE99A87C641}",
"documents": "{D20B4C7F-5EA7-424C-B25E-039F6F1FCC8A}",
"downloads": "{374DE290-123F-4565-9164-39C4925E467B}",
}
for name, guid := range folderGUIDs {
if p := getWindowsSpecialFolder(guid, name); p != "" {
paths[name] = p
} else {
paths[name] = filepath.Join(homeDir, strings.ToUpper(name[:1])+name[1:])
}
}
if stdruntime.GOOS == "windows" {
for _, drive := range "ABCDEFGHIJKLMNOPQRSTUVWXYZ" {
path := string(drive) + ":\\"
if _, err := os.Stat(path); err == nil {
key := fmt.Sprintf("root_%c", drive)
paths[key] = path
}
}
}
return paths, nil
}
// Reload 重新加载窗口(用于菜单项)
func (a *App) Reload() {
if a.mainWindow != nil {
a.mainWindow.Reload()
}
}
// ClearCache 清理本地缓存(用于菜单项)
func (a *App) ClearCache() {
if a.mainWindow != nil {
a.mainWindow.EmitEvent("clear-cache")
}
}
// ========== 窗口控制方法 ==========
// WindowMinimize 最小化窗口
func (a *App) WindowMinimize() {
if a.mainWindow != nil {
a.mainWindow.Minimise()
}
}
// WindowMaximize 最大化/还原窗口
func (a *App) WindowMaximize() {
if a.mainWindow == nil {
return
}
if a.mainWindow.IsMaximised() {
a.mainWindow.UnMaximise()
} else {
a.mainWindow.Maximise()
}
}
// WindowClose 关闭窗口
func (a *App) WindowClose() {
application.Get().Quit()
}
// WindowIsMaximized 检查窗口是否最大化
func (a *App) WindowIsMaximized() bool {
if a.mainWindow != nil {
return a.mainWindow.IsMaximised()
}
return false
}
// WindowToggleAlwaysOnTop 切换窗口置顶
func (a *App) WindowToggleAlwaysOnTop() bool {
if a.mainWindow == nil {
return false
}
a.isAlwaysOnTop = !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 是否已初始化
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 检查更新
func (a *App) CheckUpdate() (map[string]interface{}, error) {
api, err := a.requireUpdateAPI()
if err != nil {
return nil, err
}
return api.CheckUpdate()
}
// GetCurrentVersion 获取当前版本号
func (a *App) GetCurrentVersion() (map[string]interface{}, error) {
api, err := a.requireUpdateAPI()
if err != nil {
return nil, err
}
return api.GetCurrentVersion()
}
// GetUpdateConfig 获取更新配置
func (a *App) GetUpdateConfig() (map[string]interface{}, error) {
api, err := a.requireUpdateAPI()
if err != nil {
return nil, err
}
return api.GetUpdateConfig()
}
// SetUpdateConfig 设置更新配置
func (a *App) SetUpdateConfig(autoCheckEnabled bool, checkIntervalMinutes int, checkURL string) (map[string]interface{}, error) {
api, err := a.requireUpdateAPI()
if err != nil {
return nil, err
}
return api.SetUpdateConfig(autoCheckEnabled, checkIntervalMinutes, checkURL)
}
// DownloadUpdate 下载更新包
func (a *App) DownloadUpdate(downloadURL string) (map[string]interface{}, error) {
api, err := a.requireUpdateAPI()
if err != nil {
return nil, err
}
return api.DownloadUpdate(downloadURL)
}
// InstallUpdate 安装更新包
func (a *App) InstallUpdate(installerPath string, autoRestart bool) (map[string]interface{}, error) {
api, err := a.requireUpdateAPI()
if err != nil {
return nil, err
}
return api.InstallUpdate(installerPath, autoRestart)
}
// InstallUpdateWithHash 安装更新包(带哈希验证)
func (a *App) InstallUpdateWithHash(installerPath string, autoRestart bool, expectedHash string, hashType string) (map[string]interface{}, error) {
api, err := a.requireUpdateAPI()
if err != nil {
return nil, err
}
return api.InstallUpdateWithHash(installerPath, autoRestart, expectedHash, hashType)
}
// VerifyUpdateFile 验证更新文件哈希值
func (a *App) VerifyUpdateFile(filePath string, expectedHash string, hashType string) (map[string]interface{}, error) {
api, err := a.requireUpdateAPI()
if err != nil {
return nil, err
}
return api.VerifyUpdateFile(filePath, expectedHash, hashType)
}
// startAutoUpdateCheck 启动自动更新检查
func (a *App) startAutoUpdateCheck() {
if a.updateAPI == nil {
return
}
config, err := a.updateAPI.GetUpdateConfig()
if err != nil {
return
}
success, ok := config["success"].(bool)
if !ok || !success {
return
}
configData, ok := config["data"].(map[string]interface{})
if !ok {
return
}
autoCheckEnabled, ok := configData["auto_check_enabled"].(bool)
if !ok || !autoCheckEnabled {
return
}
interval, ok := configData["check_interval_minutes"].(int)
if !ok || interval <= 0 {
interval = 5
}
go a.checkUpdate()
a.updateTicker = time.NewTicker(time.Duration(interval) * time.Minute)
go func() {
for range a.updateTicker.C {
a.checkUpdate()
}
}()
}
// checkUpdate 执行更新检查
func (a *App) checkUpdate() {
defer func() {
if r := recover(); r != nil {
fmt.Printf("[自动检查更新] 发生错误: %v\n", r)
}
}()
a.mu.Lock()
api := a.updateAPI
a.mu.Unlock()
if api == nil {
return
}
result, err := api.CheckUpdate()
if err != nil {
return
}
success, ok := result["success"].(bool)
if !ok || !success {
return
}
data, ok := result["data"].(map[string]interface{})
if !ok {
return
}
hasUpdate, ok := data["has_update"].(bool)
if ok && hasUpdate && a.mainWindow != nil {
a.mainWindow.EmitEvent("update-available", data)
}
}
// ========== 审计日志接口 ==========
// GetAuditLogs 获取审计日志
func (a *App) GetAuditLogs(limit int) ([]map[string]interface{}, error) {
return a.filesystem.GetAuditLogs(limit)
}
// ========== 文件服务器接口 ==========
// GetFileServerURL 获取本地文件服务器的URL
func (a *App) GetFileServerURL() string {
return "http://localhost:8073"
}
// DetectFileTypeByContent 通过文件内容检测文件类型
func (a *App) DetectFileTypeByContent(path string) (map[string]interface{}, error) {
return filesystem.DetectFileTypeByContentSimple(path)
}
// ========== 回收站接口 ==========
// GetRecycleBinEntries 获取回收站条目
func (a *App) GetRecycleBinEntries() ([]map[string]interface{}, error) {
return a.filesystem.GetRecycleBinEntries()
}
// RestoreFromRecycleBin 从回收站恢复文件
func (a *App) RestoreFromRecycleBin(recyclePath string) error {
return a.filesystem.RestoreFromRecycleBin(recyclePath)
}
// DeletePermanently 永久删除回收站中的文件
func (a *App) DeletePermanently(recyclePath string) error {
return a.filesystem.DeletePermanently(recyclePath)
}
// EmptyRecycleBin 清空回收站
func (a *App) EmptyRecycleBin() error {
return a.filesystem.EmptyRecycleBin()
}
// ========== 应用配置接口 ==========
// GetAppConfig 获取应用配置
func (a *App) GetAppConfig() (map[string]interface{}, error) {
if a.configAPI == nil {
return nil, fmt.Errorf("配置服务正在初始化中")
}
return a.configAPI.GetAppConfig()
}
// SaveAppConfigRequest 保存应用配置请求
type SaveAppConfigRequest struct {
Tabs []api.AppTabDefinition `json:"tabs"`
VisibleTabs []string `json:"visibleTabs"`
DefaultTab string `json:"defaultTab"`
}
// SaveAppConfig 保存应用配置
func (a *App) SaveAppConfig(req SaveAppConfigRequest) (map[string]interface{}, error) {
if a.configAPI == nil {
return nil, fmt.Errorf("配置服务正在初始化中")
}
oldConfig, _ := a.configAPI.GetAppConfig()
var oldVisibleTabs []string
if success, ok := oldConfig["success"].(bool); ok && success {
if data, ok := oldConfig["data"].(map[string]interface{}); ok {
if vtInterface, ok := data["visibleTabs"].([]interface{}); ok {
oldVisibleTabs = common.InterfaceSliceToStringSlice(vtInterface)
}
}
}
apiReq := api.SaveAppConfigRequest{
Tabs: req.Tabs,
VisibleTabs: req.VisibleTabs,
DefaultTab: req.DefaultTab,
}
result, err := a.configAPI.SaveAppConfig(apiReq)
if err != nil {
return result, err
}
if success, ok := result["success"].(bool); ok && success {
a.handleNewlyEnabledModules(oldVisibleTabs, req.VisibleTabs)
}
return result, nil
}
// handleNewlyEnabledModules 处理新启用的模块
func (a *App) handleNewlyEnabledModules(oldTabs, newTabs []string) {
newlyEnabled := common.Difference(newTabs, oldTabs)
if len(newlyEnabled) == 0 {
return
}
fmt.Printf("[模块] 检测到新启用的模块: %v\n", newlyEnabled)
for _, tab := range newlyEnabled {
switch tab {
case common.TabFileSystem:
a.initFilesystemModule()
case common.TabDevice:
fmt.Println("[模块] 设备测试模块已启用")
}
}
}
// initFilesystemModule 延迟初始化文件系统模块
func (a *App) initFilesystemModule() {
if a.filesystem != nil {
fmt.Println("[模块] 文件系统模块已初始化,跳过")
return
}
fmt.Println("[模块] 延迟初始化文件系统模块...")
fsConfig := filesystem.DefaultConfig()
var err error
a.filesystem, err = filesystem.NewFileSystemService(fsConfig)
if err != nil {
fmt.Printf("[模块] 文件系统模块初始化失败: %v\n", err)
return
}
go a.startFileServer()
fmt.Println("[模块] 文件系统模块初始化完成")
}
// ExportPDF 导出PDF文件
func (a *App) ExportPDF(content string, title string, fileName string, fontSize int, pageWidth int, pageHeight int) (map[string]interface{}, error) {
if a.pdfAPI == nil {
return map[string]interface{}{
"success": false,
"message": "PDF导出功能未初始化",
}, fmt.Errorf("PDF导出功能未初始化")
}
req := api.PdfExportRequest{
Content: content,
Title: title,
FileName: fileName,
FontSize: fontSize,
PageWidth: pageWidth,
PageHeight: pageHeight,
}
result, err := a.pdfAPI.ExportMarkdownToPDF(req)
if err != nil {
return map[string]interface{}{
"success": false,
"message": err.Error(),
}, err
}
return map[string]interface{}{
"success": result.Success,
"message": result.Message,
"path": result.Path,
"size": result.Size,
}, nil
}
// SelectPDFSaveDirectory 选择PDF保存目录
func (a *App) SelectPDFSaveDirectory() (string, error) {
if a.pdfAPI == nil {
return "", fmt.Errorf("PDF导出功能未初始化")
}
return a.pdfAPI.SelectDirectory()
}