Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f7d648ea52 | |||
| ce2698f245 | |||
| edd5b7c869 |
40
app.go
40
app.go
@@ -14,6 +14,7 @@ import (
|
|||||||
"u-desk/internal/common"
|
"u-desk/internal/common"
|
||||||
"u-desk/internal/database"
|
"u-desk/internal/database"
|
||||||
"u-desk/internal/filesystem"
|
"u-desk/internal/filesystem"
|
||||||
|
"u-desk/internal/service"
|
||||||
"u-desk/internal/storage"
|
"u-desk/internal/storage"
|
||||||
"u-desk/internal/system"
|
"u-desk/internal/system"
|
||||||
|
|
||||||
@@ -59,7 +60,11 @@ func (a *App) Startup(ctx context.Context) {
|
|||||||
// 2.5. 迁移旧配置
|
// 2.5. 迁移旧配置
|
||||||
_ = a.configAPI.MigrateTabConfig()
|
_ = a.configAPI.MigrateTabConfig()
|
||||||
|
|
||||||
// 3. 读取配置,获取可见的 Tabs
|
// 3. 初始化版本号(提前触发缓存,避免后续重复计算)
|
||||||
|
version := service.GetCurrentVersion()
|
||||||
|
fmt.Printf("[启动] 当前版本: %s\n", version)
|
||||||
|
|
||||||
|
// 4. 读取配置,获取可见的 Tabs
|
||||||
visibleTabs := a.getVisibleTabs()
|
visibleTabs := a.getVisibleTabs()
|
||||||
fmt.Printf("[启动] 可用的模块: %v\n", visibleTabs)
|
fmt.Printf("[启动] 可用的模块: %v\n", visibleTabs)
|
||||||
|
|
||||||
@@ -173,28 +178,31 @@ func (a *App) startFileServer() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建一个占位服务器用于保持引用(实际服务器由 StartLocalFileServer 管理)
|
|
||||||
a.fileServer = &http.Server{
|
|
||||||
Addr: "localhost:18765",
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("[文件服务器] 启动在 http://localhost:18765")
|
fmt.Println("[文件服务器] 启动在 http://localhost:18765")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shutdown 应用关闭时调用
|
// Shutdown 应用关闭时调用
|
||||||
func (a *App) Shutdown(ctx context.Context) {
|
func (a *App) Shutdown(ctx context.Context) {
|
||||||
// 关闭文件系统服务(优雅关闭,释放资源)
|
// 创建带超时的上下文(5秒超时)
|
||||||
|
shutdownCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// 1. 关闭文件系统服务(优雅关闭,释放资源)
|
||||||
if a.filesystem != nil {
|
if a.filesystem != nil {
|
||||||
fmt.Println("[文件系统服务] 正在关闭...")
|
fmt.Println("[文件系统服务] 正在关闭...")
|
||||||
if err := a.filesystem.Close(ctx); err != nil {
|
if err := a.filesystem.Close(shutdownCtx); err != nil {
|
||||||
fmt.Printf("[文件系统服务] 关闭失败: %v\n", err)
|
fmt.Printf("[文件系统服务] 关闭失败: %v\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Println("[文件系统服务] 已关闭")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 停止文件服务器
|
// 2. 停止文件服务器(使用全局服务器的关闭方法)
|
||||||
if a.fileServer != nil {
|
fmt.Println("[文件服务器] 正在关闭...")
|
||||||
fmt.Println("[文件服务器] 正在关闭...")
|
if err := filesystem.ShutdownLocalFileServer(); err != nil {
|
||||||
a.fileServer.Shutdown(ctx)
|
fmt.Printf("[文件服务器] 关闭失败: %v\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Println("[文件服务器] 已关闭")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,17 +278,17 @@ func (a *App) ListDir(path string) ([]map[string]interface{}, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateDir 创建目录
|
// CreateDir 创建目录
|
||||||
func (a *App) CreateDir(path string) error {
|
func (a *App) CreateDir(path string) (*filesystem.FileOperationResult, error) {
|
||||||
return a.filesystem.CreateDir(path)
|
return a.filesystem.CreateDir(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateFile 创建文件
|
// CreateFile 创建文件
|
||||||
func (a *App) CreateFile(path string) error {
|
func (a *App) CreateFile(path string) (*filesystem.FileOperationResult, error) {
|
||||||
return a.filesystem.CreateFile(path)
|
return a.filesystem.CreateFile(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeletePath 删除文件或目录
|
// DeletePath 删除文件或目录
|
||||||
func (a *App) DeletePath(path string) error {
|
func (a *App) DeletePath(path string) (*filesystem.FileOperationResult, error) {
|
||||||
return a.filesystem.DeletePath(path)
|
return a.filesystem.DeletePath(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,7 +299,7 @@ type RenamePathRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RenamePath 重命名文件或目录
|
// RenamePath 重命名文件或目录
|
||||||
func (a *App) RenamePath(req RenamePathRequest) error {
|
func (a *App) RenamePath(req RenamePathRequest) (*filesystem.FileOperationResult, error) {
|
||||||
return a.filesystem.RenamePath(req.OldPath, req.NewPath)
|
return a.filesystem.RenamePath(req.OldPath, req.NewPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,13 +50,6 @@ func (api *UpdateAPI) CheckUpdate() (map[string]interface{}, error) {
|
|||||||
// GetCurrentVersion 获取当前版本号
|
// GetCurrentVersion 获取当前版本号
|
||||||
func (api *UpdateAPI) GetCurrentVersion() (map[string]interface{}, error) {
|
func (api *UpdateAPI) GetCurrentVersion() (map[string]interface{}, error) {
|
||||||
version := service.GetCurrentVersion()
|
version := service.GetCurrentVersion()
|
||||||
|
|
||||||
// 同步配置中的版本号
|
|
||||||
if config, err := service.LoadUpdateConfig(); err == nil && config.CurrentVersion != version {
|
|
||||||
config.CurrentVersion = version
|
|
||||||
service.SaveUpdateConfig(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
return successResponse(map[string]interface{}{
|
return successResponse(map[string]interface{}{
|
||||||
"version": version,
|
"version": version,
|
||||||
}), nil
|
}), nil
|
||||||
@@ -69,13 +62,6 @@ func (api *UpdateAPI) GetUpdateConfig() (map[string]interface{}, error) {
|
|||||||
return errorResponse(err.Error()), nil
|
return errorResponse(err.Error()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 同步最新版本号
|
|
||||||
latestVersion := service.GetCurrentVersion()
|
|
||||||
if config.CurrentVersion != latestVersion {
|
|
||||||
config.CurrentVersion = latestVersion
|
|
||||||
service.SaveUpdateConfig(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
return successResponse(map[string]interface{}{
|
return successResponse(map[string]interface{}{
|
||||||
"current_version": config.CurrentVersion,
|
"current_version": config.CurrentVersion,
|
||||||
"last_check_time": config.LastCheckTime.Format("2006-01-02 15:04:05"),
|
"last_check_time": config.LastCheckTime.Format("2006-01-02 15:04:05"),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package filesystem
|
package filesystem
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
@@ -10,12 +11,14 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// LocalFileServer 本地文件服务器(独立的 HTTP 服务器)
|
// LocalFileServer 本地文件服务器(独立的 HTTP 服务器)
|
||||||
type LocalFileServer struct {
|
type LocalFileServer struct {
|
||||||
server *http.Server
|
server *http.Server
|
||||||
addr string
|
addr string
|
||||||
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -258,3 +261,35 @@ func HandleLocalFile(w http.ResponseWriter, r *http.Request) {
|
|||||||
func isAllowedFileType(ext string) bool {
|
func isAllowedFileType(ext string) bool {
|
||||||
return defaultFileTypeManager.IsAllowed(ext)
|
return defaultFileTypeManager.IsAllowed(ext)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shutdown 优雅关闭文件服务器
|
||||||
|
func (lfs *LocalFileServer) Shutdown() error {
|
||||||
|
if lfs == nil || lfs.server == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lfs.mu.Lock()
|
||||||
|
defer lfs.mu.Unlock()
|
||||||
|
|
||||||
|
// 创建带超时的上下文
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
log.Printf("[LocalFileServer] 正在关闭...")
|
||||||
|
|
||||||
|
if err := lfs.server.Shutdown(ctx); err != nil {
|
||||||
|
log.Printf("[LocalFileServer] 关闭失败: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[LocalFileServer] 已关闭")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShutdownLocalFileServer 关闭全局文件服务器
|
||||||
|
func ShutdownLocalFileServer() error {
|
||||||
|
if localFileServer != nil {
|
||||||
|
return localFileServer.Shutdown()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,90 +8,9 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ========== 向后兼容的全局函数包装器 ==========
|
// ========== 辅助函数 ==========
|
||||||
// 这些函数提供向后兼容性,内部委托给 FileSystemService
|
|
||||||
// 新代码应该使用 FileSystemService 而不是这些全局函数
|
|
||||||
|
|
||||||
// ReadFile 读取文件内容(向后兼容包装器)
|
|
||||||
func ReadFile(path string) (string, error) {
|
|
||||||
service, err := GetGlobalService()
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("服务未初始化: %v", err)
|
|
||||||
}
|
|
||||||
return service.ReadFile(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteFile 写入文件(向后兼容包装器)
|
|
||||||
func WriteFile(path, content string) error {
|
|
||||||
service, err := GetGlobalService()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("服务未初始化: %v", err)
|
|
||||||
}
|
|
||||||
return service.WriteFile(path, content)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListDir 列出目录内容(向后兼容包装器)
|
|
||||||
func ListDir(path string) ([]map[string]interface{}, error) {
|
|
||||||
service, err := GetGlobalService()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("服务未初始化: %v", err)
|
|
||||||
}
|
|
||||||
return service.ListDir(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateDir 创建目录(向后兼容包装器)
|
|
||||||
func CreateDir(path string) error {
|
|
||||||
service, err := GetGlobalService()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("服务未初始化: %v", err)
|
|
||||||
}
|
|
||||||
return service.CreateDir(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateFile 创建空文件(向后兼容包装器)
|
|
||||||
func CreateFile(path string) error {
|
|
||||||
service, err := GetGlobalService()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("服务未初始化: %v", err)
|
|
||||||
}
|
|
||||||
return service.CreateFile(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeletePath 删除文件或目录(向后兼容包装器)
|
|
||||||
func DeletePath(path string) error {
|
|
||||||
service, err := GetGlobalService()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("服务未初始化: %v", err)
|
|
||||||
}
|
|
||||||
return service.DeletePath(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeletePathWithConfig 使用指定配置删除文件或目录(向后兼容包装器)
|
|
||||||
func DeletePathWithConfig(path string, config *Config) error {
|
|
||||||
service, err := GetGlobalService()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("服务未初始化: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 临时替换服务的配置
|
|
||||||
originalConfig := service.config
|
|
||||||
service.config = config
|
|
||||||
defer func() { service.config = originalConfig }()
|
|
||||||
|
|
||||||
return service.DeletePath(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFileInfo 获取文件信息(向后兼容包装器)
|
|
||||||
func GetFileInfo(path string) (map[string]interface{}, error) {
|
|
||||||
service, err := GetGlobalService()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("服务未初始化: %v", err)
|
|
||||||
}
|
|
||||||
return service.GetFileInfo(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenPath 打开文件或目录(使用系统默认程序)
|
// OpenPath 打开文件或目录(使用系统默认程序)
|
||||||
// 这是一个核心工具函数,保留为独立函数
|
|
||||||
func OpenPath(path string) error {
|
func OpenPath(path string) error {
|
||||||
// 使用 path.validator 进行验证
|
// 使用 path.validator 进行验证
|
||||||
validator := NewPathValidator(DefaultConfig())
|
validator := NewPathValidator(DefaultConfig())
|
||||||
@@ -132,16 +51,7 @@ func OpenPath(path string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenamePath 重命名文件或目录(向后兼容包装器)
|
// ========== 工具函数 ==========
|
||||||
func RenamePath(oldPath, newPath string) error {
|
|
||||||
service, err := GetGlobalService()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("服务未初始化: %v", err)
|
|
||||||
}
|
|
||||||
return service.RenamePath(oldPath, newPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== 辅助函数 ==========
|
|
||||||
|
|
||||||
// formatBytes 格式化字节大小为人类可读格式
|
// formatBytes 格式化字节大小为人类可读格式
|
||||||
func formatBytes(bytes int64) string {
|
func formatBytes(bytes int64) string {
|
||||||
|
|||||||
@@ -13,6 +13,19 @@ import (
|
|||||||
"u-desk/internal/common"
|
"u-desk/internal/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// FileOperationResult 文件操作结果
|
||||||
|
type FileOperationResult struct {
|
||||||
|
Path string `json:"path"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
SizeStr string `json:"size_str,omitempty"`
|
||||||
|
IsDir bool `json:"is_dir"`
|
||||||
|
ModTime string `json:"mod_time,omitempty"`
|
||||||
|
Mode string `json:"mode,omitempty"`
|
||||||
|
OldPath string `json:"old_path,omitempty"` // 仅重命名操作时有值
|
||||||
|
Deleted bool `json:"deleted,omitempty"` // 仅删除操作时有值
|
||||||
|
}
|
||||||
|
|
||||||
// FileSystemService 文件系统服务
|
// FileSystemService 文件系统服务
|
||||||
// 统一管理所有文件系统相关的功能,使用依赖注入而非全局变量
|
// 统一管理所有文件系统相关的功能,使用依赖注入而非全局变量
|
||||||
type FileSystemService struct {
|
type FileSystemService struct {
|
||||||
@@ -173,52 +186,52 @@ func (s *FileSystemService) Open(path string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Delete 删除文件或目录(实现 FileService 接口)
|
// Delete 删除文件或目录(实现 FileService 接口)
|
||||||
func (s *FileSystemService) Delete(path string) error {
|
func (s *FileSystemService) Delete(path string) (*FileOperationResult, error) {
|
||||||
return s.DeletePathWithContext(context.Background(), path)
|
return s.DeletePathWithContext(context.Background(), path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeletePath 删除文件或目录
|
// DeletePath 删除文件或目录
|
||||||
func (s *FileSystemService) DeletePath(path string) error {
|
func (s *FileSystemService) DeletePath(path string) (*FileOperationResult, error) {
|
||||||
return s.DeletePathWithContext(context.Background(), path)
|
return s.DeletePathWithContext(context.Background(), path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeletePathWithContext 带上下文的删除操作
|
// DeletePathWithContext 带上下文的删除操作,返回被删除文件的信息
|
||||||
func (s *FileSystemService) DeletePathWithContext(ctx context.Context, path string) error {
|
func (s *FileSystemService) DeletePathWithContext(ctx context.Context, path string) (*FileOperationResult, error) {
|
||||||
// 路径验证
|
// 路径验证
|
||||||
if err := s.validatePath(path); err != nil {
|
if err := s.validatePath(path); err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取文件信息
|
// 获取文件信息(在删除前保存)
|
||||||
info, err := os.Stat(path)
|
info, err := os.Stat(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return fmt.Errorf("文件或目录不存在")
|
return nil, fmt.Errorf("文件或目录不存在")
|
||||||
}
|
}
|
||||||
return fmt.Errorf("获取文件信息失败: %v", err)
|
return nil, fmt.Errorf("获取文件信息失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查删除限制
|
// 检查删除限制
|
||||||
exceeds, details, checkErr := CheckDeleteRestrictions(path, info, s.config)
|
exceeds, details, checkErr := CheckDeleteRestrictions(path, info, s.config)
|
||||||
if checkErr != nil {
|
if checkErr != nil {
|
||||||
return checkErr
|
return nil, checkErr
|
||||||
}
|
}
|
||||||
|
|
||||||
if exceeds {
|
if exceeds {
|
||||||
if s.config.Security.DeleteRestrictions.RequireConfirm {
|
if s.config.Security.DeleteRestrictions.RequireConfirm {
|
||||||
return &DeleteRestrictionWarning{
|
return nil, &DeleteRestrictionWarning{
|
||||||
Path: path,
|
Path: path,
|
||||||
Details: details,
|
Details: details,
|
||||||
Info: info,
|
Info: info,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return fmt.Errorf("删除限制: %s", details)
|
return nil, fmt.Errorf("删除限制: %s", details)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 文件锁检查(可选)
|
// 文件锁检查(可选)
|
||||||
if s.lockChecker != nil {
|
if s.lockChecker != nil {
|
||||||
if err := s.lockChecker.SafeDeleteWithLockCheck(path); err != nil {
|
if err := s.lockChecker.SafeDeleteWithLockCheck(path); err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,7 +246,7 @@ func (s *FileSystemService) DeletePathWithContext(ctx context.Context, path stri
|
|||||||
s.logDelete(path, info.IsDir(), info.Size(), deleteErr)
|
s.logDelete(path, info.IsDir(), info.Size(), deleteErr)
|
||||||
|
|
||||||
if deleteErr != nil {
|
if deleteErr != nil {
|
||||||
return fmt.Errorf("删除失败: %v", deleteErr)
|
return nil, fmt.Errorf("删除失败: %v", deleteErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果启用回收站,移动到回收站而非永久删除
|
// 如果启用回收站,移动到回收站而非永久删除
|
||||||
@@ -247,7 +260,17 @@ func (s *FileSystemService) DeletePathWithContext(ctx context.Context, path stri
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
// 返回被删除的文件信息,用于前端更新
|
||||||
|
return &FileOperationResult{
|
||||||
|
Path: filepath.ToSlash(path), // 统一使用正斜杠
|
||||||
|
Name: info.Name(),
|
||||||
|
Size: info.Size(),
|
||||||
|
SizeStr: formatBytes(info.Size()),
|
||||||
|
IsDir: info.IsDir(),
|
||||||
|
ModTime: info.ModTime().Format("2006-01-02 15:04:05"),
|
||||||
|
Mode: info.Mode().String(),
|
||||||
|
Deleted: true,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListDir 列出目录内容
|
// ListDir 列出目录内容
|
||||||
@@ -274,7 +297,7 @@ func (s *FileSystemService) ListDir(path string) ([]map[string]interface{}, erro
|
|||||||
fullPath := filepath.Join(path, entry.Name())
|
fullPath := filepath.Join(path, entry.Name())
|
||||||
result = append(result, map[string]interface{}{
|
result = append(result, map[string]interface{}{
|
||||||
"name": entry.Name(),
|
"name": entry.Name(),
|
||||||
"path": fullPath,
|
"path": filepath.ToSlash(fullPath), // 统一使用正斜杠
|
||||||
"is_dir": entry.IsDir(),
|
"is_dir": entry.IsDir(),
|
||||||
"size": info.Size(),
|
"size": info.Size(),
|
||||||
"mod_time": info.ModTime().Format("2006-01-02 15:04:05"),
|
"mod_time": info.ModTime().Format("2006-01-02 15:04:05"),
|
||||||
@@ -292,14 +315,14 @@ func (s *FileSystemService) ListDir(path string) ([]map[string]interface{}, erro
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateDir 创建目录
|
// CreateDir 创建目录,返回创建的目录信息
|
||||||
func (s *FileSystemService) CreateDir(path string) error {
|
func (s *FileSystemService) CreateDir(path string) (*FileOperationResult, error) {
|
||||||
if err := s.validatePath(path); err != nil {
|
if err := s.validatePath(path); err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.MkdirAll(path, DefaultDirPermissions); err != nil {
|
if err := os.MkdirAll(path, DefaultDirPermissions); err != nil {
|
||||||
return fmt.Errorf("创建目录失败: %v", err)
|
return nil, fmt.Errorf("创建目录失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.logAudit(AuditLogEntry{
|
s.logAudit(AuditLogEntry{
|
||||||
@@ -310,23 +333,42 @@ func (s *FileSystemService) CreateDir(path string) error {
|
|||||||
Success: true,
|
Success: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
return nil
|
// 获取创建的目录信息
|
||||||
|
info, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
// 创建成功但获取信息失败,返回基本信息
|
||||||
|
return &FileOperationResult{
|
||||||
|
Path: filepath.ToSlash(path), // 统一使用正斜杠
|
||||||
|
Name: filepath.Base(path),
|
||||||
|
IsDir: true,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &FileOperationResult{
|
||||||
|
Path: filepath.ToSlash(path), // 统一使用正斜杠
|
||||||
|
Name: info.Name(),
|
||||||
|
Size: info.Size(),
|
||||||
|
SizeStr: formatBytes(info.Size()),
|
||||||
|
IsDir: true,
|
||||||
|
ModTime: info.ModTime().Format("2006-01-02 15:04:05"),
|
||||||
|
Mode: info.Mode().String(),
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateFile 创建空文件
|
// CreateFile 创建空文件,返回创建的文件信息
|
||||||
func (s *FileSystemService) CreateFile(path string) error {
|
func (s *FileSystemService) CreateFile(path string) (*FileOperationResult, error) {
|
||||||
if err := s.validatePath(path); err != nil {
|
if err := s.validatePath(path); err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查文件是否已存在
|
// 检查文件是否已存在
|
||||||
if _, err := os.Stat(path); err == nil {
|
if _, err := os.Stat(path); err == nil {
|
||||||
return fmt.Errorf("文件已存在")
|
return nil, fmt.Errorf("文件已存在")
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := os.Create(path)
|
file, err := os.Create(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("创建文件失败: %v", err)
|
return nil, fmt.Errorf("创建文件失败: %v", err)
|
||||||
}
|
}
|
||||||
file.Close()
|
file.Close()
|
||||||
|
|
||||||
@@ -338,7 +380,27 @@ func (s *FileSystemService) CreateFile(path string) error {
|
|||||||
Success: true,
|
Success: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
return nil
|
// 获取创建的文件信息
|
||||||
|
info, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
// 创建成功但获取信息失败,返回基本信息
|
||||||
|
return &FileOperationResult{
|
||||||
|
Path: filepath.ToSlash(path), // 统一使用正斜杠
|
||||||
|
Name: filepath.Base(path),
|
||||||
|
IsDir: false,
|
||||||
|
Size: 0,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &FileOperationResult{
|
||||||
|
Path: filepath.ToSlash(path), // 统一使用正斜杠
|
||||||
|
Name: info.Name(),
|
||||||
|
Size: info.Size(),
|
||||||
|
SizeStr: formatBytes(info.Size()),
|
||||||
|
IsDir: false,
|
||||||
|
ModTime: info.ModTime().Format("2006-01-02 15:04:05"),
|
||||||
|
Mode: info.Mode().String(),
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetInfo 获取文件信息(实现 FileService 接口)
|
// GetInfo 获取文件信息(实现 FileService 接口)
|
||||||
@@ -362,7 +424,7 @@ func (s *FileSystemService) GetFileInfo(path string) (map[string]interface{}, er
|
|||||||
|
|
||||||
return map[string]interface{}{
|
return map[string]interface{}{
|
||||||
"name": info.Name(),
|
"name": info.Name(),
|
||||||
"path": path,
|
"path": filepath.ToSlash(path), // 统一使用正斜杠
|
||||||
"size": info.Size(),
|
"size": info.Size(),
|
||||||
"size_str": formatBytes(info.Size()),
|
"size_str": formatBytes(info.Size()),
|
||||||
"is_dir": info.IsDir(),
|
"is_dir": info.IsDir(),
|
||||||
@@ -380,21 +442,21 @@ func (s *FileSystemService) OpenPath(path string) error {
|
|||||||
return OpenPath(path)
|
return OpenPath(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenamePath 重命名文件或目录
|
// RenamePath 重命名文件或目录,返回新文件信息
|
||||||
func (s *FileSystemService) RenamePath(oldPath, newPath string) error {
|
func (s *FileSystemService) RenamePath(oldPath, newPath string) (*FileOperationResult, error) {
|
||||||
// 验证旧路径
|
// 验证旧路径
|
||||||
if err := s.validatePath(oldPath); err != nil {
|
if err := s.validatePath(oldPath); err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证新路径
|
// 验证新路径
|
||||||
if err := s.validatePath(newPath); err != nil {
|
if err := s.validatePath(newPath); err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行重命名
|
// 执行重命名
|
||||||
if err := os.Rename(oldPath, newPath); err != nil {
|
if err := os.Rename(oldPath, newPath); err != nil {
|
||||||
return fmt.Errorf("重命名失败: %v", err)
|
return nil, fmt.Errorf("重命名失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.logAudit(AuditLogEntry{
|
s.logAudit(AuditLogEntry{
|
||||||
@@ -405,7 +467,27 @@ func (s *FileSystemService) RenamePath(oldPath, newPath string) error {
|
|||||||
Success: true,
|
Success: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
return nil
|
// 获取新文件信息
|
||||||
|
info, err := os.Stat(newPath)
|
||||||
|
if err != nil {
|
||||||
|
// 重命名成功但获取信息失败,返回基本信息
|
||||||
|
return &FileOperationResult{
|
||||||
|
Path: filepath.ToSlash(newPath), // 统一使用正斜杠
|
||||||
|
Name: filepath.Base(newPath),
|
||||||
|
OldPath: filepath.ToSlash(oldPath),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &FileOperationResult{
|
||||||
|
Path: filepath.ToSlash(newPath), // 统一使用正斜杠
|
||||||
|
Name: info.Name(),
|
||||||
|
Size: info.Size(),
|
||||||
|
SizeStr: formatBytes(info.Size()),
|
||||||
|
IsDir: info.IsDir(),
|
||||||
|
ModTime: info.ModTime().Format("2006-01-02 15:04:05"),
|
||||||
|
Mode: info.Mode().String(),
|
||||||
|
OldPath: filepath.ToSlash(oldPath),
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== ZIP操作接口 ==========
|
// ========== ZIP操作接口 ==========
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
package filesystem
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
)
|
|
||||||
|
|
||||||
// FileService 文件操作核心接口
|
|
||||||
// 定义所有文件操作的基本功能,便于mock测试
|
|
||||||
type FileService interface {
|
|
||||||
// 基本操作
|
|
||||||
Read(path string) (string, error)
|
|
||||||
Write(path, content string) error
|
|
||||||
Delete(path string) error
|
|
||||||
List(path string) ([]map[string]interface{}, error)
|
|
||||||
CreateDir(path string) error
|
|
||||||
CreateFile(path string) error
|
|
||||||
GetInfo(path string) (map[string]interface{}, error)
|
|
||||||
Open(path string) error
|
|
||||||
|
|
||||||
// 快捷方式
|
|
||||||
ResolveShortcut(lnkPath string) (targetPath string, err error)
|
|
||||||
|
|
||||||
// 配置
|
|
||||||
GetConfig() *Config
|
|
||||||
Close(ctx context.Context) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确保实现接口
|
|
||||||
var _ FileService = (*FileSystemService)(nil)
|
|
||||||
|
|
||||||
@@ -181,7 +181,7 @@ func ListZipContents(zipPath string) ([]map[string]interface{}, error) {
|
|||||||
|
|
||||||
entry := map[string]interface{}{
|
entry := map[string]interface{}{
|
||||||
"name": name,
|
"name": name,
|
||||||
"path": file.Name, // zip 中的完整路径
|
"path": file.Name, // zip 中的完整路径(已使用 /)
|
||||||
"is_dir": isDir,
|
"is_dir": isDir,
|
||||||
"size": file.UncompressedSize64,
|
"size": file.UncompressedSize64,
|
||||||
"compressed": file.CompressedSize64,
|
"compressed": file.CompressedSize64,
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ func getCompressionMethodString(method uint16) string {
|
|||||||
func createFileInfoMap(file *zip.File, includeExtra ...bool) map[string]interface{} {
|
func createFileInfoMap(file *zip.File, includeExtra ...bool) map[string]interface{} {
|
||||||
info := map[string]interface{}{
|
info := map[string]interface{}{
|
||||||
"name": filepath.Base(file.Name),
|
"name": filepath.Base(file.Name),
|
||||||
"path": file.Name,
|
"path": file.Name, // zip 中的路径(已使用 /)
|
||||||
"is_dir": file.Mode().IsDir(),
|
"is_dir": file.Mode().IsDir(),
|
||||||
"size": file.UncompressedSize64,
|
"size": file.UncompressedSize64,
|
||||||
"compressed": file.CompressedSize64,
|
"compressed": file.CompressedSize64,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -62,20 +61,13 @@ func NewUpdateService(checkURL string) *UpdateService {
|
|||||||
|
|
||||||
// CheckUpdate 检查更新
|
// CheckUpdate 检查更新
|
||||||
func (s *UpdateService) CheckUpdate() (*UpdateCheckResult, error) {
|
func (s *UpdateService) CheckUpdate() (*UpdateCheckResult, error) {
|
||||||
log.Printf("[更新检查] 开始检查更新,检查地址: %s", s.checkURL)
|
|
||||||
|
|
||||||
config, err := LoadUpdateConfig()
|
config, err := LoadUpdateConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("加载配置失败: %v", err)
|
return nil, fmt.Errorf("加载配置失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 同步版本号
|
// 获取当前版本(使用缓存)
|
||||||
currentVersionStr, err := s.syncConfigVersion(config)
|
currentVersion, err := ParseVersion(GetCurrentVersion())
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
currentVersion, err := ParseVersion(currentVersionStr)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("解析当前版本失败: %v", err)
|
return nil, fmt.Errorf("解析当前版本失败: %v", err)
|
||||||
}
|
}
|
||||||
@@ -86,14 +78,6 @@ func (s *UpdateService) CheckUpdate() (*UpdateCheckResult, error) {
|
|||||||
return nil, fmt.Errorf("获取远程版本信息失败: %v", err)
|
return nil, fmt.Errorf("获取远程版本信息失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[更新检查] 远程版本信息: 版本=%s, 下载地址=%s, 强制更新=%v, 更新日志长度=%d",
|
|
||||||
remoteInfo.Version, remoteInfo.DownloadURL, remoteInfo.ForceUpdate, len(remoteInfo.Changelog))
|
|
||||||
if remoteInfo.Changelog != "" {
|
|
||||||
log.Printf("[更新检查] 更新日志内容: %s", remoteInfo.Changelog)
|
|
||||||
} else {
|
|
||||||
log.Printf("[更新检查] 警告: 远程接口未返回更新日志")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析远程版本号
|
// 解析远程版本号
|
||||||
remoteVersion, err := ParseVersion(remoteInfo.Version)
|
remoteVersion, err := ParseVersion(remoteInfo.Version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -102,55 +86,30 @@ func (s *UpdateService) CheckUpdate() (*UpdateCheckResult, error) {
|
|||||||
|
|
||||||
// 比较版本
|
// 比较版本
|
||||||
hasUpdate := remoteVersion.IsNewerThan(currentVersion)
|
hasUpdate := remoteVersion.IsNewerThan(currentVersion)
|
||||||
log.Printf("[更新检查] 版本比较: 当前=%s, 远程=%s, 有更新=%v",
|
|
||||||
currentVersion.String(), remoteVersion.String(), hasUpdate)
|
|
||||||
|
|
||||||
// 更新最后检查时间
|
// 更新最后检查时间
|
||||||
config.UpdateLastCheckTime()
|
config.UpdateLastCheckTime()
|
||||||
|
|
||||||
result := &UpdateCheckResult{
|
return &UpdateCheckResult{
|
||||||
HasUpdate: hasUpdate,
|
HasUpdate: hasUpdate,
|
||||||
CurrentVersion: currentVersionStr,
|
CurrentVersion: GetCurrentVersion(),
|
||||||
LatestVersion: remoteInfo.Version,
|
LatestVersion: remoteInfo.Version,
|
||||||
DownloadURL: remoteInfo.DownloadURL,
|
DownloadURL: remoteInfo.DownloadURL,
|
||||||
Changelog: remoteInfo.Changelog,
|
Changelog: remoteInfo.Changelog,
|
||||||
ForceUpdate: remoteInfo.ForceUpdate,
|
ForceUpdate: remoteInfo.ForceUpdate,
|
||||||
ReleaseDate: remoteInfo.ReleaseDate,
|
ReleaseDate: remoteInfo.ReleaseDate,
|
||||||
FileSize: remoteInfo.FileSize,
|
FileSize: remoteInfo.FileSize,
|
||||||
}
|
}, nil
|
||||||
|
|
||||||
log.Printf("[更新检查] 检查完成: 有更新=%v", hasUpdate)
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// syncConfigVersion 同步配置中的版本号
|
|
||||||
func (s *UpdateService) syncConfigVersion(config *UpdateConfig) (string, error) {
|
|
||||||
currentVersionStr := GetCurrentVersion()
|
|
||||||
if currentVersionStr == "" {
|
|
||||||
currentVersionStr = config.CurrentVersion
|
|
||||||
log.Printf("[更新检查] 使用配置中的版本号: %s", currentVersionStr)
|
|
||||||
} else if config.CurrentVersion != currentVersionStr {
|
|
||||||
log.Printf("[更新检查] 配置中的版本号 (%s) 与当前版本号 (%s) 不一致,更新配置",
|
|
||||||
config.CurrentVersion, currentVersionStr)
|
|
||||||
config.CurrentVersion = currentVersionStr
|
|
||||||
if err := SaveUpdateConfig(config); err != nil {
|
|
||||||
log.Printf("[更新检查] 更新配置失败: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return currentVersionStr, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetchRemoteVersionInfo 获取远程版本信息
|
// fetchRemoteVersionInfo 获取远程版本信息
|
||||||
func (s *UpdateService) fetchRemoteVersionInfo() (*RemoteVersionInfo, error) {
|
func (s *UpdateService) fetchRemoteVersionInfo() (*RemoteVersionInfo, error) {
|
||||||
if s.checkURL == "" {
|
if s.checkURL == "" {
|
||||||
log.Printf("[远程版本] 版本检查 URL 未配置")
|
|
||||||
return nil, fmt.Errorf("版本检查 URL 未配置,请先设置检查地址")
|
return nil, fmt.Errorf("版本检查 URL 未配置,请先设置检查地址")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[远程版本] 请求远程版本信息: %s", s.checkURL)
|
|
||||||
|
|
||||||
// 添加时间戳参数防止缓存
|
// 添加时间戳参数防止缓存
|
||||||
timestamp := time.Now().UnixMilli() // 使用毫秒级时间戳
|
timestamp := time.Now().UnixMilli()
|
||||||
var requestURL string
|
var requestURL string
|
||||||
if strings.Contains(s.checkURL, "?") {
|
if strings.Contains(s.checkURL, "?") {
|
||||||
requestURL = fmt.Sprintf("%s&t=%d", s.checkURL, timestamp)
|
requestURL = fmt.Sprintf("%s&t=%d", s.checkURL, timestamp)
|
||||||
@@ -158,8 +117,6 @@ func (s *UpdateService) fetchRemoteVersionInfo() (*RemoteVersionInfo, error) {
|
|||||||
requestURL = fmt.Sprintf("%s?t=%d", s.checkURL, timestamp)
|
requestURL = fmt.Sprintf("%s?t=%d", s.checkURL, timestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[远程版本] 实际请求URL: %s", requestURL)
|
|
||||||
|
|
||||||
// 创建 HTTP 客户端,设置超时
|
// 创建 HTTP 客户端,设置超时
|
||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
Timeout: 10 * time.Second,
|
Timeout: 10 * time.Second,
|
||||||
@@ -168,12 +125,10 @@ func (s *UpdateService) fetchRemoteVersionInfo() (*RemoteVersionInfo, error) {
|
|||||||
// 发送请求
|
// 发送请求
|
||||||
resp, err := client.Get(requestURL)
|
resp, err := client.Get(requestURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[远程版本] 网络请求失败: %v", err)
|
|
||||||
return nil, fmt.Errorf("网络请求失败: %v", err)
|
return nil, fmt.Errorf("网络请求失败: %v", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
log.Printf("[远程版本] HTTP 响应状态码: %d", resp.StatusCode)
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return nil, fmt.Errorf("服务器返回错误状态码: %d", resp.StatusCode)
|
return nil, fmt.Errorf("服务器返回错误状态码: %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
@@ -181,25 +136,19 @@ func (s *UpdateService) fetchRemoteVersionInfo() (*RemoteVersionInfo, error) {
|
|||||||
// 读取响应
|
// 读取响应
|
||||||
body, err := io.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[远程版本] 读取响应失败: %v", err)
|
|
||||||
return nil, fmt.Errorf("读取响应失败: %v", err)
|
return nil, fmt.Errorf("读取响应失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[远程版本] 响应内容长度: %d 字节", len(body))
|
|
||||||
|
|
||||||
// 解析 JSON
|
// 解析 JSON
|
||||||
var remoteInfo RemoteVersionInfo
|
var remoteInfo RemoteVersionInfo
|
||||||
if err := json.Unmarshal(body, &remoteInfo); err != nil {
|
if err := json.Unmarshal(body, &remoteInfo); err != nil {
|
||||||
log.Printf("[远程版本] 解析 JSON 失败: %v, 响应内容: %s", err, string(body))
|
|
||||||
return nil, fmt.Errorf("解析响应失败: %v", err)
|
return nil, fmt.Errorf("解析响应失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if remoteInfo.Version == "" {
|
if remoteInfo.Version == "" {
|
||||||
log.Printf("[远程版本] 远程版本信息不完整,响应内容: %s", string(body))
|
|
||||||
return nil, fmt.Errorf("远程版本信息不完整")
|
return nil, fmt.Errorf("远程版本信息不完整")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[远程版本] 成功获取远程版本信息: %+v", remoteInfo)
|
|
||||||
return &remoteInfo, nil
|
return &remoteInfo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package service
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
@@ -71,20 +70,16 @@ func LoadUpdateConfig() (*UpdateConfig, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 同步最新版本号
|
|
||||||
latestVersion := GetCurrentVersion()
|
|
||||||
if config.CurrentVersion == "" || config.CurrentVersion != latestVersion {
|
|
||||||
if config.CurrentVersion != "" {
|
|
||||||
log.Printf("[配置] 配置中的版本号 (%s) 与最新版本号 (%s) 不一致", config.CurrentVersion, latestVersion)
|
|
||||||
}
|
|
||||||
config.CurrentVersion = latestVersion
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用默认检查地址
|
// 使用默认检查地址
|
||||||
if config.CheckURL == "" {
|
if config.CheckURL == "" {
|
||||||
config.CheckURL = "https://img.1216.top/u-desk/last-version.json"
|
config.CheckURL = "https://img.1216.top/u-desk/last-version.json"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 确保版本号不为空(使用缓存的版本号)
|
||||||
|
if config.CurrentVersion == "" {
|
||||||
|
config.CurrentVersion = GetCurrentVersion()
|
||||||
|
}
|
||||||
|
|
||||||
return &config, nil
|
return &config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ==================== 常量定义 ====================
|
// ==================== 常量定义 ====================
|
||||||
@@ -15,6 +16,12 @@ import (
|
|||||||
// AppVersion 应用版本号(发布时直接修改此处)
|
// AppVersion 应用版本号(发布时直接修改此处)
|
||||||
const AppVersion = "0.3.0"
|
const AppVersion = "0.3.0"
|
||||||
|
|
||||||
|
// 版本号缓存
|
||||||
|
var (
|
||||||
|
cachedVersion string
|
||||||
|
versionOnce sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
// ==================== 类型定义 ====================
|
// ==================== 类型定义 ====================
|
||||||
|
|
||||||
// Version 版本号结构
|
// Version 版本号结构
|
||||||
@@ -100,22 +107,25 @@ func (v *Version) IsOlderThan(other *Version) bool {
|
|||||||
|
|
||||||
// ==================== 版本号获取 ====================
|
// ==================== 版本号获取 ====================
|
||||||
|
|
||||||
// GetCurrentVersion 获取当前版本号
|
// GetCurrentVersion 获取当前版本号(带缓存)
|
||||||
// 优先级:硬编码版本号 > wails.json(开发模式)> 默认值
|
// 优先级:硬编码版本号 > wails.json(开发模式)> 默认值
|
||||||
func GetCurrentVersion() string {
|
func GetCurrentVersion() string {
|
||||||
if AppVersion != "" {
|
versionOnce.Do(func() {
|
||||||
log.Printf("[版本] 使用硬编码版本号: %s", AppVersion)
|
if AppVersion != "" {
|
||||||
return AppVersion
|
cachedVersion = AppVersion
|
||||||
}
|
return
|
||||||
|
}
|
||||||
|
|
||||||
version := getVersionFromWailsJSON()
|
version := getVersionFromWailsJSON()
|
||||||
if version != "" {
|
if version != "" {
|
||||||
log.Printf("[版本] 从 wails.json 获取版本号: %s", version)
|
cachedVersion = version
|
||||||
return version
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[版本] 使用默认版本号: 0.0.1")
|
cachedVersion = "0.0.1"
|
||||||
return "0.0.1"
|
})
|
||||||
|
|
||||||
|
return cachedVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 配置文件读取 ====================
|
// ==================== 配置文件读取 ====================
|
||||||
|
|||||||
833
web/package-lock.json
generated
833
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -34,10 +34,13 @@
|
|||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
"marked": "^17.0.1",
|
"marked": "^17.0.1",
|
||||||
"mermaid": "^11.12.2",
|
"mermaid": "^11.12.2",
|
||||||
|
"pinia": "^3.0.4",
|
||||||
"vue": "^3.5.26"
|
"vue": "^3.5.26"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^6.0.3",
|
"@vitejs/plugin-vue": "^6.0.3",
|
||||||
|
"unplugin-vue-components": "^0.27.4",
|
||||||
|
"unplugin-auto-import": "^0.18.3",
|
||||||
"vite": "^7.3.0"
|
"vite": "^7.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
242
web/src/App.vue
242
web/src/App.vue
@@ -58,29 +58,30 @@
|
|||||||
<!-- 设置抽屉 -->
|
<!-- 设置抽屉 -->
|
||||||
<SettingsPanel
|
<SettingsPanel
|
||||||
v-model="showSettings"
|
v-model="showSettings"
|
||||||
:config="appConfig"
|
:config="configStore.appConfig"
|
||||||
@save="handleSaveConfig"
|
@save="handleSaveConfig"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 升级提示弹窗 -->
|
<!-- 升级提示弹窗 -->
|
||||||
<UpdateNotification
|
<UpdateNotification
|
||||||
v-model="showUpdateNotification"
|
v-model="updateStore.showUpdate"
|
||||||
:update-info="updateInfo"
|
:update-info="updateStore.updateInfo"
|
||||||
@install="handleUpdateInstall"
|
@install="updateStore.installUpdate"
|
||||||
@skip="handleUpdateSkip"
|
|
||||||
/>
|
/>
|
||||||
</a-layout>
|
</a-layout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {computed, onMounted, ref, watch} from 'vue'
|
import { computed, watch, ref, onMounted, onUnmounted } from 'vue'
|
||||||
import {IconSettings} from '@arco-design/web-vue/es/icon'
|
import { IconSettings } from '@arco-design/web-vue/es/icon'
|
||||||
import {Message} from '@arco-design/web-vue'
|
import { Message } from '@arco-design/web-vue'
|
||||||
import DbCli from './views/db-cli/index.vue'
|
import DbCli from './views/db-cli/index.vue'
|
||||||
import ThemeToggle from './components/ThemeToggle.vue'
|
import ThemeToggle from './components/ThemeToggle.vue'
|
||||||
import FileSystem from './components/FileSystem/index.vue'
|
import FileSystem from './components/FileSystem/index.vue'
|
||||||
import SettingsPanel from './components/SettingsPanel.vue'
|
import SettingsPanel from './components/SettingsPanel.vue'
|
||||||
import UpdateNotification from './components/UpdateNotification.vue'
|
import UpdateNotification from './components/UpdateNotification.vue'
|
||||||
|
import { useUpdateStore } from './stores/update'
|
||||||
|
import { useConfigStore } from './stores/config'
|
||||||
|
|
||||||
// 存储键
|
// 存储键
|
||||||
const ACTIVE_TAB_STORAGE_KEY = 'app-active-tab'
|
const ACTIVE_TAB_STORAGE_KEY = 'app-active-tab'
|
||||||
@@ -91,124 +92,39 @@ const activeTab = ref((savedTab === 'user' ? 'file-system' : savedTab) || 'file-
|
|||||||
const showSettings = ref(false)
|
const showSettings = ref(false)
|
||||||
const isMaximized = ref(false)
|
const isMaximized = ref(false)
|
||||||
|
|
||||||
// 更新相关状态
|
// 使用 stores
|
||||||
const showUpdateNotification = ref(false)
|
const updateStore = useUpdateStore()
|
||||||
const updateInfo = ref(null)
|
const configStore = useConfigStore()
|
||||||
const checkedUpdate = ref(false)
|
|
||||||
|
|
||||||
// 应用配置
|
// 应用配置(从 store 获取)
|
||||||
const appConfig = ref({
|
const appConfig = computed(() => configStore.appConfig)
|
||||||
tabs: [],
|
|
||||||
visibleTabs: [],
|
|
||||||
defaultTab: 'file-system'
|
|
||||||
})
|
|
||||||
|
|
||||||
// 可见 Tabs(根据配置动态生成)
|
// 可见 Tabs(从 store 获取)
|
||||||
const visibleTabs = computed(() => {
|
const visibleTabs = computed(() => configStore.visibleTabs)
|
||||||
if (!appConfig.value.tabs || appConfig.value.tabs.length === 0) {
|
|
||||||
// 默认配置
|
|
||||||
return [
|
|
||||||
{key: 'file-system', title: '文件管理'},
|
|
||||||
{key: 'db-cli', title: '数据库'}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
return appConfig.value.tabs
|
|
||||||
.filter(tab => tab.visible)
|
|
||||||
.sort((a, b) => {
|
|
||||||
const aIndex = appConfig.value.visibleTabs.indexOf(a.key)
|
|
||||||
const bIndex = appConfig.value.visibleTabs.indexOf(b.key)
|
|
||||||
return aIndex - bIndex
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// 加载配置
|
|
||||||
const loadConfig = async () => {
|
|
||||||
try {
|
|
||||||
// 检查 Wails 绑定是否准备好
|
|
||||||
if (!window.go || !window.go.main || !window.go.main.App) {
|
|
||||||
console.warn('Wails 绑定未准备好,等待重试...')
|
|
||||||
setTimeout(() => loadConfig(), 100)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await window.go.main.App.GetAppConfig()
|
|
||||||
if (result.success) {
|
|
||||||
const tabs = result.data.tabs || []
|
|
||||||
const visibleTabs = result.data.visibleTabs || []
|
|
||||||
|
|
||||||
// 确保 tabs 数组中的 visible 属性与 visibleTabs 同步
|
|
||||||
const syncedTabs = tabs.map(tab => ({
|
|
||||||
...tab,
|
|
||||||
visible: visibleTabs.includes(tab.key)
|
|
||||||
}))
|
|
||||||
|
|
||||||
appConfig.value = {
|
|
||||||
tabs: syncedTabs,
|
|
||||||
visibleTabs: visibleTabs,
|
|
||||||
defaultTab: result.data.defaultTab || 'file-system'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置默认 Tab
|
|
||||||
activeTab.value = appConfig.value.defaultTab
|
|
||||||
} else {
|
|
||||||
console.error('加载配置失败:', result.message)
|
|
||||||
// 使用默认配置
|
|
||||||
useDefaultConfig()
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载配置失败:', error)
|
|
||||||
// 使用默认配置
|
|
||||||
useDefaultConfig()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用默认配置
|
|
||||||
const useDefaultConfig = () => {
|
|
||||||
appConfig.value = {
|
|
||||||
tabs: [
|
|
||||||
{key: 'file-system', title: '文件管理', visible: true, enabled: true},
|
|
||||||
{key: 'db-cli', title: '数据库', visible: true, enabled: true}
|
|
||||||
],
|
|
||||||
visibleTabs: ['file-system', 'db-cli'],
|
|
||||||
defaultTab: 'file-system'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存配置
|
// 保存配置
|
||||||
const handleSaveConfig = async (config) => {
|
const handleSaveConfig = async (config) => {
|
||||||
try {
|
try {
|
||||||
const result = await window.go.main.App.SaveAppConfig({
|
await configStore.saveConfig(config)
|
||||||
tabs: config.tabs,
|
showSettings.value = false
|
||||||
visibleTabs: config.visibleTabs,
|
|
||||||
defaultTab: config.defaultTab
|
|
||||||
})
|
|
||||||
|
|
||||||
if (result.success) {
|
// 如果当前激活的 Tab 被隐藏,切换到默认 Tab
|
||||||
// 更新本地配置
|
if (!config.visibleTabs.includes(activeTab.value)) {
|
||||||
appConfig.value = {
|
activeTab.value = config.defaultTab
|
||||||
tabs: [...config.tabs],
|
|
||||||
visibleTabs: [...config.visibleTabs],
|
|
||||||
defaultTab: config.defaultTab
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果当前激活的 Tab 被隐藏,切换到默认 Tab
|
|
||||||
if (!config.visibleTabs.includes(activeTab.value)) {
|
|
||||||
activeTab.value = config.defaultTab
|
|
||||||
}
|
|
||||||
|
|
||||||
Message.success('配置保存成功')
|
|
||||||
showSettings.value = false
|
|
||||||
} else {
|
|
||||||
Message.error(result.message || '保存配置失败')
|
|
||||||
throw new Error(result.message)
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// 错误已在 store 中处理
|
||||||
console.error('保存配置失败:', error)
|
console.error('保存配置失败:', error)
|
||||||
throw error
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加载配置(调用 store 方法)
|
||||||
|
const loadConfig = async () => {
|
||||||
|
await configStore.loadConfig()
|
||||||
|
// 设置默认 Tab
|
||||||
|
activeTab.value = configStore.defaultTab
|
||||||
|
}
|
||||||
|
|
||||||
// 获取组件
|
// 获取组件
|
||||||
const getComponent = (key) => {
|
const getComponent = (key) => {
|
||||||
const components = {
|
const components = {
|
||||||
@@ -218,75 +134,22 @@ const getComponent = (key) => {
|
|||||||
return components[key] || null
|
return components[key] || null
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查更新
|
// 组件挂载时加载配置
|
||||||
const checkForUpdates = async () => {
|
|
||||||
try {
|
|
||||||
// 等待 Wails 绑定准备好
|
|
||||||
if (!window.go || !window.go.main || !window.go.main.App) {
|
|
||||||
console.warn('Wails 绑定未准备好,延迟检查更新...')
|
|
||||||
setTimeout(() => checkForUpdates(), 1000)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取更新配置
|
|
||||||
const configResult = await window.go.main.App.GetUpdateConfig()
|
|
||||||
if (!configResult.success) {
|
|
||||||
console.error('获取更新配置失败:', configResult.message)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = configResult.data
|
|
||||||
const shouldCheck = config.auto_check_enabled
|
|
||||||
|
|
||||||
if (!shouldCheck) {
|
|
||||||
console.log('自动更新检查已关闭')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[自动检查] 开始检查更新...')
|
|
||||||
|
|
||||||
// 检查更新
|
|
||||||
const result = await window.go.main.App.CheckUpdate()
|
|
||||||
if (result.success && result.data) {
|
|
||||||
checkedUpdate.value = true
|
|
||||||
|
|
||||||
// 检查是否已跳过此版本
|
|
||||||
const skippedVersion = localStorage.getItem('skipped_version')
|
|
||||||
if (result.data.has_update) {
|
|
||||||
// 如果是强制更新,或者未跳过此版本,则显示提示
|
|
||||||
if (result.data.force_update || skippedVersion !== result.data.latest_version) {
|
|
||||||
console.log('[自动检查] 发现新版本:', result.data.latest_version)
|
|
||||||
updateInfo.value = result.data
|
|
||||||
// 延迟显示,让用户先看到应用界面
|
|
||||||
setTimeout(() => {
|
|
||||||
showUpdateNotification.value = true
|
|
||||||
}, 2000)
|
|
||||||
} else {
|
|
||||||
console.log('[自动检查] 此版本已跳过')
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('[自动检查] 已是最新版本')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('检查更新失败:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 组件挂载时加载配置和检查更新
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadConfig()
|
loadConfig()
|
||||||
// 延迟检查更新,避免阻塞应用启动
|
|
||||||
|
// 设置更新事件监听
|
||||||
|
updateStore.setupEventListeners()
|
||||||
|
|
||||||
|
// 延迟检查更新(启动后 3 秒,静默模式)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!checkedUpdate.value) {
|
updateStore.checkForUpdates(true)
|
||||||
checkForUpdates()
|
|
||||||
}
|
|
||||||
}, 3000)
|
}, 3000)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 监听 activeTab 变化,自动保存到 localStorage
|
// 组件卸载时清理事件监听
|
||||||
watch(activeTab, (newTab) => {
|
onUnmounted(() => {
|
||||||
localStorage.setItem(ACTIVE_TAB_STORAGE_KEY, newTab)
|
updateStore.removeEventListeners()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 窗口控制方法
|
// 窗口控制方法
|
||||||
@@ -321,29 +184,6 @@ const handleClose = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 升级提示事件处理
|
|
||||||
const handleUpdateInstall = async (filePath) => {
|
|
||||||
try {
|
|
||||||
const result = await window.go.main.App.InstallUpdate(filePath, true)
|
|
||||||
if (result.success) {
|
|
||||||
Message.success({
|
|
||||||
content: '安装成功!应用将在几秒后重启...',
|
|
||||||
duration: 3000
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Message.error(result.message || '安装失败')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('安装失败:', error)
|
|
||||||
Message.error('安装失败:' + (error.message || error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleUpdateSkip = () => {
|
|
||||||
// 清除跳过的版本记录(如果用户选择"稍后提醒")
|
|
||||||
// 版本记录在组件内部处理
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听 activeTab 变化,如果当前 Tab 不在可见列表中,切换到默认 Tab
|
// 监听 activeTab 变化,如果当前 Tab 不在可见列表中,切换到默认 Tab
|
||||||
watch(activeTab, (newTab) => {
|
watch(activeTab, (newTab) => {
|
||||||
// 保存到 localStorage
|
// 保存到 localStorage
|
||||||
@@ -351,8 +191,8 @@ watch(activeTab, (newTab) => {
|
|||||||
|
|
||||||
// 检查 Tab 是否在可见列表中
|
// 检查 Tab 是否在可见列表中
|
||||||
const isVisible = appConfig.value.visibleTabs.includes(newTab)
|
const isVisible = appConfig.value.visibleTabs.includes(newTab)
|
||||||
if (!isVisible && appConfig.value.visibleTabs.length > 0) {
|
if (!isVisible && appConfig.value.visibleTabs.length > 0 && newTab !== appConfig.value.defaultTab) {
|
||||||
// 切换到默认 Tab
|
// 切换到默认 Tab(避免重复触发)
|
||||||
activeTab.value = appConfig.value.defaultTab
|
activeTab.value = appConfig.value.defaultTab
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,6 +4,24 @@
|
|||||||
|
|
||||||
import type { SystemInfo, CPU, Memory, Disk, File } from './types'
|
import type { SystemInfo, CPU, Memory, Disk, File } from './types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换后端文件数据格式(蛇形 → 驼峰)
|
||||||
|
* 后端返回 is_dir,前端使用 isDir
|
||||||
|
*/
|
||||||
|
function transformFile(file: any): File {
|
||||||
|
return {
|
||||||
|
...file,
|
||||||
|
isDir: file.is_dir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量转换文件列表
|
||||||
|
*/
|
||||||
|
function transformFileList(files: any[]): File[] {
|
||||||
|
return files.map(transformFile)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取系统信息
|
* 获取系统信息
|
||||||
*/
|
*/
|
||||||
@@ -51,7 +69,9 @@ export async function listDir(path: string): Promise<File[]> {
|
|||||||
if (!window.go?.main?.App?.ListDir) {
|
if (!window.go?.main?.App?.ListDir) {
|
||||||
throw new Error('ListDir API 不可用')
|
throw new Error('ListDir API 不可用')
|
||||||
}
|
}
|
||||||
return await window.go.main.App.ListDir(path)
|
|
||||||
|
const files = await window.go.main.App.ListDir(path)
|
||||||
|
return transformFileList(files)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -81,41 +101,41 @@ export async function writeFile(path: string, content: string): Promise<void> {
|
|||||||
/**
|
/**
|
||||||
* 删除文件或目录
|
* 删除文件或目录
|
||||||
*/
|
*/
|
||||||
export async function deletePath(path: string): Promise<void> {
|
export async function deletePath(path: string): Promise<any> {
|
||||||
if (!window.go?.main?.App?.DeletePath) {
|
if (!window.go?.main?.App?.DeletePath) {
|
||||||
throw new Error('DeletePath API 不可用')
|
throw new Error('DeletePath API 不可用')
|
||||||
}
|
}
|
||||||
await window.go.main.App.DeletePath(path)
|
return await window.go.main.App.DeletePath(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建目录
|
* 创建目录
|
||||||
*/
|
*/
|
||||||
export async function createDir(path: string): Promise<void> {
|
export async function createDir(path: string): Promise<any> {
|
||||||
if (!window.go?.main?.App?.CreateDir) {
|
if (!window.go?.main?.App?.CreateDir) {
|
||||||
throw new Error('CreateDir API 不可用')
|
throw new Error('CreateDir API 不可用')
|
||||||
}
|
}
|
||||||
await window.go.main.App.CreateDir(path)
|
return await window.go.main.App.CreateDir(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建文件
|
* 创建文件
|
||||||
*/
|
*/
|
||||||
export async function createFile(path: string): Promise<void> {
|
export async function createFile(path: string): Promise<any> {
|
||||||
if (!window.go?.main?.App?.CreateFile) {
|
if (!window.go?.main?.App?.CreateFile) {
|
||||||
throw new Error('CreateFile API 不可用')
|
throw new Error('CreateFile API 不可用')
|
||||||
}
|
}
|
||||||
await window.go.main.App.CreateFile(path)
|
return await window.go.main.App.CreateFile(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 重命名文件或目录
|
* 重命名文件或目录
|
||||||
*/
|
*/
|
||||||
export async function renamePath(oldPath: string, newPath: string): Promise<void> {
|
export async function renamePath(oldPath: string, newPath: string): Promise<any> {
|
||||||
if (!window.go?.main?.App?.RenamePath) {
|
if (!window.go?.main?.App?.RenamePath) {
|
||||||
throw new Error('RenamePath API 不可用')
|
throw new Error('RenamePath API 不可用')
|
||||||
}
|
}
|
||||||
await window.go.main.App.RenamePath({
|
return await window.go.main.App.RenamePath({
|
||||||
oldPath: String(oldPath),
|
oldPath: String(oldPath),
|
||||||
newPath: String(newPath)
|
newPath: String(newPath)
|
||||||
})
|
})
|
||||||
@@ -135,14 +155,12 @@ export async function getEnvVars(): Promise<Record<string, string>> {
|
|||||||
* 列出 zip 文件内容
|
* 列出 zip 文件内容
|
||||||
*/
|
*/
|
||||||
export async function listZipContents(zipPath: string): Promise<File[]> {
|
export async function listZipContents(zipPath: string): Promise<File[]> {
|
||||||
console.log('[API] listZipContents 调用:', zipPath)
|
|
||||||
if (!window.go?.main?.App?.ListZipContents) {
|
if (!window.go?.main?.App?.ListZipContents) {
|
||||||
throw new Error('ListZipContents API 不可用')
|
throw new Error('ListZipContents API 不可用')
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const result = await window.go.main.App.ListZipContents(zipPath)
|
const result = await window.go.main.App.ListZipContents(zipPath)
|
||||||
console.log('[API] listZipContents 结果:', result?.length || 0, '个文件')
|
return transformFileList(result)
|
||||||
return result
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[API] listZipContents 错误:', error)
|
console.error('[API] listZipContents 错误:', error)
|
||||||
throw error
|
throw error
|
||||||
@@ -153,13 +171,11 @@ export async function listZipContents(zipPath: string): Promise<File[]> {
|
|||||||
* 从 zip 文件中提取单个文件内容
|
* 从 zip 文件中提取单个文件内容
|
||||||
*/
|
*/
|
||||||
export async function extractFileFromZip(zipPath: string, filePath: string): Promise<string> {
|
export async function extractFileFromZip(zipPath: string, filePath: string): Promise<string> {
|
||||||
console.log('[API] extractFileFromZip 调用:', { zipPath, filePath })
|
|
||||||
if (!window.go?.main?.App?.ExtractFileFromZip) {
|
if (!window.go?.main?.App?.ExtractFileFromZip) {
|
||||||
throw new Error('ExtractFileFromZip API 不可用')
|
throw new Error('ExtractFileFromZip API 不可用')
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const result = await window.go.main.App.ExtractFileFromZip(zipPath, filePath)
|
const result = await window.go.main.App.ExtractFileFromZip(zipPath, filePath)
|
||||||
console.log('[API] extractFileFromZip 成功, 内容长度:', result?.length || 0)
|
|
||||||
return result
|
return result
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[API] extractFileFromZip 错误:', error)
|
console.error('[API] extractFileFromZip 错误:', error)
|
||||||
@@ -172,13 +188,11 @@ export async function extractFileFromZip(zipPath: string, filePath: string): Pro
|
|||||||
* 返回临时文件的完整路径,适用于图片等二进制文件
|
* 返回临时文件的完整路径,适用于图片等二进制文件
|
||||||
*/
|
*/
|
||||||
export async function extractFileFromZipToTemp(zipPath: string, filePath: string): Promise<string> {
|
export async function extractFileFromZipToTemp(zipPath: string, filePath: string): Promise<string> {
|
||||||
console.log('[API] extractFileFromZipToTemp 调用:', { zipPath, filePath })
|
|
||||||
if (!window.go?.main?.App?.ExtractFileFromZipToTemp) {
|
if (!window.go?.main?.App?.ExtractFileFromZipToTemp) {
|
||||||
throw new Error('ExtractFileFromZipToTemp API 不可用')
|
throw new Error('ExtractFileFromZipToTemp API 不可用')
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const result = await window.go.main.App.ExtractFileFromZipToTemp(zipPath, filePath)
|
const result = await window.go.main.App.ExtractFileFromZipToTemp(zipPath, filePath)
|
||||||
console.log('[API] extractFileFromZipToTemp 成功, 临时文件路径:', result)
|
|
||||||
return result
|
return result
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[API] extractFileFromZipToTemp 错误:', error)
|
console.error('[API] extractFileFromZipToTemp 错误:', error)
|
||||||
@@ -190,14 +204,12 @@ export async function extractFileFromZipToTemp(zipPath: string, filePath: string
|
|||||||
* 获取 zip 文件中特定文件的信息
|
* 获取 zip 文件中特定文件的信息
|
||||||
*/
|
*/
|
||||||
export async function getZipFileInfo(zipPath: string, filePath: string): Promise<File> {
|
export async function getZipFileInfo(zipPath: string, filePath: string): Promise<File> {
|
||||||
console.log('[API] getZipFileInfo 调用:', { zipPath, filePath })
|
|
||||||
if (!window.go?.main?.App?.GetZipFileInfo) {
|
if (!window.go?.main?.App?.GetZipFileInfo) {
|
||||||
throw new Error('GetZipFileInfo API 不可用')
|
throw new Error('GetZipFileInfo API 不可用')
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const result = await window.go.main.App.GetZipFileInfo(zipPath, filePath)
|
const result = await window.go.main.App.GetZipFileInfo(zipPath, filePath)
|
||||||
console.log('[API] getZipFileInfo 结果:', result)
|
return transformFile(result)
|
||||||
return result
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[API] getZipFileInfo 错误:', error)
|
console.error('[API] getZipFileInfo 错误:', error)
|
||||||
throw error
|
throw error
|
||||||
@@ -208,13 +220,11 @@ export async function getZipFileInfo(zipPath: string, filePath: string): Promise
|
|||||||
* 使用系统默认程序打开文件或目录
|
* 使用系统默认程序打开文件或目录
|
||||||
*/
|
*/
|
||||||
export async function openPath(path: string): Promise<void> {
|
export async function openPath(path: string): Promise<void> {
|
||||||
console.log('[API] openPath 调用:', path)
|
|
||||||
if (!window.go?.main?.App?.OpenPath) {
|
if (!window.go?.main?.App?.OpenPath) {
|
||||||
throw new Error('OpenPath API 不可用')
|
throw new Error('OpenPath API 不可用')
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await window.go.main.App.OpenPath(path)
|
await window.go.main.App.OpenPath(path)
|
||||||
console.log('[API] openPath 成功')
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[API] openPath 错误:', error)
|
console.error('[API] openPath 错误:', error)
|
||||||
throw error
|
throw error
|
||||||
@@ -242,13 +252,11 @@ export async function resolveShortcut(lnkPath: string): Promise<{
|
|||||||
targetAccessible?: boolean
|
targetAccessible?: boolean
|
||||||
targetInfo?: any
|
targetInfo?: any
|
||||||
}> {
|
}> {
|
||||||
console.log('[API] resolveShortcut 调用:', lnkPath)
|
|
||||||
if (!window.go?.main?.App?.ResolveShortcut) {
|
if (!window.go?.main?.App?.ResolveShortcut) {
|
||||||
throw new Error('ResolveShortcut API 不可用')
|
throw new Error('ResolveShortcut API 不可用')
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const result = await window.go.main.App.ResolveShortcut(lnkPath)
|
const result = await window.go.main.App.ResolveShortcut(lnkPath)
|
||||||
console.log('[API] resolveShortcut 结果:', result)
|
|
||||||
return result
|
return result
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[API] resolveShortcut 错误:', error)
|
console.error('[API] resolveShortcut 错误:', error)
|
||||||
|
|||||||
@@ -3,121 +3,32 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, watch, onBeforeUnmount, computed } from 'vue'
|
import { ref, onMounted, watch, onBeforeUnmount, computed, nextTick } from 'vue'
|
||||||
import { EditorView, lineNumbers, highlightActiveLineGutter, keymap } from '@codemirror/view'
|
import { EditorView, lineNumbers, highlightActiveLineGutter, keymap } from '@codemirror/view'
|
||||||
import { EditorState } from '@codemirror/state'
|
import { EditorState } from '@codemirror/state'
|
||||||
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands'
|
import { defaultKeymap, history } from '@codemirror/commands'
|
||||||
import { javascript } from '@codemirror/lang-javascript'
|
|
||||||
import { json } from '@codemirror/lang-json'
|
|
||||||
import { cpp } from '@codemirror/lang-cpp'
|
|
||||||
import { css } from '@codemirror/lang-css'
|
|
||||||
import { go } from '@codemirror/lang-go'
|
|
||||||
import { html } from '@codemirror/lang-html'
|
|
||||||
import { java } from '@codemirror/lang-java'
|
|
||||||
import { markdown } from '@codemirror/lang-markdown'
|
|
||||||
import { php } from '@codemirror/lang-php'
|
|
||||||
import { python } from '@codemirror/lang-python'
|
|
||||||
import { rust } from '@codemirror/lang-rust'
|
|
||||||
import { sql } from '@codemirror/lang-sql'
|
|
||||||
import { yaml } from '@codemirror/lang-yaml'
|
|
||||||
import { StreamLanguage } from '@codemirror/language'
|
|
||||||
import { oneDark } from '@codemirror/theme-one-dark'
|
|
||||||
import { bracketMatching } from '@codemirror/language'
|
import { bracketMatching } from '@codemirror/language'
|
||||||
import { useTheme } from '@/composables/useTheme'
|
import { oneDark } from '@codemirror/theme-one-dark'
|
||||||
|
import { useThemeStore } from '@/stores/theme'
|
||||||
|
import { loadLanguageExtension, getLanguageFromExtension } from '@/utils/codeMirrorLoader'
|
||||||
|
|
||||||
// Legacy modes for languages without dedicated packages
|
|
||||||
import { csharp, kotlin } from '@codemirror/legacy-modes/mode/clike'
|
|
||||||
import { swift } from '@codemirror/legacy-modes/mode/swift'
|
|
||||||
import { ruby } from '@codemirror/legacy-modes/mode/ruby'
|
|
||||||
import { shell } from '@codemirror/legacy-modes/mode/shell'
|
|
||||||
import { octave } from '@codemirror/legacy-modes/mode/octave'
|
|
||||||
import { perl } from '@codemirror/legacy-modes/mode/perl'
|
|
||||||
import { r } from '@codemirror/legacy-modes/mode/r'
|
|
||||||
import { properties } from '@codemirror/legacy-modes/mode/properties'
|
|
||||||
import { dockerFile } from '@codemirror/legacy-modes/mode/dockerfile'
|
|
||||||
import { stex } from '@codemirror/legacy-modes/mode/stex'
|
|
||||||
import { xml } from '@codemirror/legacy-modes/mode/xml'
|
|
||||||
|
|
||||||
// ==================== Constants ====================
|
|
||||||
// 文件扩展名到 CodeMirror 语言包的映射
|
|
||||||
const LANGUAGE_MAP = {
|
|
||||||
// JavaScript/TypeScript (使用 javascript 包)
|
|
||||||
javascript: ['js', 'jsx', 'mjs', 'cjs', 'cts', 'mts'],
|
|
||||||
typescript: ['ts', 'tsx', 'cts', 'mts'],
|
|
||||||
|
|
||||||
// 数据格式
|
|
||||||
json: ['json'],
|
|
||||||
yaml: ['yaml', 'yml'],
|
|
||||||
xml: ['xml', 'xhtml', 'svg'],
|
|
||||||
|
|
||||||
// Web
|
|
||||||
html: ['html', 'htm'],
|
|
||||||
css: ['css', 'scss', 'sass', 'less'],
|
|
||||||
|
|
||||||
// 系统编程
|
|
||||||
cpp: ['cpp', 'c', 'cc', 'cxx', 'h', 'hpp', 'hxx'],
|
|
||||||
rust: ['rs'],
|
|
||||||
go: ['go'],
|
|
||||||
|
|
||||||
// 脚本语言
|
|
||||||
python: ['py', 'pyw'],
|
|
||||||
php: ['php'],
|
|
||||||
ruby: ['rb'],
|
|
||||||
perl: ['pl', 'pm'],
|
|
||||||
shell: ['sh', 'bash', 'zsh', 'fish', 'cmd', 'bat', 'ps1'],
|
|
||||||
sql: ['sql'],
|
|
||||||
|
|
||||||
// JVM 语言
|
|
||||||
java: ['java'],
|
|
||||||
kotlin: ['kt', 'kts'],
|
|
||||||
csharp: ['cs', 'csx'],
|
|
||||||
|
|
||||||
// 其他语言
|
|
||||||
swift: ['swift'],
|
|
||||||
markdown: ['md', 'markdown'],
|
|
||||||
r: ['r'],
|
|
||||||
matlab: ['m'],
|
|
||||||
latex: ['tex'],
|
|
||||||
makefile: ['makefile', 'make', 'mk', 'gnumakefile'],
|
|
||||||
ini: ['ini', 'cfg', 'conf', 'properties'],
|
|
||||||
dockerfile: ['dockerfile', 'containerfile'],
|
|
||||||
gitignore: ['gitignore', 'gitignore-global', 'gitattributes'],
|
|
||||||
|
|
||||||
// 纯文本(未知类型)
|
|
||||||
text: ['txt', 'text', 'log', 'csv']
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== Props & Emits ====================
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: { type: String, required: true },
|
||||||
type: String,
|
fileExtension: { type: String, default: '' }
|
||||||
required: true
|
|
||||||
},
|
|
||||||
fileExtension: {
|
|
||||||
type: String,
|
|
||||||
default: ''
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue'])
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
// ==================== State ====================
|
const themeStore = useThemeStore()
|
||||||
const { isDark } = useTheme()
|
|
||||||
const editorContainer = ref(null)
|
const editorContainer = ref(null)
|
||||||
let view = null
|
let view = null
|
||||||
|
|
||||||
// ==================== Editor Management ====================
|
const createExtensions = async () => {
|
||||||
/**
|
|
||||||
* 创建编辑器扩展配置
|
|
||||||
*/
|
|
||||||
const createExtensions = () => {
|
|
||||||
const extensions = [
|
const extensions = [
|
||||||
lineNumbers(),
|
lineNumbers(),
|
||||||
highlightActiveLineGutter(),
|
highlightActiveLineGutter(),
|
||||||
history(),
|
history(),
|
||||||
keymap.of(defaultKeymap),
|
keymap.of(defaultKeymap),
|
||||||
// 不使用 historyKeymap,避免 Ctrl+Z 与外部重置功能冲突
|
|
||||||
// 用户可以通过外部的重置按钮或 Ctrl+Z(全局快捷键)恢复原始内容
|
|
||||||
bracketMatching(),
|
bracketMatching(),
|
||||||
EditorView.updateListener.of((update) => {
|
EditorView.updateListener.of((update) => {
|
||||||
if (update.docChanged) {
|
if (update.docChanged) {
|
||||||
@@ -125,202 +36,64 @@ const createExtensions = () => {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
EditorView.theme({
|
EditorView.theme({
|
||||||
'&': {
|
'&': { height: '100%', fontSize: '13px' },
|
||||||
height: '100%',
|
'.cm-scroller': { overflow: 'auto', fontFamily: 'Consolas, Monaco, Courier New, monospace' },
|
||||||
fontSize: '13px'
|
'.cm-content': { padding: '8px', minHeight: '100%' },
|
||||||
},
|
'.cm-line': { padding: '0 0' },
|
||||||
'.cm-scroller': {
|
'&.cm-focused': { outline: 'none' }
|
||||||
overflow: 'auto',
|
|
||||||
fontFamily: 'Consolas, Monaco, Courier New, monospace'
|
|
||||||
},
|
|
||||||
'.cm-content': {
|
|
||||||
padding: '8px',
|
|
||||||
minHeight: '100%'
|
|
||||||
},
|
|
||||||
'.cm-line': {
|
|
||||||
padding: '0 0'
|
|
||||||
},
|
|
||||||
'&.cm-focused': {
|
|
||||||
outline: 'none'
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
|
|
||||||
// 主题
|
if (themeStore.isDark) {
|
||||||
if (isDark.value) {
|
|
||||||
extensions.push(oneDark)
|
extensions.push(oneDark)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 语言支持
|
const language = getLanguageFromExtension(props.fileExtension)
|
||||||
const ext = props.fileExtension.toLowerCase()
|
if (language !== 'text') {
|
||||||
|
const langExtension = await loadLanguageExtension(language)
|
||||||
// JavaScript/TypeScript
|
if (langExtension) {
|
||||||
if (LANGUAGE_MAP.javascript.includes(ext) || LANGUAGE_MAP.typescript.includes(ext)) {
|
extensions.push(langExtension)
|
||||||
extensions.push(javascript({ jsx: true }))
|
}
|
||||||
}
|
}
|
||||||
// JSON
|
|
||||||
else if (LANGUAGE_MAP.json.includes(ext)) {
|
|
||||||
extensions.push(json())
|
|
||||||
}
|
|
||||||
// YAML
|
|
||||||
else if (LANGUAGE_MAP.yaml.includes(ext)) {
|
|
||||||
extensions.push(yaml())
|
|
||||||
}
|
|
||||||
// HTML
|
|
||||||
else if (LANGUAGE_MAP.html.includes(ext)) {
|
|
||||||
extensions.push(html())
|
|
||||||
}
|
|
||||||
// CSS (including SCSS, SASS, LESS)
|
|
||||||
else if (LANGUAGE_MAP.css.includes(ext)) {
|
|
||||||
extensions.push(css())
|
|
||||||
}
|
|
||||||
// C/C++
|
|
||||||
else if (LANGUAGE_MAP.cpp.includes(ext)) {
|
|
||||||
extensions.push(cpp())
|
|
||||||
}
|
|
||||||
// Rust
|
|
||||||
else if (LANGUAGE_MAP.rust.includes(ext)) {
|
|
||||||
extensions.push(rust())
|
|
||||||
}
|
|
||||||
// Go
|
|
||||||
else if (LANGUAGE_MAP.go.includes(ext)) {
|
|
||||||
extensions.push(go())
|
|
||||||
}
|
|
||||||
// Python
|
|
||||||
else if (LANGUAGE_MAP.python.includes(ext)) {
|
|
||||||
extensions.push(python())
|
|
||||||
}
|
|
||||||
// PHP
|
|
||||||
else if (LANGUAGE_MAP.php.includes(ext)) {
|
|
||||||
extensions.push(php())
|
|
||||||
}
|
|
||||||
// SQL
|
|
||||||
else if (LANGUAGE_MAP.sql.includes(ext)) {
|
|
||||||
extensions.push(sql())
|
|
||||||
}
|
|
||||||
// Markdown
|
|
||||||
else if (LANGUAGE_MAP.markdown.includes(ext)) {
|
|
||||||
extensions.push(markdown())
|
|
||||||
}
|
|
||||||
// Java
|
|
||||||
else if (LANGUAGE_MAP.java.includes(ext)) {
|
|
||||||
extensions.push(java())
|
|
||||||
}
|
|
||||||
// Ruby
|
|
||||||
else if (LANGUAGE_MAP.ruby.includes(ext)) {
|
|
||||||
extensions.push(StreamLanguage.define(ruby))
|
|
||||||
}
|
|
||||||
// Shell
|
|
||||||
else if (LANGUAGE_MAP.shell.includes(ext)) {
|
|
||||||
extensions.push(StreamLanguage.define(shell))
|
|
||||||
}
|
|
||||||
// Kotlin
|
|
||||||
else if (LANGUAGE_MAP.kotlin.includes(ext)) {
|
|
||||||
extensions.push(StreamLanguage.define(kotlin))
|
|
||||||
}
|
|
||||||
// C#
|
|
||||||
else if (LANGUAGE_MAP.csharp.includes(ext)) {
|
|
||||||
extensions.push(StreamLanguage.define(csharp))
|
|
||||||
}
|
|
||||||
// Swift
|
|
||||||
else if (LANGUAGE_MAP.swift.includes(ext)) {
|
|
||||||
extensions.push(StreamLanguage.define(swift))
|
|
||||||
}
|
|
||||||
// R
|
|
||||||
else if (LANGUAGE_MAP.r.includes(ext)) {
|
|
||||||
extensions.push(StreamLanguage.define(r))
|
|
||||||
}
|
|
||||||
// Perl
|
|
||||||
else if (LANGUAGE_MAP.perl.includes(ext)) {
|
|
||||||
extensions.push(StreamLanguage.define(perl))
|
|
||||||
}
|
|
||||||
// LaTeX
|
|
||||||
else if (LANGUAGE_MAP.latex.includes(ext)) {
|
|
||||||
extensions.push(StreamLanguage.define(stex))
|
|
||||||
}
|
|
||||||
// Makefile (使用纯文本,legacy-modes 没有专门的 makefile 支持)
|
|
||||||
else if (LANGUAGE_MAP.makefile.includes(ext)) {
|
|
||||||
// 纯文本模式,不添加语言扩展
|
|
||||||
}
|
|
||||||
// INI/Properties/Dockerfile
|
|
||||||
else if (LANGUAGE_MAP.ini.includes(ext)) {
|
|
||||||
extensions.push(StreamLanguage.define(properties))
|
|
||||||
}
|
|
||||||
else if (LANGUAGE_MAP.dockerfile.includes(ext)) {
|
|
||||||
extensions.push(StreamLanguage.define(dockerFile))
|
|
||||||
}
|
|
||||||
// XML (包括 SVG)
|
|
||||||
else if (LANGUAGE_MAP.xml.includes(ext)) {
|
|
||||||
extensions.push(StreamLanguage.define(xml))
|
|
||||||
}
|
|
||||||
// Matlab/Octave
|
|
||||||
else if (LANGUAGE_MAP.matlab.includes(ext)) {
|
|
||||||
extensions.push(StreamLanguage.define(octave))
|
|
||||||
}
|
|
||||||
// 其他类型(包括 gitignore, dockerfile, txt 等)使用纯文本模式
|
|
||||||
// 不添加任何语言扩展,保持纯文本
|
|
||||||
|
|
||||||
return extensions
|
return extensions
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const createEditor = async (docContent = '') => {
|
||||||
* 创建编辑器实例
|
|
||||||
*/
|
|
||||||
const createEditor = (docContent = '') => {
|
|
||||||
if (!editorContainer.value) return
|
if (!editorContainer.value) return
|
||||||
|
|
||||||
const state = EditorState.create({
|
const extensions = await createExtensions()
|
||||||
doc: docContent,
|
const state = EditorState.create({ doc: docContent, extensions })
|
||||||
extensions: createExtensions()
|
view = new EditorView({ state, parent: editorContainer.value })
|
||||||
})
|
|
||||||
|
|
||||||
view = new EditorView({
|
|
||||||
state,
|
|
||||||
parent: editorContainer.value
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const recreateEditor = async () => {
|
||||||
* 重建编辑器(保留内容)
|
|
||||||
*/
|
|
||||||
const recreateEditor = () => {
|
|
||||||
if (!view) return
|
if (!view) return
|
||||||
const currentDoc = view.state.doc.toString()
|
const currentDoc = view.state.doc.toString()
|
||||||
view.destroy()
|
view.destroy()
|
||||||
createEditor(currentDoc)
|
await createEditor(currentDoc)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Lifecycle ====================
|
onMounted(async () => {
|
||||||
onMounted(() => {
|
await createEditor(props.modelValue || '')
|
||||||
createEditor(props.modelValue || '')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (view) {
|
view?.destroy()
|
||||||
view.destroy()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// ==================== Watchers ====================
|
|
||||||
// 监听外部内容变化
|
|
||||||
watch(() => props.modelValue, (newValue) => {
|
watch(() => props.modelValue, (newValue) => {
|
||||||
if (view && newValue !== view.state.doc.toString()) {
|
if (view && newValue !== view.state.doc.toString()) {
|
||||||
view.dispatch({
|
view.dispatch({
|
||||||
changes: {
|
changes: { from: 0, to: view.state.doc.length, insert: newValue || '' }
|
||||||
from: 0,
|
|
||||||
to: view.state.doc.length,
|
|
||||||
insert: newValue || ''
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 监听主题或文件扩展名变化,重建编辑器
|
const isDark = computed(() => themeStore.isDark)
|
||||||
// 使用 nextTick 确保 DOM 更新完成后再重建,避免视觉抖动
|
|
||||||
import { nextTick } from 'vue'
|
|
||||||
watch([isDark, () => props.fileExtension], async () => {
|
watch([isDark, () => props.fileExtension], async () => {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
recreateEditor()
|
await recreateEditor()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ const {
|
|||||||
deleteFile,
|
deleteFile,
|
||||||
} = useFileOperations({
|
} = useFileOperations({
|
||||||
onSuccess: (operation, data) => {
|
onSuccess: (operation, data) => {
|
||||||
console.log(`[DeviceTest] ${operation} 成功:`, data)
|
// 成功回调
|
||||||
},
|
},
|
||||||
onError: (operation, error) => {
|
onError: (operation, error) => {
|
||||||
console.error(`[DeviceTest] ${operation} 失败:`, error)
|
console.error(`[DeviceTest] ${operation} 失败:`, error)
|
||||||
@@ -271,7 +271,6 @@ const { storedValue: pathHistory } = useLocalStorage(
|
|||||||
try {
|
try {
|
||||||
const oldContent = localStorage.getItem(STORAGE_KEYS.DEVICE_TEST.FILE_CONTENT)
|
const oldContent = localStorage.getItem(STORAGE_KEYS.DEVICE_TEST.FILE_CONTENT)
|
||||||
if (oldContent) {
|
if (oldContent) {
|
||||||
console.log('[DeviceTest] 清理旧的文件内容缓存')
|
|
||||||
localStorage.removeItem(STORAGE_KEYS.DEVICE_TEST.FILE_CONTENT)
|
localStorage.removeItem(STORAGE_KEYS.DEVICE_TEST.FILE_CONTENT)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -47,6 +47,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { ContextMenuConfig, FileItem } from '@/types/file-system'
|
import type { ContextMenuConfig, FileItem } from '@/types/file-system'
|
||||||
|
import { isOfficeFile } from '@/utils/fileTypeHelpers'
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -63,15 +64,6 @@ interface Emits {
|
|||||||
|
|
||||||
const emit = defineEmits<Emits>()
|
const emit = defineEmits<Emits>()
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断是否为 Office 文件
|
|
||||||
*/
|
|
||||||
const isOfficeFile = (filename: string): boolean => {
|
|
||||||
if (!filename || typeof filename !== 'string') return false
|
|
||||||
const ext = filename.split('.').pop()?.toLowerCase()
|
|
||||||
return ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'].includes(ext || '')
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理菜单项点击
|
* 处理菜单项点击
|
||||||
*/
|
*/
|
||||||
|
|||||||
272
web/src/components/FileSystem/components/DropdownItem.vue
Normal file
272
web/src/components/FileSystem/components/DropdownItem.vue
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="dropdown-item"
|
||||||
|
@mouseenter="onHover"
|
||||||
|
@mouseleave="onLeave"
|
||||||
|
@click="onClick"
|
||||||
|
>
|
||||||
|
<div class="item-content">
|
||||||
|
<icon-folder v-if="item.isDir" />
|
||||||
|
<icon-file v-else />
|
||||||
|
<span class="item-name">{{ item.name }}</span>
|
||||||
|
<icon-right v-if="item.isDir" class="item-arrow" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 子级菜单(递归) -->
|
||||||
|
<Transition name="dropdown-fade">
|
||||||
|
<div
|
||||||
|
v-if="visible"
|
||||||
|
class="sub-dropdown"
|
||||||
|
:style="style"
|
||||||
|
@mouseenter="onSubmenuHover"
|
||||||
|
@mouseleave="onSubmenuLeave"
|
||||||
|
>
|
||||||
|
<div v-if="loading" class="dropdown-loading">
|
||||||
|
<a-spin :size="16" />
|
||||||
|
<span>加载中...</span>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="error" class="dropdown-error">
|
||||||
|
<icon-exclamation-circle />
|
||||||
|
<span>{{ error }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="!children.length" class="dropdown-empty">
|
||||||
|
<icon-folder />
|
||||||
|
<span>空文件夹</span>
|
||||||
|
</div>
|
||||||
|
<template v-else>
|
||||||
|
<DropdownItem
|
||||||
|
v-for="child in children"
|
||||||
|
:key="child.path"
|
||||||
|
:item="child"
|
||||||
|
:level="level + 1"
|
||||||
|
@navigate="emitNavigate"
|
||||||
|
@openFile="emitOpenFile"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, inject, watch, type Ref } from 'vue'
|
||||||
|
import { IconRight, IconFolder, IconFile, IconExclamationCircle } from '@arco-design/web-vue/es/icon'
|
||||||
|
import { listDir } from '@/api/system'
|
||||||
|
import { sortFileList } from '@/utils/fileUtils'
|
||||||
|
import { useTimeout } from '@/composables/useTimeout'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
item: {
|
||||||
|
name: string
|
||||||
|
path: string
|
||||||
|
isDir: boolean
|
||||||
|
}
|
||||||
|
level: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'navigate', path: string): void
|
||||||
|
(e: 'openFile', path: string): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>()
|
||||||
|
|
||||||
|
const { setTimeout: delay, clearTimeout } = useTimeout()
|
||||||
|
|
||||||
|
const openMenus = inject<Ref<Map<number, string>>>('openMenus', ref(new Map()))
|
||||||
|
const closeMenu = inject<(level: number) => void>('closeMenu', () => {})
|
||||||
|
|
||||||
|
const visible = ref(false)
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref('')
|
||||||
|
const children = ref<Array<{ name: string; path: string; isDir: boolean }>>([])
|
||||||
|
const style = ref<{ top: string; left: string }>({ top: '0px', left: '0px' })
|
||||||
|
|
||||||
|
const hoverTimer = ref<number | null>(null)
|
||||||
|
const leaveTimer = ref<number | null>(null)
|
||||||
|
const hoveringMenu = ref(false)
|
||||||
|
|
||||||
|
const menuKey = `menu-${props.item.path}-${props.level}`
|
||||||
|
|
||||||
|
watch(openMenus, (map) => {
|
||||||
|
visible.value = map.get(props.level) === menuKey
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
|
const loadChildren = async () => {
|
||||||
|
if (!props.item.isDir) return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
error.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const files = await listDir(props.item.path)
|
||||||
|
children.value = sortFileList(files.map(f => ({
|
||||||
|
name: f.name,
|
||||||
|
path: f.path,
|
||||||
|
isDir: f.isDir
|
||||||
|
})))
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[DropdownItem] 加载失败:', err)
|
||||||
|
error.value = '加载失败'
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openMenu = (rect: DOMRect) => {
|
||||||
|
closeMenu(props.level)
|
||||||
|
|
||||||
|
const newMap = new Map(openMenus.value)
|
||||||
|
newMap.set(props.level, menuKey)
|
||||||
|
openMenus.value = newMap
|
||||||
|
|
||||||
|
style.value = {
|
||||||
|
top: `${rect.top}px`,
|
||||||
|
left: `${rect.right + 4}px`
|
||||||
|
}
|
||||||
|
|
||||||
|
visible.value = true
|
||||||
|
loadChildren()
|
||||||
|
}
|
||||||
|
|
||||||
|
const scheduleClose = (ms: number) => {
|
||||||
|
return delay(() => {
|
||||||
|
if (!hoveringMenu.value) closeMenu(props.level)
|
||||||
|
}, ms)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onHover = (event: MouseEvent) => {
|
||||||
|
if (!props.item.isDir) return
|
||||||
|
|
||||||
|
hoveringMenu.value = false
|
||||||
|
if (leaveTimer.value) clearTimeout(leaveTimer.value)
|
||||||
|
|
||||||
|
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect()
|
||||||
|
hoverTimer.value = delay(() => openMenu(rect), 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onLeave = () => {
|
||||||
|
if (hoverTimer.value) clearTimeout(hoverTimer.value)
|
||||||
|
leaveTimer.value = scheduleClose(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSubmenuHover = () => {
|
||||||
|
hoveringMenu.value = true
|
||||||
|
if (leaveTimer.value) clearTimeout(leaveTimer.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSubmenuLeave = () => {
|
||||||
|
hoveringMenu.value = false
|
||||||
|
leaveTimer.value = scheduleClose(100)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClick = () => {
|
||||||
|
if (leaveTimer.value) clearTimeout(leaveTimer.value)
|
||||||
|
|
||||||
|
const event = props.item.isDir ? 'navigate' : 'openFile'
|
||||||
|
emit(event, props.item.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
const emitNavigate = (path: string) => emit('navigate', path)
|
||||||
|
const emitOpenFile = (path: string) => emit('openFile', path)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dropdown-item {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: var(--border-radius-small);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.1s;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--color-text-1);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-name {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-arrow {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-3);
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:hover {
|
||||||
|
background: var(--color-fill-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 子级菜单 */
|
||||||
|
.sub-dropdown {
|
||||||
|
position: fixed;
|
||||||
|
min-width: 200px;
|
||||||
|
max-width: 280px;
|
||||||
|
max-height: 320px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: var(--color-bg-popup);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--border-radius-small);
|
||||||
|
box-shadow: var(--shadow2-dropdown);
|
||||||
|
z-index: calc(1000 + var(--level, 0));
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-loading,
|
||||||
|
.dropdown-error,
|
||||||
|
.dropdown-empty {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
color: var(--color-text-3);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 动画 */
|
||||||
|
.dropdown-fade-enter-active,
|
||||||
|
.dropdown-fade-leave-active {
|
||||||
|
transition: opacity 0.15s, transform 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-fade-enter-from,
|
||||||
|
.dropdown-fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滚动条 */
|
||||||
|
.sub-dropdown::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-dropdown::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-dropdown::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--color-fill-3);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-dropdown::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--color-fill-4);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -129,7 +129,7 @@
|
|||||||
|
|
||||||
<!-- 编辑模式 -->
|
<!-- 编辑模式 -->
|
||||||
<div v-else class="html-edit-wrapper">
|
<div v-else class="html-edit-wrapper">
|
||||||
<CodeEditor
|
<AsyncCodeEditor
|
||||||
:model-value="config.fileContent"
|
:model-value="config.fileContent"
|
||||||
:file-extension="config.currentFileExtension"
|
:file-extension="config.currentFileExtension"
|
||||||
@update:model-value="handleContentUpdate"
|
@update:model-value="handleContentUpdate"
|
||||||
@@ -185,7 +185,7 @@
|
|||||||
|
|
||||||
<!-- 编辑模式 -->
|
<!-- 编辑模式 -->
|
||||||
<div v-else class="markdown-edit-wrapper">
|
<div v-else class="markdown-edit-wrapper">
|
||||||
<CodeEditor
|
<AsyncCodeEditor
|
||||||
:model-value="config.fileContent"
|
:model-value="config.fileContent"
|
||||||
:file-extension="config.currentFileExtension"
|
:file-extension="config.currentFileExtension"
|
||||||
@update:model-value="handleContentUpdate"
|
@update:model-value="handleContentUpdate"
|
||||||
@@ -238,7 +238,7 @@
|
|||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CodeEditor
|
<AsyncCodeEditor
|
||||||
:model-value="config.fileContent"
|
:model-value="config.fileContent"
|
||||||
:file-extension="config.currentFileExtension"
|
:file-extension="config.currentFileExtension"
|
||||||
@update:model-value="handleContentUpdate"
|
@update:model-value="handleContentUpdate"
|
||||||
@@ -254,14 +254,20 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, watch, nextTick } from 'vue'
|
import { computed, watch, nextTick, defineAsyncComponent } from 'vue'
|
||||||
import { Message } from '@arco-design/web-vue'
|
import { Message } from '@arco-design/web-vue'
|
||||||
import { IconSave, IconEdit, IconEye, IconUndo, IconCopy } from '@arco-design/web-vue/es/icon'
|
import { IconSave, IconEdit, IconEye, IconUndo, IconCopy } from '@arco-design/web-vue/es/icon'
|
||||||
import CodeEditor from '@/components/CodeEditor.vue'
|
|
||||||
import { getFileName } from '@/utils/fileUtils'
|
import { getFileName } from '@/utils/fileUtils'
|
||||||
import type { FileEditorPanelConfig } from '@/types/file-system'
|
import type { FileEditorPanelConfig } from '@/types/file-system'
|
||||||
import { renderMermaidDiagrams } from '@/utils/markedExtensions'
|
import { renderMermaidDiagrams } from '@/utils/markedExtensions'
|
||||||
|
|
||||||
|
// 异步加载 CodeEditor 组件,减少初始包大小
|
||||||
|
const AsyncCodeEditor = defineAsyncComponent({
|
||||||
|
loader: () => import('@/components/CodeEditor.vue'),
|
||||||
|
delay: 200,
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
interface Props {
|
interface Props {
|
||||||
config: FileEditorPanelConfig
|
config: FileEditorPanelConfig
|
||||||
|
|||||||
300
web/src/components/FileSystem/components/PathBreadcrumb.vue
Normal file
300
web/src/components/FileSystem/components/PathBreadcrumb.vue
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
<template>
|
||||||
|
<div class="path-breadcrumb">
|
||||||
|
<div class="breadcrumb-items">
|
||||||
|
<template v-for="(segment, index) in segments" :key="index">
|
||||||
|
<!-- 路径段 -->
|
||||||
|
<div
|
||||||
|
class="breadcrumb-segment"
|
||||||
|
:class="{ 'is-hoverable': index < segments.length - 1 }"
|
||||||
|
@mouseenter="onHover(segment, index)"
|
||||||
|
@mouseleave="onLeave"
|
||||||
|
@click="onClick(segment)"
|
||||||
|
>
|
||||||
|
<span class="segment-text">{{ segment.name }}</span>
|
||||||
|
|
||||||
|
<!-- 悬停弹出菜单 -->
|
||||||
|
<Transition name="dropdown-fade">
|
||||||
|
<div
|
||||||
|
v-if="activeIndex === index"
|
||||||
|
class="siblings-dropdown main-dropdown"
|
||||||
|
@mouseenter="onMenuEnter"
|
||||||
|
@mouseleave="onMenuLeave"
|
||||||
|
>
|
||||||
|
<div v-if="loading" class="dropdown-loading">
|
||||||
|
<a-spin :size="16" />
|
||||||
|
<span>加载中...</span>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="error" class="dropdown-error">
|
||||||
|
<icon-exclamation-circle />
|
||||||
|
<span>{{ error }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="!children.length" class="dropdown-empty">
|
||||||
|
<icon-folder />
|
||||||
|
<span>空文件夹</span>
|
||||||
|
</div>
|
||||||
|
<template v-else>
|
||||||
|
<DropdownItem
|
||||||
|
v-for="child in children"
|
||||||
|
:key="child.path"
|
||||||
|
:item="child"
|
||||||
|
:level="1"
|
||||||
|
@navigate="onNavigate"
|
||||||
|
@openFile="onOpenFile"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分隔符 -->
|
||||||
|
<icon-right v-if="index < segments.length - 1" class="breadcrumb-separator" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch, provide, type Ref } from 'vue'
|
||||||
|
import { IconRight, IconFolder, IconFile, IconExclamationCircle } from '@arco-design/web-vue/es/icon'
|
||||||
|
import { listDir } from '@/api/system'
|
||||||
|
import { getParentPath, joinPath, normalizePathSeparators } from '@/utils/pathHelpers'
|
||||||
|
import { sortFileList } from '@/utils/fileUtils'
|
||||||
|
import { useTimeout } from '@/composables/useTimeout'
|
||||||
|
import DropdownItem from './DropdownItem.vue'
|
||||||
|
|
||||||
|
const { setTimeout: delay, clearTimeout } = useTimeout()
|
||||||
|
|
||||||
|
const openMenus = ref<Map<number, string>>(new Map())
|
||||||
|
|
||||||
|
const closeMenu = (level: number) => {
|
||||||
|
const newMap = new Map(openMenus.value)
|
||||||
|
newMap.delete(level)
|
||||||
|
openMenus.value = newMap
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeAllMenus = () => {
|
||||||
|
openMenus.value = new Map()
|
||||||
|
}
|
||||||
|
|
||||||
|
provide('openMenus', openMenus)
|
||||||
|
provide('closeMenu', closeMenu)
|
||||||
|
provide('closeAllMenus', closeAllMenus)
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
path: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'navigate', path: string): void
|
||||||
|
(e: 'openFile', path: string): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>()
|
||||||
|
|
||||||
|
interface PathSegment {
|
||||||
|
name: string
|
||||||
|
path: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const segments = computed<PathSegment[]>(() => {
|
||||||
|
if (!props.path) return []
|
||||||
|
|
||||||
|
const normalizedPath = props.path.replace(/\\/g, '/')
|
||||||
|
|
||||||
|
if (/^[A-Za-z]:\/?$/.test(normalizedPath)) {
|
||||||
|
const driveLetter = normalizedPath.charAt(0) + ':'
|
||||||
|
return [{ name: driveLetter, path: driveLetter + '/' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = normalizedPath.split('/').filter(p => p)
|
||||||
|
let currentPath = ''
|
||||||
|
|
||||||
|
return parts.map((part, index) => {
|
||||||
|
if (index === 0 && part.endsWith(':')) {
|
||||||
|
currentPath = part + '/'
|
||||||
|
} else {
|
||||||
|
currentPath += '/' + part
|
||||||
|
}
|
||||||
|
return { name: part, path: currentPath }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const activeIndex = ref<number | null>(null)
|
||||||
|
const closeTimer = ref<NodeJS.Timeout | null>(null)
|
||||||
|
const children = ref<Array<{ name: string; path: string; isDir: boolean }>>([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref('')
|
||||||
|
|
||||||
|
const loadChildren = async (path: string) => {
|
||||||
|
loading.value = true
|
||||||
|
error.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const files = await listDir(path)
|
||||||
|
children.value = sortFileList(files.map(f => ({
|
||||||
|
name: f.name,
|
||||||
|
path: f.path,
|
||||||
|
isDir: f.isDir
|
||||||
|
})))
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Breadcrumb] 加载子目录失败:', err)
|
||||||
|
error.value = '加载失败'
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetAndClose = () => {
|
||||||
|
activeIndex.value = null
|
||||||
|
closeAllMenus()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onHover = (segment: PathSegment, index: number) => {
|
||||||
|
if (index === segments.value.length - 1) return
|
||||||
|
|
||||||
|
delay(() => {
|
||||||
|
activeIndex.value = index
|
||||||
|
loadChildren(segment.path)
|
||||||
|
}, 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMenuEnter = () => {
|
||||||
|
if (closeTimer.value) clearTimeout(closeTimer.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMenuLeave = () => {
|
||||||
|
closeTimer.value = delay(() => {
|
||||||
|
resetAndClose()
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClick = (segment: PathSegment) => {
|
||||||
|
emit('navigate', segment.path)
|
||||||
|
resetAndClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onNavigate = (path: string) => {
|
||||||
|
emit('navigate', path)
|
||||||
|
resetAndClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onOpenFile = (path: string) => {
|
||||||
|
emit('openFile', path)
|
||||||
|
resetAndClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.path, () => {
|
||||||
|
activeIndex.value = null
|
||||||
|
children.value = []
|
||||||
|
openMenus.value = new Map()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.path-breadcrumb {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-items {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-segment {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: default;
|
||||||
|
transition: background 0.2s;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-segment.is-hoverable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-segment.is-hoverable:hover {
|
||||||
|
background: var(--color-fill-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.segment-text {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--color-text-1);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-separator {
|
||||||
|
color: var(--color-text-3);
|
||||||
|
font-size: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin: 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 弹出菜单 */
|
||||||
|
.siblings-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 4px);
|
||||||
|
left: 0;
|
||||||
|
min-width: 200px;
|
||||||
|
max-width: 280px;
|
||||||
|
max-height: 320px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: var(--color-bg-popup);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--border-radius-small);
|
||||||
|
box-shadow: var(--shadow2-dropdown);
|
||||||
|
z-index: 1000;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-loading,
|
||||||
|
.dropdown-error,
|
||||||
|
.dropdown-empty {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
color: var(--color-text-3);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 动画 */
|
||||||
|
.dropdown-fade-enter-active,
|
||||||
|
.dropdown-fade-leave-active {
|
||||||
|
transition: opacity 0.15s, transform 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-fade-enter-from,
|
||||||
|
.dropdown-fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滚动条样式 */
|
||||||
|
.siblings-dropdown::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.siblings-dropdown::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.siblings-dropdown::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--color-fill-3);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.siblings-dropdown::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--color-fill-4);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
@drop="handleDrop($event, index)"
|
@drop="handleDrop($event, index)"
|
||||||
@dragend="handleDragEnd"
|
@dragend="handleDragEnd"
|
||||||
>
|
>
|
||||||
<span class="sidebar-item-icon">{{ fav.is_dir ? '📁' : '📄' }}</span>
|
<span class="sidebar-item-icon">{{ fav.isDir ? '📁' : '📄' }}</span>
|
||||||
<span class="sidebar-item-name" :title="fav.name">{{ fav.name }}</span>
|
<span class="sidebar-item-name" :title="fav.name">{{ fav.name }}</span>
|
||||||
<a-button
|
<a-button
|
||||||
type="text"
|
type="text"
|
||||||
|
|||||||
@@ -25,25 +25,19 @@
|
|||||||
退出 ZIP
|
退出 ZIP
|
||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
<!-- 正常模式:路径输入 -->
|
<!-- 正常模式:面包屑导航 -->
|
||||||
<a-auto-complete
|
<div v-else class="path-breadcrumb-wrapper">
|
||||||
v-else
|
<PathBreadcrumb
|
||||||
:model-value="normalizedPath"
|
:path="config.filePath"
|
||||||
:data="normalizedPathHistory"
|
@navigate="handleGoToPath"
|
||||||
placeholder="输入路径 (如: C:/Users)"
|
@openFile="handleOpenFile"
|
||||||
class="path-input"
|
/>
|
||||||
@select="handlePathSelect"
|
<a-tooltip content="复制路径" position="top">
|
||||||
@pressEnter="handlePathSelect"
|
<div class="copy-icon-wrapper" @click="handleCopyPath">
|
||||||
@update:model-value="handlePathUpdate"
|
<icon-copy />
|
||||||
>
|
</div>
|
||||||
<template #append>
|
</a-tooltip>
|
||||||
<a-tooltip content="复制路径" position="top">
|
</div>
|
||||||
<div class="copy-icon-wrapper" @click="handleCopyPath">
|
|
||||||
<icon-copy />
|
|
||||||
</div>
|
|
||||||
</a-tooltip>
|
|
||||||
</template>
|
|
||||||
</a-auto-complete>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -119,6 +113,7 @@
|
|||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { IconForward, IconHistory, IconRefresh, IconMenu, IconClose, IconRight, IconCopy } from '@arco-design/web-vue/es/icon'
|
import { IconForward, IconHistory, IconRefresh, IconMenu, IconClose, IconRight, IconCopy } from '@arco-design/web-vue/es/icon'
|
||||||
import type { ToolbarConfig } from '@/types/file-system'
|
import type { ToolbarConfig } from '@/types/file-system'
|
||||||
|
import PathBreadcrumb from './PathBreadcrumb.vue'
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -134,21 +129,13 @@ interface Emits {
|
|||||||
(e: 'refresh'): void
|
(e: 'refresh'): void
|
||||||
(e: 'exitZip'): void
|
(e: 'exitZip'): void
|
||||||
(e: 'goToPath', path: string): void
|
(e: 'goToPath', path: string): void
|
||||||
|
(e: 'openFile', path: string): void
|
||||||
(e: 'navigateToZipDirectory', path: string): void
|
(e: 'navigateToZipDirectory', path: string): void
|
||||||
(e: 'showMessage', message: string, type: 'success' | 'error' | 'warning' | 'info'): void
|
(e: 'showMessage', message: string, type: 'success' | 'error' | 'warning' | 'info'): void
|
||||||
}
|
}
|
||||||
|
|
||||||
const emit = defineEmits<Emits>()
|
const emit = defineEmits<Emits>()
|
||||||
|
|
||||||
// 将反斜杠转换为正斜杠显示
|
|
||||||
const normalizedPath = computed(() => {
|
|
||||||
return props.config.filePath?.replace(/\\/g, '/') || ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const normalizedPathHistory = computed(() => {
|
|
||||||
return props.config.pathHistory.map(path => path.replace(/\\/g, '/'))
|
|
||||||
})
|
|
||||||
|
|
||||||
// 事件处理
|
// 事件处理
|
||||||
const handlePathUpdate = (path: string) => {
|
const handlePathUpdate = (path: string) => {
|
||||||
emit('update:filePath', path)
|
emit('update:filePath', path)
|
||||||
@@ -162,6 +149,10 @@ const handleGoToPath = (path: string) => {
|
|||||||
emit('goToPath', path)
|
emit('goToPath', path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleOpenFile = (path: string) => {
|
||||||
|
emit('openFile', path)
|
||||||
|
}
|
||||||
|
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
emit('refresh')
|
emit('refresh')
|
||||||
}
|
}
|
||||||
@@ -235,22 +226,34 @@ const handleCopyPath = async () => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 覆盖 Arco 输入框 append 的默认 padding */
|
.path-breadcrumb-wrapper {
|
||||||
.path-input-wrapper :deep(.arco-input-append) {
|
display: flex;
|
||||||
padding: 0 !important;
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 200px;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: var(--color-fill-1);
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path-breadcrumb-wrapper:hover {
|
||||||
|
border-color: var(--color-border-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.copy-icon-wrapper {
|
.copy-icon-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 0 8px;
|
padding: 4px 8px;
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--color-text-3);
|
color: var(--color-text-3);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
|
border-radius: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.copy-icon-wrapper:hover {
|
.copy-icon-wrapper:hover {
|
||||||
|
|||||||
@@ -25,7 +25,13 @@ export function useFavorites() {
|
|||||||
try {
|
try {
|
||||||
const stored = localStorage.getItem(STORAGE_KEYS.FAVORITE_FILES)
|
const stored = localStorage.getItem(STORAGE_KEYS.FAVORITE_FILES)
|
||||||
if (stored) {
|
if (stored) {
|
||||||
favorites.value = JSON.parse(stored)
|
const loaded = JSON.parse(stored) as FavoriteFile[]
|
||||||
|
|
||||||
|
// 数据迁移:将旧字段 is_dir 转换为 isDir
|
||||||
|
favorites.value = loaded.map(fav => ({
|
||||||
|
...fav,
|
||||||
|
isDir: fav.isDir ?? (fav as any).is_dir ?? false
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载收藏列表失败:', error)
|
console.error('加载收藏列表失败:', error)
|
||||||
@@ -62,10 +68,10 @@ export function useFavorites() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 标准化路径用于比较(处理正斜杠/反斜杠不一致)
|
* 标准化路径用于比较(后端已统一为 /,直接转小写)
|
||||||
*/
|
*/
|
||||||
const normalizePath = (path: string): string => {
|
const normalizePath = (path: string): string => {
|
||||||
return path.replace(/\\/g, '/').toLowerCase()
|
return path.toLowerCase()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ export function useFileEdit(options: UseFileEditOptions = {}) {
|
|||||||
// 文本或代码文件(可编辑)
|
// 文本或代码文件(可编辑)
|
||||||
const isTextFile = FILE_EXTENSIONS.TEXT.includes(ext) ||
|
const isTextFile = FILE_EXTENSIONS.TEXT.includes(ext) ||
|
||||||
FILE_EXTENSIONS.CODE.includes(ext) ||
|
FILE_EXTENSIONS.CODE.includes(ext) ||
|
||||||
['json', 'xml', 'yaml', 'yml', 'toml', 'ini', 'conf', 'cfg', 'props'].includes(ext)
|
FILE_EXTENSIONS.CONFIG.includes(ext)
|
||||||
|
|
||||||
// 如果是媒体文件或文本文件,就不是二进制
|
// 如果是媒体文件或文本文件,就不是二进制
|
||||||
if (isMediaFile || isTextFile) return false
|
if (isMediaFile || isTextFile) return false
|
||||||
|
|||||||
@@ -80,12 +80,13 @@ export function useFileOperations(options: UseFileOperationsOptions = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除路径(文件或目录)
|
* 删除路径(文件或目录),返回被删除的文件信息
|
||||||
*/
|
*/
|
||||||
const deletePath = async (path: string): Promise<void> => {
|
const deletePath = async (path: string): Promise<FileItem> => {
|
||||||
try {
|
try {
|
||||||
await deletePathApi(path)
|
const result = await deletePathApi(path)
|
||||||
onSuccess?.('deletePath', { path })
|
onSuccess?.('deletePath', { path, result })
|
||||||
|
return result as FileItem
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const err = error as Error
|
const err = error as Error
|
||||||
onError?.('deletePath', err)
|
onError?.('deletePath', err)
|
||||||
@@ -94,16 +95,17 @@ export function useFileOperations(options: UseFileOperationsOptions = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建新文件
|
* 创建新文件,返回创建的文件信息
|
||||||
*/
|
*/
|
||||||
const createNewFile = async (
|
const createNewFile = async (
|
||||||
dirPath: string,
|
dirPath: string,
|
||||||
filename: string,
|
filename: string,
|
||||||
content: string = ''
|
content: string = ''
|
||||||
): Promise<void> => {
|
): Promise<FileItem> => {
|
||||||
try {
|
try {
|
||||||
await createFile(dirPath, filename, content)
|
const result = await createFile(dirPath, filename, content)
|
||||||
onSuccess?.('createFile', { dirPath, filename })
|
onSuccess?.('createFile', { dirPath, filename, result })
|
||||||
|
return result as FileItem
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const err = error as Error
|
const err = error as Error
|
||||||
onError?.('createFile', err)
|
onError?.('createFile', err)
|
||||||
@@ -112,12 +114,13 @@ export function useFileOperations(options: UseFileOperationsOptions = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建新目录
|
* 创建新目录,返回创建的目录信息
|
||||||
*/
|
*/
|
||||||
const createNewDir = async (parentPath: string, dirname: string): Promise<void> => {
|
const createNewDir = async (parentPath: string, dirname: string): Promise<FileItem> => {
|
||||||
try {
|
try {
|
||||||
await createDir(parentPath, dirname)
|
const result = await createDir(parentPath, dirname)
|
||||||
onSuccess?.('createDir', { parentPath, dirname })
|
onSuccess?.('createDir', { parentPath, dirname, result })
|
||||||
|
return result as FileItem
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const err = error as Error
|
const err = error as Error
|
||||||
onError?.('createDir', err)
|
onError?.('createDir', err)
|
||||||
@@ -126,9 +129,9 @@ export function useFileOperations(options: UseFileOperationsOptions = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 重命名文件或目录
|
* 重命名文件或目录,返回新文件信息
|
||||||
*/
|
*/
|
||||||
const rename = async (oldPath: string, newName: string): Promise<void> => {
|
const rename = async (oldPath: string, newName: string): Promise<FileItem> => {
|
||||||
// 构造新路径
|
// 构造新路径
|
||||||
const separator = oldPath.includes('\\') ? '\\' : '/'
|
const separator = oldPath.includes('\\') ? '\\' : '/'
|
||||||
const parentPath = oldPath.substring(
|
const parentPath = oldPath.substring(
|
||||||
@@ -138,8 +141,9 @@ export function useFileOperations(options: UseFileOperationsOptions = {}) {
|
|||||||
const newPath = parentPath + separator + newName
|
const newPath = parentPath + separator + newName
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await renamePathApi(oldPath, newPath)
|
const result = await renamePathApi(oldPath, newPath)
|
||||||
onSuccess?.('rename', { oldPath, newPath })
|
onSuccess?.('rename', { oldPath, newPath, result })
|
||||||
|
return result as FileItem
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const err = error as Error
|
const err = error as Error
|
||||||
onError?.('rename', err)
|
onError?.('rename', err)
|
||||||
|
|||||||
@@ -118,6 +118,11 @@ export function useFilePreview(options: UseFilePreviewOptions = {}) {
|
|||||||
return 'Code' as FileType
|
return 'Code' as FileType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 配置文件(返回 Code 类型,因为它们也是可编辑的文本格式)
|
||||||
|
if (FILE_EXTENSIONS.CONFIG.includes(ext)) {
|
||||||
|
return 'Code' as FileType
|
||||||
|
}
|
||||||
|
|
||||||
// 文本
|
// 文本
|
||||||
if (FILE_EXTENSIONS.TEXT.includes(ext)) {
|
if (FILE_EXTENSIONS.TEXT.includes(ext)) {
|
||||||
return 'Text' as FileType
|
return 'Text' as FileType
|
||||||
@@ -227,7 +232,8 @@ export function useFilePreview(options: UseFilePreviewOptions = {}) {
|
|||||||
const ext = filename.split('.').pop()?.toLowerCase() || ''
|
const ext = filename.split('.').pop()?.toLowerCase() || ''
|
||||||
return FILE_EXTENSIONS.CODE.includes(ext) ||
|
return FILE_EXTENSIONS.CODE.includes(ext) ||
|
||||||
FILE_EXTENSIONS.TEXT.includes(ext) ||
|
FILE_EXTENSIONS.TEXT.includes(ext) ||
|
||||||
['html', 'htm', 'md', 'markdown', 'json', 'xml'].includes(ext)
|
FILE_EXTENSIONS.CONFIG.includes(ext) ||
|
||||||
|
['html', 'htm', 'md', 'markdown'].includes(ext)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
import { ref, watch, computed } from 'vue'
|
import { ref, watch, computed } from 'vue'
|
||||||
import { STORAGE_KEYS } from '@/utils/constants'
|
import { STORAGE_KEYS } from '@/utils/constants'
|
||||||
|
import { normalizePathSeparators } from '@/utils/pathHelpers'
|
||||||
import type { PathHistory } from '@/types/file-system'
|
import type { PathHistory } from '@/types/file-system'
|
||||||
|
|
||||||
export interface UsePathNavigationOptions {
|
export interface UsePathNavigationOptions {
|
||||||
@@ -18,6 +19,10 @@ export interface UsePathNavigationOptions {
|
|||||||
const restoreLastPath = (): string | null => {
|
const restoreLastPath = (): string | null => {
|
||||||
try {
|
try {
|
||||||
const lastPath = localStorage.getItem(STORAGE_KEYS.FILESYSTEM.FILE_PATH)
|
const lastPath = localStorage.getItem(STORAGE_KEYS.FILESYSTEM.FILE_PATH)
|
||||||
|
if (lastPath) {
|
||||||
|
// 规范化旧路径(可能包含反斜杠)
|
||||||
|
return normalizePathSeparators(lastPath)
|
||||||
|
}
|
||||||
return lastPath
|
return lastPath
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('恢复路径失败:', error)
|
console.error('恢复路径失败:', error)
|
||||||
@@ -56,8 +61,8 @@ export function usePathNavigation(options: UsePathNavigationOptions = {}) {
|
|||||||
if (!path || path === filePath.value) return
|
if (!path || path === filePath.value) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 路径规范化
|
// 路径规范化(处理反斜杠并统一为正斜杠)
|
||||||
const normalizedPath = normalizePath(path)
|
const normalizedPath = normalizePathSeparators(path)
|
||||||
filePath.value = normalizedPath
|
filePath.value = normalizedPath
|
||||||
|
|
||||||
// 添加到历史记录
|
// 添加到历史记录
|
||||||
@@ -177,11 +182,10 @@ export function usePathNavigation(options: UsePathNavigationOptions = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 路径规范化(统一分隔符)
|
* 路径规范化(统一为正斜杠)
|
||||||
*/
|
*/
|
||||||
const normalizePath = (path: string): string => {
|
const normalizePath = (path: string): string => {
|
||||||
if (!path) return ''
|
return normalizePathSeparators(path)
|
||||||
return path.replace(/\\/g, '/')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,200 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="file-system-container">
|
|
||||||
<div class="debug-info">
|
|
||||||
<h3>FileSystem Debug Info</h3>
|
|
||||||
<p>filePath: {{ filePath }}</p>
|
|
||||||
<p>fileList length: {{ fileList.length }}</p>
|
|
||||||
<p>showSidebar: {{ showSidebar }}</p>
|
|
||||||
<p>hasSelectedFile: {{ hasSelectedFile }}</p>
|
|
||||||
<button @click="testClick">测试点击</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 顶部工具栏 -->
|
|
||||||
<Toolbar
|
|
||||||
:config="toolbarConfig"
|
|
||||||
@update:file-path="handleFilePathUpdate"
|
|
||||||
@update:show-sidebar="handleSidebarToggle"
|
|
||||||
@refresh="handleRefresh"
|
|
||||||
@exit-zip="handleExitZip"
|
|
||||||
@go-to-path="handleGoToPath"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, computed, onMounted } from 'vue'
|
|
||||||
import { Message } from '@arco-design/web-vue'
|
|
||||||
import { sortFileList } from '@/utils/fileUtils'
|
|
||||||
|
|
||||||
// 导入子组件
|
|
||||||
import Toolbar from './components/Toolbar.vue'
|
|
||||||
|
|
||||||
// 导入 Composables
|
|
||||||
import { useFileOperations } from './composables/useFileOperations'
|
|
||||||
import { useFavorites } from './composables/useFavorites'
|
|
||||||
import { usePathNavigation } from './composables/usePathNavigation'
|
|
||||||
|
|
||||||
// 定义组件名称
|
|
||||||
defineOptions({
|
|
||||||
name: 'FileSystem'
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log('FileSystem component setup started')
|
|
||||||
|
|
||||||
// ========== 状态管理 ==========
|
|
||||||
|
|
||||||
const fileList = ref([])
|
|
||||||
const fileLoading = ref(false)
|
|
||||||
const selectedFileItem = ref(null)
|
|
||||||
|
|
||||||
const showSidebar = ref(true)
|
|
||||||
const panelWidth = ref({ left: 50, right: 50 })
|
|
||||||
|
|
||||||
// ========== Composables 初始化 ==========
|
|
||||||
|
|
||||||
// 文件操作
|
|
||||||
const { listDirectory, readFile } = useFileOperations({
|
|
||||||
onSuccess: (operation, data) => {
|
|
||||||
console.log('Operation success:', operation, data)
|
|
||||||
},
|
|
||||||
onError: (operation, error) => {
|
|
||||||
console.error('Operation error:', operation, error)
|
|
||||||
Message.error(`${operation} 失败: ${error.message}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 收藏夹
|
|
||||||
const { favorites, draggingState } = useFavorites()
|
|
||||||
|
|
||||||
// 路径导航
|
|
||||||
const { filePath, history, navigate, onPathSelect, onPathEnter, browseDirectory } =
|
|
||||||
usePathNavigation({
|
|
||||||
onListDirectory: async (path) => {
|
|
||||||
await loadDirectory(path)
|
|
||||||
},
|
|
||||||
initialPath: 'C:\\'
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log('Composables initialized')
|
|
||||||
console.log('Initial filePath:', filePath.value)
|
|
||||||
|
|
||||||
// ========== 计算属性 ==========
|
|
||||||
|
|
||||||
const hasSelectedFile = computed(() => selectedFileItem.value !== null)
|
|
||||||
|
|
||||||
const toolbarConfig = computed(() => ({
|
|
||||||
filePath: filePath.value || '',
|
|
||||||
pathHistory: history.value?.paths?.slice(-10) || [],
|
|
||||||
commonPaths: [
|
|
||||||
{ name: '📁 桌面', path: 'C:\\Users\\Public\\Desktop' },
|
|
||||||
{ name: '📁 文档', path: 'C:\\Users\\Public\\Documents' },
|
|
||||||
{ name: '📁 下载', path: 'C:\\Users\\Public\\Downloads' }
|
|
||||||
],
|
|
||||||
isBrowsingZip: false,
|
|
||||||
displayPath: filePath.value || '',
|
|
||||||
fileLoading: fileLoading.value,
|
|
||||||
showSidebar: showSidebar.value
|
|
||||||
}))
|
|
||||||
|
|
||||||
// ========== 事件处理 ==========
|
|
||||||
|
|
||||||
const handleFilePathUpdate = (path: string) => {
|
|
||||||
console.log('handleFilePathUpdate:', path)
|
|
||||||
filePath.value = path
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSidebarToggle = (show: boolean) => {
|
|
||||||
console.log('handleSidebarToggle:', show)
|
|
||||||
showSidebar.value = show
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
|
||||||
console.log('handleRefresh')
|
|
||||||
await loadDirectory(filePath.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleExitZip = () => {
|
|
||||||
console.log('handleExitZip')
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleGoToPath = async (path: string) => {
|
|
||||||
console.log('handleGoToPath:', path)
|
|
||||||
await navigate(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
const testClick = () => {
|
|
||||||
console.log('Test button clicked')
|
|
||||||
Message.success('测试成功!')
|
|
||||||
console.log('Current state:', {
|
|
||||||
filePath: filePath.value,
|
|
||||||
fileList: fileList.value,
|
|
||||||
favorites: favorites.value
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== 工具函数 ==========
|
|
||||||
|
|
||||||
const loadDirectory = async (path: string) => {
|
|
||||||
console.log('loadDirectory:', path)
|
|
||||||
fileLoading.value = true
|
|
||||||
try {
|
|
||||||
fileList.value = await listDirectory(path)
|
|
||||||
fileList.value = sortFileList(fileList.value)
|
|
||||||
console.log('Files loaded:', fileList.value.length)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Load directory error:', error)
|
|
||||||
Message.error(`加载目录失败: ${error}`)
|
|
||||||
} finally {
|
|
||||||
fileLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== 生命周期 ==========
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
console.log('FileSystem mounted')
|
|
||||||
console.log('Loading initial directory:', filePath.value)
|
|
||||||
|
|
||||||
// 加载默认目录
|
|
||||||
loadDirectory(filePath.value)
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.file-system-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.debug-info {
|
|
||||||
padding: 20px;
|
|
||||||
background: #f0f0f0;
|
|
||||||
margin: 10px;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.debug-info h3 {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.debug-info p {
|
|
||||||
margin: 5px 0;
|
|
||||||
font-family: 'Consolas', monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.debug-info button {
|
|
||||||
margin-top: 10px;
|
|
||||||
padding: 8px 16px;
|
|
||||||
background: #1890ff;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.debug-info button:hover {
|
|
||||||
background: #40a9ff;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
@refresh="handleRefresh"
|
@refresh="handleRefresh"
|
||||||
@exit-zip="handleExitZip"
|
@exit-zip="handleExitZip"
|
||||||
@go-to-path="handleGoToPath"
|
@go-to-path="handleGoToPath"
|
||||||
|
@open-file="handleOpenFile"
|
||||||
@navigate-to-zip-directory="handleNavigateToZipDirectory"
|
@navigate-to-zip-directory="handleNavigateToZipDirectory"
|
||||||
@show-message="handleShowMessage"
|
@show-message="handleShowMessage"
|
||||||
/>
|
/>
|
||||||
@@ -118,7 +119,9 @@ import { useCommonPaths } from './composables/useCommonPaths'
|
|||||||
|
|
||||||
// 导入工具函数
|
// 导入工具函数
|
||||||
import { getFileName, sortFileList } from '@/utils/fileUtils'
|
import { getFileName, sortFileList } from '@/utils/fileUtils'
|
||||||
|
import { getParentPath } from '@/utils/pathHelpers'
|
||||||
import { isImageFile, isVideoFile, isAudioFile, isPdfFile, isHtmlFile, isMarkdownFile } from '@/utils/fileTypeHelpers'
|
import { isImageFile, isVideoFile, isAudioFile, isPdfFile, isHtmlFile, isMarkdownFile } from '@/utils/fileTypeHelpers'
|
||||||
|
import { listDir } from '@/api/system'
|
||||||
import { STORAGE_KEYS, DEFAULTS, UI_TEXT, VALIDATION_RULES, FILE_EXTENSIONS } from '@/utils/constants'
|
import { STORAGE_KEYS, DEFAULTS, UI_TEXT, VALIDATION_RULES, FILE_EXTENSIONS } from '@/utils/constants'
|
||||||
|
|
||||||
// 导入类型
|
// 导入类型
|
||||||
@@ -345,6 +348,35 @@ const handleGoToPath = async (path: string) => {
|
|||||||
await navigate(path)
|
await navigate(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleOpenFile = async (path: string) => {
|
||||||
|
// 检查是文件还是目录
|
||||||
|
try {
|
||||||
|
const parentPath = getParentPath(path)
|
||||||
|
const files = await listDir(parentPath)
|
||||||
|
|
||||||
|
// 后端已统一返回 / 路径,直接比较
|
||||||
|
const targetFile = files.find(f => f.path === path)
|
||||||
|
|
||||||
|
if (targetFile) {
|
||||||
|
if (targetFile.isDir) {
|
||||||
|
// 是目录,导航进入
|
||||||
|
await navigate(path)
|
||||||
|
} else {
|
||||||
|
// 是文件,选中并加载
|
||||||
|
selectedFileItem.value = targetFile
|
||||||
|
await loadFileContent(path)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 未找到,尝试直接导航(可能是目录)
|
||||||
|
await navigate(path)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('打开文件失败:', error)
|
||||||
|
// 如果出错,尝试直接导航
|
||||||
|
await navigate(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleNavigateToZipDirectory = async (path: string) => {
|
const handleNavigateToZipDirectory = async (path: string) => {
|
||||||
// 暂时不处理 ZIP
|
// 暂时不处理 ZIP
|
||||||
}
|
}
|
||||||
@@ -359,7 +391,7 @@ const handleShowMessage = (message: string, type: 'success' | 'error' | 'warning
|
|||||||
|
|
||||||
// 侧边栏事件
|
// 侧边栏事件
|
||||||
const handleOpenFavorite = async (file: FavoriteFile) => {
|
const handleOpenFavorite = async (file: FavoriteFile) => {
|
||||||
if (file.is_dir) {
|
if (file.isDir) {
|
||||||
await navigate(file.path)
|
await navigate(file.path)
|
||||||
} else {
|
} else {
|
||||||
await selectFile(file.path)
|
await selectFile(file.path)
|
||||||
@@ -416,7 +448,7 @@ const handleFileClick = async (file: FileItem) => {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// 正常文件系统浏览
|
// 正常文件系统浏览
|
||||||
if (file.is_dir) {
|
if (file.isDir) {
|
||||||
// 目录:使用 navigate 函数,确保历史记录正确更新
|
// 目录:使用 navigate 函数,确保历史记录正确更新
|
||||||
await navigate(file.path)
|
await navigate(file.path)
|
||||||
} else {
|
} else {
|
||||||
@@ -427,7 +459,7 @@ const handleFileClick = async (file: FileItem) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleFileDoubleClick = async (file: FileItem) => {
|
const handleFileDoubleClick = async (file: FileItem) => {
|
||||||
if (file.is_dir) {
|
if (file.isDir) {
|
||||||
await navigate(file.path)
|
await navigate(file.path)
|
||||||
} else {
|
} else {
|
||||||
// 检查是否为 ZIP 文件 - 暂时禁用
|
// 检查是否为 ZIP 文件 - 暂时禁用
|
||||||
@@ -532,54 +564,67 @@ const handleSaveEditing = async (oldPath: string, newName: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fileOps.rename(oldPath, trimmedName)
|
// 如果重命名的是当前打开的文件,先关闭编辑器和预览
|
||||||
|
|
||||||
// 如果重命名的是当前打开的文件,更新其路径
|
|
||||||
if (selectedFileItem.value?.path === oldPath) {
|
if (selectedFileItem.value?.path === oldPath) {
|
||||||
selectedFileItem.value = {
|
// 如果是文件(不是文件夹),才需要关闭编辑器
|
||||||
...selectedFileItem.value,
|
if (!selectedFileItem.value.isDir) {
|
||||||
path: newPath,
|
// 清空编辑器内容
|
||||||
name: trimmedName
|
await clearContent()
|
||||||
|
|
||||||
|
// 清空预览URL
|
||||||
|
if (previewUrl.value) {
|
||||||
|
previewUrl.value = ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 取消选中状态
|
||||||
|
selectedFileItem.value = null
|
||||||
|
|
||||||
|
// 等待文件句柄释放(文件需要更长时间)
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 300))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 刷新文件列表(重命名成功后必须刷新)
|
const renamedFile = await fileOps.rename(oldPath, trimmedName)
|
||||||
await loadDirectory(filePath.value)
|
|
||||||
|
// 更新文件列表(保留收藏状态)
|
||||||
|
updateFileInList(oldPath, renamedFile)
|
||||||
|
|
||||||
// 如果重命名的是收藏的文件,更新收藏夹中的路径
|
// 如果重命名的是收藏的文件,更新收藏夹中的路径
|
||||||
// 注意:必须在刷新文件列表后才能找到新文件
|
|
||||||
if (isFavorite(oldPath)) {
|
if (isFavorite(oldPath)) {
|
||||||
// 移除旧路径
|
|
||||||
removeFav(oldPath)
|
removeFav(oldPath)
|
||||||
// 添加新路径(保持收藏状态)
|
toggleFav(renamedFile)
|
||||||
const newFile = fileList.value.find(f => f.path === newPath)
|
|
||||||
if (newFile) {
|
|
||||||
toggleFav(newFile)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Message.success(`✓ 重命名成功: ${trimmedName}`)
|
Message.success(`✓ 重命名成功: ${trimmedName}`)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// 解析并清理错误消息
|
// 提取错误信息
|
||||||
let errorMsg = error?.message || error?.toString() || '未知错误'
|
let errorMsg = error?.message || error?.toString() || '未知错误'
|
||||||
|
|
||||||
// 清理后端返回的错误消息(去除命令和路径部分)
|
// 清理后端返回的错误前缀
|
||||||
// 格式:rename oldPath newPath: actual error message
|
|
||||||
if (errorMsg.includes(': ')) {
|
|
||||||
const parts = errorMsg.split(': ')
|
|
||||||
if (parts.length > 1) {
|
|
||||||
// 取最后一部分作为真正的错误信息
|
|
||||||
errorMsg = parts.slice(1).join(': ')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清理常见的错误前缀
|
|
||||||
errorMsg = errorMsg
|
errorMsg = errorMsg
|
||||||
.replace(/^rename\s+.*?:\s*/i, '') // 移除 "rename path: " 前缀
|
.replace(/^rename\s+.*?:\s*/i, '')
|
||||||
.replace(/^create\s+.*?:\s*/i, '') // 移除 "create path: " 前缀
|
.replace(/^create\s+.*?:\s*/i, '')
|
||||||
|
.replace(/^delete\s+.*?:\s*/i, '')
|
||||||
.trim()
|
.trim()
|
||||||
|
|
||||||
|
// 针对常见错误提供友好提示
|
||||||
|
if (errorMsg.includes('being used by another process') ||
|
||||||
|
errorMsg.includes('being used by another process') ||
|
||||||
|
errorMsg.includes('被另一个进程占用')) {
|
||||||
|
errorMsg = '文件正在被其他程序使用,请先关闭该文件后重试'
|
||||||
|
if (selectedFileItem.value?.isDir) {
|
||||||
|
errorMsg = '文件夹正在被其他程序使用(如文件管理器、终端等),请先关闭后重试'
|
||||||
|
}
|
||||||
|
} else if (errorMsg.includes('access is denied') ||
|
||||||
|
errorMsg.includes('permission denied')) {
|
||||||
|
errorMsg = '权限不足,无法重命名该文件'
|
||||||
|
} else if (errorMsg.includes('no such file') ||
|
||||||
|
errorMsg.includes('cannot find')) {
|
||||||
|
errorMsg = '文件不存在,可能已被删除或移动'
|
||||||
|
}
|
||||||
|
|
||||||
Message.error(`重命名失败: ${errorMsg}`)
|
Message.error(`重命名失败: ${errorMsg}`)
|
||||||
|
|
||||||
// 失败时恢复编辑状态
|
// 失败时恢复编辑状态
|
||||||
editingFilePath.value = oldPath
|
editingFilePath.value = oldPath
|
||||||
editingFileName.value = oldName
|
editingFileName.value = oldName
|
||||||
@@ -715,7 +760,7 @@ const handleCreateFile = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否已存在同名文件
|
// 检查是否已存在同名文件
|
||||||
const existingFile = fileList.value.find(f => f.name === fileName && !f.is_dir)
|
const existingFile = fileList.value.find(f => f.name === fileName && !f.isDir)
|
||||||
if (existingFile) {
|
if (existingFile) {
|
||||||
Message.error(`文件 "${fileName}" 已存在`)
|
Message.error(`文件 "${fileName}" 已存在`)
|
||||||
// 重新显示对话框
|
// 重新显示对话框
|
||||||
@@ -730,9 +775,9 @@ const handleCreateFile = async () => {
|
|||||||
const fullPath = `${filePath.value}\\${fileName}`
|
const fullPath = `${filePath.value}\\${fileName}`
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fileOps.createNewFile(fullPath)
|
const newFile = await fileOps.createNewFile(fullPath)
|
||||||
Message.success(`✓ 文件 "${fileName}" 创建成功`)
|
Message.success(`✓ 文件 "${fileName}" 创建成功`)
|
||||||
await loadDirectory(filePath.value)
|
addFileToList(newFile)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
Message.error(`创建文件失败: ${error.message || error}`)
|
Message.error(`创建文件失败: ${error.message || error}`)
|
||||||
}
|
}
|
||||||
@@ -771,7 +816,7 @@ const handleCreateDir = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否已存在同名文件夹
|
// 检查是否已存在同名文件夹
|
||||||
const existingFolder = fileList.value.find(f => f.name === folderName && f.is_dir)
|
const existingFolder = fileList.value.find(f => f.name === folderName && f.isDir)
|
||||||
if (existingFolder) {
|
if (existingFolder) {
|
||||||
Message.error(`文件夹 "${folderName}" 已存在`)
|
Message.error(`文件夹 "${folderName}" 已存在`)
|
||||||
// 重新显示对话框
|
// 重新显示对话框
|
||||||
@@ -786,9 +831,9 @@ const handleCreateDir = async () => {
|
|||||||
const fullPath = `${filePath.value}\\${folderName}`
|
const fullPath = `${filePath.value}\\${folderName}`
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fileOps.createNewDir(fullPath)
|
const newDir = await fileOps.createNewDir(fullPath)
|
||||||
Message.success(`✓ 文件夹 "${folderName}" 创建成功`)
|
Message.success(`✓ 文件夹 "${folderName}" 创建成功`)
|
||||||
await loadDirectory(filePath.value)
|
addFileToList(newDir)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
Message.error(`创建文件夹失败: ${error.message || error}`)
|
Message.error(`创建文件夹失败: ${error.message || error}`)
|
||||||
}
|
}
|
||||||
@@ -809,7 +854,7 @@ const validateFileName = (name: string): boolean => {
|
|||||||
*/
|
*/
|
||||||
const handleDeleteFile = async (file: FileItem) => {
|
const handleDeleteFile = async (file: FileItem) => {
|
||||||
const targetPath = file.path
|
const targetPath = file.path
|
||||||
const isDirectory = file.is_dir
|
const isDirectory = file.isDir
|
||||||
const fileName = file.name || targetPath
|
const fileName = file.name || targetPath
|
||||||
|
|
||||||
// 根据类型显示不同的确认信息
|
// 根据类型显示不同的确认信息
|
||||||
@@ -827,6 +872,9 @@ const handleDeleteFile = async (file: FileItem) => {
|
|||||||
await fileOps.deletePath(targetPath)
|
await fileOps.deletePath(targetPath)
|
||||||
Message.success('删除成功')
|
Message.success('删除成功')
|
||||||
|
|
||||||
|
// 从文件列表中移除
|
||||||
|
removeFileFromList(targetPath)
|
||||||
|
|
||||||
// 如果删除的是收藏的文件,从收藏夹中移除
|
// 如果删除的是收藏的文件,从收藏夹中移除
|
||||||
if (isFavorite(targetPath)) {
|
if (isFavorite(targetPath)) {
|
||||||
removeFav(targetPath)
|
removeFav(targetPath)
|
||||||
@@ -836,9 +884,6 @@ const handleDeleteFile = async (file: FileItem) => {
|
|||||||
if (selectedFileItem.value?.path === targetPath) {
|
if (selectedFileItem.value?.path === targetPath) {
|
||||||
selectedFileItem.value = null
|
selectedFileItem.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// 刷新文件列表
|
|
||||||
await loadDirectory(filePath.value)
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
Message.error(`删除失败: ${error.message || error}`)
|
Message.error(`删除失败: ${error.message || error}`)
|
||||||
}
|
}
|
||||||
@@ -906,12 +951,12 @@ const isMediaPreviewable = (filename: string): boolean => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const selectFile = async (path: string) => {
|
const selectFile = async (path: string) => {
|
||||||
// 标准化路径进行比较(处理正斜杠/反斜杠不一致的问题)
|
// 后端已统一返回 / 路径,直接比较
|
||||||
const normalizedPath = path.replace(/\\/g, '/').toLowerCase()
|
const normalizedPath = path.toLowerCase()
|
||||||
|
|
||||||
// 尝试在当前文件列表中查找
|
// 尝试在当前文件列表中查找
|
||||||
const file = fileList.value.find(f => {
|
const file = fileList.value.find(f => {
|
||||||
const normalizedFilePath = f.path.replace(/\\/g, '/').toLowerCase()
|
const normalizedFilePath = f.path.toLowerCase()
|
||||||
return normalizedFilePath === normalizedPath
|
return normalizedFilePath === normalizedPath
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -925,7 +970,7 @@ const selectFile = async (path: string) => {
|
|||||||
selectedFileItem.value = {
|
selectedFileItem.value = {
|
||||||
path,
|
path,
|
||||||
name: fileName,
|
name: fileName,
|
||||||
is_dir: false,
|
isDir: false,
|
||||||
size: 0,
|
size: 0,
|
||||||
mod_time: '',
|
mod_time: '',
|
||||||
is_favorite: isFavorite(path)
|
is_favorite: isFavorite(path)
|
||||||
@@ -988,6 +1033,35 @@ const loadDirectory = async (path: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加文件到列表(保持排序)
|
||||||
|
*/
|
||||||
|
const addFileToList = (item: FileItem) => {
|
||||||
|
fileList.value = sortFileList([...fileList.value, { ...item, is_favorite: false }])
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从列表中移除文件
|
||||||
|
*/
|
||||||
|
const removeFileFromList = (path: string) => {
|
||||||
|
fileList.value = fileList.value.filter(f => f.path !== path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新列表中的文件信息(保留运行时状态如 is_favorite)
|
||||||
|
*/
|
||||||
|
const updateFileInList = (oldPath: string, newItem: FileItem) => {
|
||||||
|
const index = fileList.value.findIndex(f => f.path === oldPath)
|
||||||
|
if (index !== -1) {
|
||||||
|
// 保留原有属性(如 is_favorite),更新其他字段
|
||||||
|
fileList.value[index] = {
|
||||||
|
...fileList.value[index], // 保留原有所有属性
|
||||||
|
...newItem, // 覆盖新字段
|
||||||
|
is_favorite: fileList.value[index].is_favorite // 确保保留收藏状态
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 加载 ZIP 目录内容
|
// 加载 ZIP 目录内容
|
||||||
const loadZipDirectoryContents = async (zipPath: string, currentDir: string): Promise<FileItem[]> => {
|
const loadZipDirectoryContents = async (zipPath: string, currentDir: string): Promise<FileItem[]> => {
|
||||||
fileLoading.value = true
|
fileLoading.value = true
|
||||||
@@ -1013,7 +1087,7 @@ const loadZipDirectoryContents = async (zipPath: string, currentDir: string): Pr
|
|||||||
const result = filtered.map((f: any) => ({
|
const result = filtered.map((f: any) => ({
|
||||||
name: f.name,
|
name: f.name,
|
||||||
path: f.path,
|
path: f.path,
|
||||||
is_dir: f.is_dir,
|
isDir: f.isDir,
|
||||||
size: f.size || 0,
|
size: f.size || 0,
|
||||||
mod_time: f.mod_time || '',
|
mod_time: f.mod_time || '',
|
||||||
is_favorite: false
|
is_favorite: false
|
||||||
|
|||||||
@@ -1,26 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<a-tooltip :content="tooltipText" position="bottom">
|
<a-tooltip :content="themeStore.tooltipText" position="bottom">
|
||||||
<div
|
<div
|
||||||
class="theme-toggle-btn"
|
class="theme-toggle-btn"
|
||||||
@click="handleToggle"
|
@click="handleToggle"
|
||||||
>
|
>
|
||||||
{{ isDark ? '🌙' : '☀️' }}
|
{{ themeStore.isDark ? '🌙' : '☀️' }}
|
||||||
</div>
|
</div>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { useThemeStore } from '../stores/theme'
|
||||||
import { useTheme } from '../composables/useTheme'
|
|
||||||
|
|
||||||
const { isDark, toggleTheme } = useTheme()
|
const themeStore = useThemeStore()
|
||||||
|
|
||||||
const tooltipText = computed(() => {
|
|
||||||
return isDark.value ? '切换到亮色主题' : '切换到夜间主题'
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleToggle = () => {
|
const handleToggle = () => {
|
||||||
toggleTheme()
|
themeStore.toggleTheme()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch, onMounted, onUnmounted, h, nextTick } from 'vue'
|
import { computed, watch, onMounted, onUnmounted, h, nextTick } from 'vue'
|
||||||
import { Modal, Message, Progress } from '@arco-design/web-vue'
|
import { Modal, Message, Progress } from '@arco-design/web-vue'
|
||||||
|
import { useUpdateStore } from '../stores/update'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
@@ -17,21 +18,10 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue', 'skip'])
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
// State
|
// 使用更新管理 store
|
||||||
const downloading = ref(false)
|
const updateStore = useUpdateStore()
|
||||||
const installing = ref(false)
|
|
||||||
const downloadProgress = ref(0)
|
|
||||||
const progressInfo = ref({
|
|
||||||
speed: 0,
|
|
||||||
downloaded: 0,
|
|
||||||
total: 0
|
|
||||||
})
|
|
||||||
|
|
||||||
// 节流:防止过度更新
|
|
||||||
let lastUpdateTime = 0
|
|
||||||
const UPDATE_THROTTLE = 100 // 100ms 最小更新间隔
|
|
||||||
|
|
||||||
// 模态框实例
|
// 模态框实例
|
||||||
let confirmModalInstance = null
|
let confirmModalInstance = null
|
||||||
@@ -53,22 +43,6 @@ watch(() => props.modelValue, (val) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Utility functions
|
// Utility functions
|
||||||
const parseEventData = (event) => {
|
|
||||||
try {
|
|
||||||
return typeof event === 'string' ? JSON.parse(event) : event
|
|
||||||
} catch {
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatFileSize = (bytes) => {
|
|
||||||
if (!bytes || bytes < 0) return '0 B'
|
|
||||||
const k = 1024
|
|
||||||
const sizes = ['B', 'KB', 'MB', 'GB']
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
||||||
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatDate = (dateStr) => {
|
const formatDate = (dateStr) => {
|
||||||
if (!dateStr) return ''
|
if (!dateStr) return ''
|
||||||
try {
|
try {
|
||||||
@@ -126,7 +100,7 @@ const showUpdateModal = () => {
|
|||||||
metadata.push(formatDate(props.updateInfo.release_date))
|
metadata.push(formatDate(props.updateInfo.release_date))
|
||||||
}
|
}
|
||||||
if (props.updateInfo?.file_size) {
|
if (props.updateInfo?.file_size) {
|
||||||
metadata.push(formatFileSize(props.updateInfo.file_size))
|
metadata.push(updateStore.formatFileSize(props.updateInfo.file_size))
|
||||||
}
|
}
|
||||||
if (metadata.length > 0) {
|
if (metadata.length > 0) {
|
||||||
elements.push(
|
elements.push(
|
||||||
@@ -164,7 +138,6 @@ const showUpdateModal = () => {
|
|||||||
onCancel: () => {
|
onCancel: () => {
|
||||||
confirmModalInstance = null
|
confirmModalInstance = null
|
||||||
emit('update:modelValue', false)
|
emit('update:modelValue', false)
|
||||||
emit('skip')
|
|
||||||
},
|
},
|
||||||
onBeforeCancel: () => {
|
onBeforeCancel: () => {
|
||||||
if (forceUpdate.value) {
|
if (forceUpdate.value) {
|
||||||
@@ -178,47 +151,51 @@ const showUpdateModal = () => {
|
|||||||
|
|
||||||
// 生成进度弹窗内容
|
// 生成进度弹窗内容
|
||||||
const getProgressModalContent = () => {
|
const getProgressModalContent = () => {
|
||||||
if (downloading.value) {
|
// 下载中状态
|
||||||
// 后端返回的 progress 是 0-100,Arco Progress 组件期望 0-1
|
if (updateStore.downloading) {
|
||||||
const progressValue = Number(Math.min(100, Math.max(0, downloadProgress.value || 0)))
|
const progressValue = Math.min(100, Math.max(0, updateStore.downloadProgress || 0))
|
||||||
const finalProgress = progressValue / 100
|
const finalProgress = progressValue / 100
|
||||||
|
|
||||||
|
const { downloaded, total, speed } = updateStore.progressInfo
|
||||||
|
const sizeText = total > 0
|
||||||
|
? `${updateStore.formatFileSize(downloaded)} / ${updateStore.formatFileSize(total)}`
|
||||||
|
: updateStore.downloadProgress > 0 ? '计算文件大小...' : '准备下载...'
|
||||||
|
|
||||||
|
const speedElement = speed > 0
|
||||||
|
? h('div', { style: { fontSize: '12px', color: 'var(--color-text-3)', marginTop: '4px' } },
|
||||||
|
`下载速度: ${updateStore.formatSpeed(speed)}`
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
|
||||||
return [
|
return [
|
||||||
h('div', { style: { marginBottom: '16px' } }, [
|
h('div', { style: { marginBottom: '16px' } }, [
|
||||||
h('div', { style: { marginBottom: '8px', fontSize: '14px', color: 'var(--color-text-2)' } }, '正在下载更新包...')
|
h('div', { style: { marginBottom: '8px', fontSize: '14px', color: 'var(--color-text-2)' } }, '正在下载更新包...')
|
||||||
]),
|
]),
|
||||||
h('div', { style: { marginBottom: '8px' } }, [
|
h('div', { style: { marginBottom: '8px' } }, [
|
||||||
h(Progress, {
|
h(Progress, { percent: finalProgress, showText: true })
|
||||||
percent: finalProgress,
|
|
||||||
showText: true
|
|
||||||
})
|
|
||||||
]),
|
]),
|
||||||
h('div', { style: { fontSize: '12px', color: 'var(--color-text-3)', marginTop: '8px' } }, [
|
h('div', { style: { fontSize: '12px', color: 'var(--color-text-3)', marginTop: '8px' } }, sizeText),
|
||||||
progressInfo.value.total > 0
|
speedElement
|
||||||
? `${formatFileSize(progressInfo.value.downloaded)} / ${formatFileSize(progressInfo.value.total)}`
|
|
||||||
: downloadProgress.value > 0 ? '计算文件大小...' : '准备下载...'
|
|
||||||
]),
|
|
||||||
progressInfo.value.speed > 0
|
|
||||||
? h('div', { style: { fontSize: '12px', color: 'var(--color-text-3)', marginTop: '4px' } },
|
|
||||||
`下载速度: ${formatFileSize(progressInfo.value.speed)}/s`
|
|
||||||
)
|
|
||||||
: null
|
|
||||||
]
|
]
|
||||||
} else if (installing.value) {
|
}
|
||||||
|
|
||||||
|
// 安装中状态
|
||||||
|
if (updateStore.installing) {
|
||||||
return [
|
return [
|
||||||
h('div', { style: { marginBottom: '16px' } }, [
|
h('div', { style: { marginBottom: '16px' } }, [
|
||||||
h('div', { style: { marginBottom: '8px', fontSize: '14px', color: 'var(--color-text-2)' } }, '正在安装更新...')
|
h('div', { style: { marginBottom: '8px', fontSize: '14px', color: 'var(--color-text-2)' } }, '正在安装更新...')
|
||||||
]),
|
]),
|
||||||
h('div', { style: { fontSize: '12px', color: 'var(--color-text-3)' } }, '请稍候,应用将在安装完成后自动重启...')
|
h('div', { style: { fontSize: '12px', color: 'var(--color-text-3)' } }, '请稍候,应用将在安装完成后自动重启...')
|
||||||
]
|
]
|
||||||
} else {
|
|
||||||
return [
|
|
||||||
h('div', { style: { marginBottom: '16px', textAlign: 'center' } }, [
|
|
||||||
h('div', { style: { fontSize: '16px', color: 'rgb(var(--success-6))', marginBottom: '8px' } }, '✓ 更新完成')
|
|
||||||
]),
|
|
||||||
h('div', { style: { fontSize: '14px', color: 'var(--color-text-2)' } }, '应用将在几秒后自动重启...')
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 完成状态
|
||||||
|
return [
|
||||||
|
h('div', { style: { marginBottom: '16px', textAlign: 'center' } }, [
|
||||||
|
h('div', { style: { fontSize: '16px', color: 'rgb(var(--success-6))', marginBottom: '8px' } }, '✓ 更新完成')
|
||||||
|
]),
|
||||||
|
h('div', { style: { fontSize: '14px', color: 'var(--color-text-2)' } }, '应用将在几秒后自动重启...')
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新进度弹窗内容
|
// 更新进度弹窗内容
|
||||||
@@ -237,11 +214,6 @@ const showProgressModal = async () => {
|
|||||||
progressModalInstance = null
|
progressModalInstance = null
|
||||||
}
|
}
|
||||||
|
|
||||||
downloading.value = true
|
|
||||||
installing.value = false
|
|
||||||
downloadProgress.value = 0
|
|
||||||
progressInfo.value = { speed: 0, downloaded: 0, total: 0 }
|
|
||||||
|
|
||||||
progressModalInstance = Modal.info({
|
progressModalInstance = Modal.info({
|
||||||
title: '更新进度',
|
title: '更新进度',
|
||||||
content: () => getProgressModalContent(),
|
content: () => getProgressModalContent(),
|
||||||
@@ -252,8 +224,16 @@ const showProgressModal = async () => {
|
|||||||
|
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
|
||||||
|
// 监听 store 状态变化
|
||||||
const stopWatcher = watch(
|
const stopWatcher = watch(
|
||||||
[downloadProgress, downloading, installing, () => progressInfo.value.total, () => progressInfo.value.downloaded, () => progressInfo.value.speed],
|
[
|
||||||
|
() => updateStore.downloadProgress,
|
||||||
|
() => updateStore.downloading,
|
||||||
|
() => updateStore.installing,
|
||||||
|
() => updateStore.progressInfo.total,
|
||||||
|
() => updateStore.progressInfo.downloaded,
|
||||||
|
() => updateStore.progressInfo.speed
|
||||||
|
],
|
||||||
async () => {
|
async () => {
|
||||||
await nextTick(updateProgressModal)
|
await nextTick(updateProgressModal)
|
||||||
},
|
},
|
||||||
@@ -292,112 +272,49 @@ const handleDownload = async () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await window.go.main.App.DownloadUpdate(props.updateInfo.download_url)
|
const result = await window.go.main.App.DownloadUpdate(props.updateInfo.download_url)
|
||||||
if (!result.success) {
|
if (result.success) return
|
||||||
closeProgressModal()
|
|
||||||
Message.error(result.message || '下载启动失败')
|
closeProgressModal()
|
||||||
downloading.value = false
|
Message.error(result.message || '下载启动失败')
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('下载失败:', error)
|
console.error('下载失败:', error)
|
||||||
closeProgressModal()
|
closeProgressModal()
|
||||||
Message.error('下载失败:' + (error.message || error))
|
Message.error('下载失败:' + (error.message || error))
|
||||||
downloading.value = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 下载进度处理
|
// 下载完成处理(本地覆盖:关闭弹窗)
|
||||||
const onDownloadProgress = (event) => {
|
|
||||||
const now = Date.now()
|
|
||||||
|
|
||||||
// 节流:防止过度更新
|
|
||||||
if (now - lastUpdateTime < UPDATE_THROTTLE) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
lastUpdateTime = now
|
|
||||||
|
|
||||||
const data = parseEventData(event)
|
|
||||||
progressInfo.value = {
|
|
||||||
speed: data.speed || 0,
|
|
||||||
downloaded: data.downloaded || 0,
|
|
||||||
total: data.total || 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确保进度值在 0-100 之间,并转换为数字类型
|
|
||||||
const rawProgress = Number(data.progress) || 0
|
|
||||||
const safeProgress = Math.min(100, Math.max(0, Math.round(rawProgress)))
|
|
||||||
|
|
||||||
// 只有当新值与旧值不同时才更新
|
|
||||||
if (safeProgress !== downloadProgress.value) {
|
|
||||||
downloadProgress.value = safeProgress
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 下载完成处理
|
|
||||||
const onDownloadComplete = async (event) => {
|
const onDownloadComplete = async (event) => {
|
||||||
const data = parseEventData(event)
|
const data = typeof event === 'string' ? JSON.parse(event) : event
|
||||||
|
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
closeProgressModal()
|
closeProgressModal()
|
||||||
Message.error('下载失败:' + data.error)
|
Message.error('下载失败:' + data.error)
|
||||||
downloading.value = false
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data.success || !data.file_path) {
|
if (!data.success || !data.file_path) {
|
||||||
closeProgressModal()
|
closeProgressModal()
|
||||||
Message.error('下载完成但数据不完整')
|
Message.error('下载完成但数据不完整')
|
||||||
downloading.value = false
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadProgress.value = Math.min(100, Math.max(0, 100))
|
// 等待安装完成
|
||||||
progressInfo.value.downloaded = data.file_size || 0
|
await new Promise(resolve => setTimeout(resolve, 3000))
|
||||||
progressInfo.value.total = data.file_size || 0
|
closeProgressModal()
|
||||||
await nextTick(updateProgressModal)
|
emit('update:modelValue', false)
|
||||||
await new Promise(r => setTimeout(r, 800))
|
|
||||||
|
|
||||||
await handleInstallDirect(data.file_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 安装更新
|
|
||||||
const handleInstallDirect = async (filePath) => {
|
|
||||||
downloading.value = false
|
|
||||||
installing.value = true
|
|
||||||
await updateProgressModal()
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await window.go.main.App.InstallUpdate(filePath, true)
|
|
||||||
|
|
||||||
if (result.success || result.data?.success) {
|
|
||||||
await updateProgressModal()
|
|
||||||
setTimeout(() => {
|
|
||||||
closeProgressModal()
|
|
||||||
emit('update:modelValue', false)
|
|
||||||
}, 3000)
|
|
||||||
} else {
|
|
||||||
installing.value = false
|
|
||||||
await updateProgressModal()
|
|
||||||
Message.error(result.message || '安装失败')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('安装失败:', error)
|
|
||||||
installing.value = false
|
|
||||||
await updateProgressModal()
|
|
||||||
Message.error('安装失败:' + (error.message || error))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生命周期
|
// 生命周期
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
// 监听下载完成事件(仅用于关闭弹窗)
|
||||||
if (window.runtime?.EventsOn) {
|
if (window.runtime?.EventsOn) {
|
||||||
window.runtime.EventsOn('download-progress', onDownloadProgress)
|
|
||||||
window.runtime.EventsOn('download-complete', onDownloadComplete)
|
window.runtime.EventsOn('download-complete', onDownloadComplete)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (window.runtime?.EventsOff) {
|
if (window.runtime?.EventsOff) {
|
||||||
window.runtime.EventsOff('download-progress')
|
|
||||||
window.runtime.EventsOff('download-complete')
|
window.runtime.EventsOff('download-complete')
|
||||||
}
|
}
|
||||||
closeModals()
|
closeModals()
|
||||||
|
|||||||
@@ -79,10 +79,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</a-alert>
|
</a-alert>
|
||||||
|
|
||||||
|
<!-- 调试信息(始终显示) -->
|
||||||
|
<div style="font-size: 12px; color: #999; padding: 8px; background: var(--color-fill-2); margin-top: 16px; border-radius: 4px;">
|
||||||
|
<strong>调试信息:</strong>
|
||||||
|
<br>downloading = {{ downloading }}
|
||||||
|
<br>downloadProgress = {{ downloadProgress }}
|
||||||
|
<br>downloadStatus = {{ downloadStatus }}
|
||||||
|
<br>progressInfo = {{ progressInfo }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 下载进度 -->
|
<!-- 下载进度 -->
|
||||||
<div v-if="downloadProgress > 0 || downloading" class="download-progress">
|
<div v-if="downloadProgress > 0 || downloading" class="download-progress">
|
||||||
|
<div style="font-size: 11px; color: #999; margin-bottom: 8px;">
|
||||||
|
进度条已显示:downloadProgress={{ downloadProgress }}, downloading={{ downloading }}
|
||||||
|
</div>
|
||||||
<a-progress
|
<a-progress
|
||||||
:percent="downloadProgress / 100"
|
:percent="downloadProgress"
|
||||||
:status="downloadStatus"
|
:status="downloadStatus"
|
||||||
/>
|
/>
|
||||||
<div class="progress-info">
|
<div class="progress-info">
|
||||||
@@ -109,66 +121,37 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted } from 'vue'
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
import { Message, Modal } from '@arco-design/web-vue'
|
import { Message, Modal } from '@arco-design/web-vue'
|
||||||
import { IconCheck, IconClose } from '@arco-design/web-vue/es/icon'
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { useUpdateStore } from '../stores/update'
|
||||||
|
|
||||||
// 工具函数:解析事件数据
|
// 使用更新管理 store
|
||||||
const parseEventData = (event) => {
|
const updateStore = useUpdateStore()
|
||||||
try {
|
|
||||||
return typeof event === 'string' ? JSON.parse(event) : event
|
|
||||||
} catch {
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 状态
|
// 使用 storeToRefs 解构以保持响应性
|
||||||
|
const { checking, downloading, installing, downloadProgress, downloadStatus, progressInfo, updateInfo } = storeToRefs(updateStore)
|
||||||
|
|
||||||
|
// 本地状态
|
||||||
const currentVersion = ref('-')
|
const currentVersion = ref('-')
|
||||||
const lastCheckTime = ref('-')
|
const lastCheckTime = ref('-')
|
||||||
const checking = ref(false)
|
|
||||||
const downloading = ref(false)
|
|
||||||
const installing = ref(false)
|
|
||||||
const saving = ref(false)
|
|
||||||
const updateInfo = ref(null)
|
|
||||||
const downloadedFile = ref(null)
|
|
||||||
const installResult = ref(null)
|
const installResult = ref(null)
|
||||||
const downloadProgress = ref(0)
|
const downloadedFile = ref(null)
|
||||||
const downloadStatus = ref('active')
|
|
||||||
|
|
||||||
// 配置
|
// 工具函数
|
||||||
const config = ref({
|
|
||||||
auto_check_enabled: true,
|
|
||||||
check_interval_minutes: 60,
|
|
||||||
check_url: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
// 下载进度信息
|
|
||||||
const progressInfo = ref({
|
|
||||||
progress: 0,
|
|
||||||
speed: 0,
|
|
||||||
downloaded: 0,
|
|
||||||
total: 0
|
|
||||||
})
|
|
||||||
|
|
||||||
// 格式化文件大小
|
|
||||||
const formatFileSize = (bytes) => {
|
const formatFileSize = (bytes) => {
|
||||||
if (!bytes || bytes < 0) return '0 B'
|
return updateStore.formatFileSize(bytes)
|
||||||
const k = 1024
|
|
||||||
const sizes = ['B', 'KB', 'MB', 'GB']
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
||||||
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 格式化速度
|
|
||||||
const formatSpeed = (bytesPerSecond) => {
|
const formatSpeed = (bytesPerSecond) => {
|
||||||
return formatFileSize(bytesPerSecond) + '/s'
|
return updateStore.formatSpeed(bytesPerSecond)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载当前版本
|
// 加载当前版本
|
||||||
const loadCurrentVersion = async () => {
|
const loadCurrentVersion = async () => {
|
||||||
try {
|
try {
|
||||||
const result = await window.go.main.App.GetCurrentVersion()
|
const result = await window.go.main.App.GetCurrentVersion()
|
||||||
if (result.success) {
|
if (!result.success) return
|
||||||
currentVersion.value = result.data?.version || '-'
|
|
||||||
}
|
currentVersion.value = result.data?.version || '-'
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取版本失败:', error)
|
console.error('获取版本失败:', error)
|
||||||
}
|
}
|
||||||
@@ -178,114 +161,30 @@ const loadCurrentVersion = async () => {
|
|||||||
const loadConfig = async () => {
|
const loadConfig = async () => {
|
||||||
try {
|
try {
|
||||||
const result = await window.go.main.App.GetUpdateConfig()
|
const result = await window.go.main.App.GetUpdateConfig()
|
||||||
if (result.success) {
|
if (!result.success) return
|
||||||
config.value = {
|
|
||||||
auto_check_enabled: result.data.auto_check_enabled || false,
|
const { last_check_time = '-' } = result.data || {}
|
||||||
check_interval_minutes: result.data.check_interval_minutes || 60,
|
lastCheckTime.value = last_check_time
|
||||||
check_url: result.data.check_url || ''
|
|
||||||
}
|
|
||||||
lastCheckTime.value = result.data.last_check_time || '-'
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载配置失败:', error)
|
console.error('加载配置失败:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 配置变化时自动保存(防抖)
|
|
||||||
let saveTimer = null
|
|
||||||
const handleConfigChange = () => {
|
|
||||||
// 清除之前的定时器
|
|
||||||
if (saveTimer) {
|
|
||||||
clearTimeout(saveTimer)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置新的定时器,1秒后保存
|
|
||||||
saveTimer = setTimeout(async () => {
|
|
||||||
await saveConfig()
|
|
||||||
}, 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存配置
|
|
||||||
const saveConfig = async () => {
|
|
||||||
saving.value = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await window.go.main.App.SetUpdateConfig(
|
|
||||||
config.value.auto_check_enabled,
|
|
||||||
config.value.check_interval_minutes,
|
|
||||||
config.value.check_url
|
|
||||||
)
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
Message.success('配置已自动保存')
|
|
||||||
await loadConfig()
|
|
||||||
} else {
|
|
||||||
Message.error(result.message || '保存配置失败')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('保存配置失败:', error)
|
|
||||||
Message.error('保存配置失败:' + (error.message || error))
|
|
||||||
} finally {
|
|
||||||
saving.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查更新
|
// 检查更新
|
||||||
const handleCheckUpdate = async () => {
|
const handleCheckUpdate = async () => {
|
||||||
checking.value = true
|
|
||||||
updateInfo.value = null
|
|
||||||
installResult.value = null
|
installResult.value = null
|
||||||
|
|
||||||
try {
|
// 使用 store 的检查方法(非静默模式,显示消息)
|
||||||
const result = await window.go.main.App.CheckUpdate()
|
await updateStore.checkForUpdates(false)
|
||||||
if (result.success) {
|
|
||||||
updateInfo.value = result.data
|
// 刷新最后检查时间
|
||||||
if (result.data.has_update) {
|
await loadConfig()
|
||||||
Message.success('发现新版本!')
|
|
||||||
} else {
|
|
||||||
Message.success('已是最新版本')
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Message.error(result.message || '检查更新失败')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('检查更新失败:', error)
|
|
||||||
Message.error('检查更新失败:' + (error.message || error))
|
|
||||||
} finally {
|
|
||||||
checking.value = false
|
|
||||||
// 刷新最后检查时间
|
|
||||||
await loadConfig()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 下载更新
|
// 下载更新
|
||||||
const handleDownload = async () => {
|
const handleDownload = async () => {
|
||||||
if (!updateInfo.value?.download_url) {
|
// 使用 store 的下载方法,会自动管理状态和事件监听
|
||||||
Message.warning('下载地址不存在')
|
await updateStore.downloadUpdate()
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
downloading.value = true
|
|
||||||
downloadProgress.value = 0
|
|
||||||
downloadStatus.value = 'active'
|
|
||||||
progressInfo.value = { progress: 0, speed: 0, downloaded: 0, total: 0 }
|
|
||||||
installResult.value = null
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await window.go.main.App.DownloadUpdate(updateInfo.value.download_url)
|
|
||||||
if (result.success) {
|
|
||||||
Message.success('下载请求已发送')
|
|
||||||
} else {
|
|
||||||
downloadStatus.value = 'exception'
|
|
||||||
Message.error(result.message || '下载启动失败')
|
|
||||||
downloading.value = false
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('下载失败:', error)
|
|
||||||
downloadStatus.value = 'exception'
|
|
||||||
Message.error('下载失败:' + (error.message || error))
|
|
||||||
downloading.value = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 安装更新
|
// 安装更新
|
||||||
@@ -295,7 +194,6 @@ const handleInstall = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确认对话框
|
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: '确认安装',
|
title: '确认安装',
|
||||||
content: '安装更新后应用将自动重启,是否继续?',
|
content: '安装更新后应用将自动重启,是否继续?',
|
||||||
@@ -304,27 +202,24 @@ const handleInstall = async () => {
|
|||||||
installResult.value = null
|
installResult.value = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await window.go.main.App.InstallUpdate(
|
const result = await window.go.main.App.InstallUpdate(downloadedFile.value, true)
|
||||||
downloadedFile.value,
|
|
||||||
true // 自动重启
|
|
||||||
)
|
|
||||||
installResult.value = result.data || result
|
installResult.value = result.data || result
|
||||||
|
|
||||||
if (result.success || result.data?.success) {
|
const success = result.success || result.data?.success
|
||||||
Message.success({
|
if (!success) {
|
||||||
content: '安装成功!应用将在几秒后重启...',
|
|
||||||
duration: 3000
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Message.error(result.message || '安装失败')
|
Message.error(result.message || '安装失败')
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Message.success({
|
||||||
|
content: '安装成功!应用将在几秒后重启...',
|
||||||
|
duration: 3000
|
||||||
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('安装失败:', error)
|
console.error('安装失败:', error)
|
||||||
installResult.value = {
|
const errorMsg = '安装失败:' + (error.message || error)
|
||||||
success: false,
|
installResult.value = { success: false, message: errorMsg }
|
||||||
message: '安装失败:' + (error.message || error)
|
Message.error(errorMsg)
|
||||||
}
|
|
||||||
Message.error('安装失败:' + (error.message || error))
|
|
||||||
} finally {
|
} finally {
|
||||||
installing.value = false
|
installing.value = false
|
||||||
}
|
}
|
||||||
@@ -332,34 +227,12 @@ const handleInstall = async () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听下载进度事件
|
// 监听下载完成事件(本地覆盖:记录下载文件路径)
|
||||||
const onDownloadProgress = (event) => {
|
|
||||||
const data = parseEventData(event)
|
|
||||||
progressInfo.value = {
|
|
||||||
progress: data.progress || 0,
|
|
||||||
speed: data.speed || 0,
|
|
||||||
downloaded: data.downloaded || 0,
|
|
||||||
total: data.total || 0
|
|
||||||
}
|
|
||||||
// 确保进度值在 0-100 之间
|
|
||||||
const rawProgress = data.progress || 0
|
|
||||||
downloadProgress.value = Math.min(100, Math.max(0, Math.round(rawProgress)))
|
|
||||||
console.log('[下载进度] 原始值:', rawProgress, '处理后:', downloadProgress.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听下载完成事件
|
|
||||||
const onDownloadComplete = (event) => {
|
const onDownloadComplete = (event) => {
|
||||||
downloading.value = false
|
const data = typeof event === 'string' ? JSON.parse(event) : event
|
||||||
const data = parseEventData(event)
|
|
||||||
|
|
||||||
if (data.error) {
|
if (data.success && data.file_path) {
|
||||||
downloadStatus.value = 'exception'
|
|
||||||
Message.error('下载失败:' + data.error)
|
|
||||||
} else if (data.success) {
|
|
||||||
downloadStatus.value = 'success'
|
|
||||||
downloadProgress.value = 100
|
|
||||||
downloadedFile.value = data.file_path
|
downloadedFile.value = data.file_path
|
||||||
Message.success('下载完成!文件已保存到:' + data.file_path)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,9 +240,8 @@ onMounted(async () => {
|
|||||||
await loadCurrentVersion()
|
await loadCurrentVersion()
|
||||||
await loadConfig()
|
await loadConfig()
|
||||||
|
|
||||||
// 监听下载进度事件
|
// 监听下载完成事件(仅用于记录文件路径)
|
||||||
if (window.runtime?.EventsOn) {
|
if (window.runtime?.EventsOn) {
|
||||||
window.runtime.EventsOn('download-progress', onDownloadProgress)
|
|
||||||
window.runtime.EventsOn('download-complete', onDownloadComplete)
|
window.runtime.EventsOn('download-complete', onDownloadComplete)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -377,14 +249,8 @@ onMounted(async () => {
|
|||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
// 取消事件监听
|
// 取消事件监听
|
||||||
if (window.runtime?.EventsOff) {
|
if (window.runtime?.EventsOff) {
|
||||||
window.runtime.EventsOff('download-progress')
|
|
||||||
window.runtime.EventsOff('download-complete')
|
window.runtime.EventsOff('download-complete')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清除定时器
|
|
||||||
if (saveTimer) {
|
|
||||||
clearTimeout(saveTimer)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { ref, computed } from 'vue'
|
|||||||
import { marked } from '@/utils/markedExtensions'
|
import { marked } from '@/utils/markedExtensions'
|
||||||
import { FILE_EXTENSIONS, FILE_SIZE_THRESHOLDS } from '@/utils/constants'
|
import { FILE_EXTENSIONS, FILE_SIZE_THRESHOLDS } from '@/utils/constants'
|
||||||
import { getExt } from '@/utils/fileHelpers'
|
import { getExt } from '@/utils/fileHelpers'
|
||||||
|
import { isOfficeFile } from '@/utils/fileTypeHelpers'
|
||||||
import { debugLog, debugWarn, debugError } from '@/utils/debugLog'
|
import { debugLog, debugWarn, debugError } from '@/utils/debugLog'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -532,16 +533,6 @@ export function useFilePreview(options = {}) {
|
|||||||
imageHeight.value = 0
|
imageHeight.value = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断是否为 Office 文件
|
|
||||||
* @param {string} fileName - 文件名
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
const isOfficeFile = (fileName) => {
|
|
||||||
const ext = getExt(fileName).toLowerCase()
|
|
||||||
return ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'].includes(ext)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// 状态
|
// 状态
|
||||||
previewUrl,
|
previewUrl,
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
import { ref, computed } from 'vue'
|
|
||||||
|
|
||||||
type Theme = 'light' | 'dark'
|
|
||||||
|
|
||||||
const THEME_STORAGE_KEY = 'app-theme'
|
|
||||||
|
|
||||||
// 单例模式:全局共享主题状态
|
|
||||||
const theme = ref<Theme>('light')
|
|
||||||
let systemThemeListener: (() => void) | null = null
|
|
||||||
|
|
||||||
// 应用主题到 DOM
|
|
||||||
const applyTheme = (newTheme: Theme) => {
|
|
||||||
theme.value = newTheme
|
|
||||||
if (newTheme === 'dark') {
|
|
||||||
document.body.setAttribute('arco-theme', 'dark')
|
|
||||||
} else {
|
|
||||||
document.body.removeAttribute('arco-theme')
|
|
||||||
}
|
|
||||||
localStorage.setItem(THEME_STORAGE_KEY, newTheme)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化主题(只调用一次)
|
|
||||||
const initTheme = () => {
|
|
||||||
const savedTheme = localStorage.getItem(THEME_STORAGE_KEY) as Theme
|
|
||||||
if (savedTheme && (savedTheme === 'light' || savedTheme === 'dark')) {
|
|
||||||
applyTheme(savedTheme)
|
|
||||||
} else {
|
|
||||||
// 检测系统偏好
|
|
||||||
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
||||||
applyTheme('dark')
|
|
||||||
} else {
|
|
||||||
applyTheme('light')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听系统主题变化
|
|
||||||
if (window.matchMedia) {
|
|
||||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
|
||||||
const handleChange = (e: MediaQueryListEvent) => {
|
|
||||||
// 如果用户没有手动设置过主题,则跟随系统
|
|
||||||
if (!localStorage.getItem(THEME_STORAGE_KEY)) {
|
|
||||||
applyTheme(e.matches ? 'dark' : 'light')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mediaQuery.addEventListener('change', handleChange)
|
|
||||||
systemThemeListener = () => mediaQuery.removeEventListener('change', handleChange)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useTheme() {
|
|
||||||
// 切换主题
|
|
||||||
const toggleTheme = () => {
|
|
||||||
const newTheme: Theme = theme.value === 'light' ? 'dark' : 'light'
|
|
||||||
applyTheme(newTheme)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置为亮色主题
|
|
||||||
const setLightTheme = () => {
|
|
||||||
applyTheme('light')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置为暗色主题
|
|
||||||
const setDarkTheme = () => {
|
|
||||||
applyTheme('dark')
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
theme: computed(() => theme.value),
|
|
||||||
isDark: computed(() => theme.value === 'dark'),
|
|
||||||
toggleTheme,
|
|
||||||
setLightTheme,
|
|
||||||
setDarkTheme,
|
|
||||||
initTheme
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导出初始化函数(在 main.js 中使用)
|
|
||||||
export { initTheme }
|
|
||||||
117
web/src/composables/useTimeout.ts
Normal file
117
web/src/composables/useTimeout.ts
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
/**
|
||||||
|
* 定时器管理 Hook
|
||||||
|
* 自动管理定时器生命周期,防止内存泄漏
|
||||||
|
*
|
||||||
|
* @module composables/useTimeout
|
||||||
|
* @description 提供类型安全的定时器管理,组件卸载时自动清理所有定时器
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ref, onUnmounted } from 'vue'
|
||||||
|
|
||||||
|
export interface TimeoutOptions {
|
||||||
|
/**
|
||||||
|
* 是否在组件卸载时自动清理所有定时器
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
autoCleanup?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定时器管理 Hook
|
||||||
|
*
|
||||||
|
* @param options - 配置选项
|
||||||
|
* @returns 定时器管理方法
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const { setTimeout, clearTimeout, clearAll } = useTimeout()
|
||||||
|
*
|
||||||
|
* // 设置延迟执行
|
||||||
|
* const timer = setTimeout(() => {
|
||||||
|
* console.log('延迟执行')
|
||||||
|
* }, 1000)
|
||||||
|
*
|
||||||
|
* // 清除特定定时器
|
||||||
|
* clearTimeout(timer)
|
||||||
|
*
|
||||||
|
* // 清除所有定时器
|
||||||
|
* clearAll()
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function useTimeout(options: TimeoutOptions = {}) {
|
||||||
|
const { autoCleanup = true } = options
|
||||||
|
|
||||||
|
// 使用 Set 存储所有定时器 ID
|
||||||
|
const timers = ref<Set<NodeJS.Timeout>>(new Set())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置定时器(自动管理生命周期)
|
||||||
|
* @param callback - 要执行的回调函数
|
||||||
|
* @param delay - 延迟时间(毫秒)
|
||||||
|
* @returns 定时器 ID
|
||||||
|
*/
|
||||||
|
const setTimeout = <T = void>(
|
||||||
|
callback: () => T,
|
||||||
|
delay: number
|
||||||
|
): NodeJS.Timeout => {
|
||||||
|
const timer = window.setTimeout(() => {
|
||||||
|
try {
|
||||||
|
callback()
|
||||||
|
} finally {
|
||||||
|
// 执行完成后自动从集合中移除
|
||||||
|
timers.value.delete(timer)
|
||||||
|
}
|
||||||
|
}, delay)
|
||||||
|
|
||||||
|
// 添加到集合中
|
||||||
|
timers.value.add(timer)
|
||||||
|
|
||||||
|
return timer
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除特定定时器
|
||||||
|
* @param timer - 要清除的定时器 ID
|
||||||
|
*/
|
||||||
|
const clearTimeout = (timer: NodeJS.Timeout) => {
|
||||||
|
window.clearTimeout(timer)
|
||||||
|
timers.value.delete(timer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除所有定时器
|
||||||
|
*/
|
||||||
|
const clearAll = () => {
|
||||||
|
timers.value.forEach((timer) => {
|
||||||
|
window.clearTimeout(timer)
|
||||||
|
})
|
||||||
|
timers.value.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前活跃的定时器数量
|
||||||
|
*/
|
||||||
|
const getActiveCount = () => timers.value.size
|
||||||
|
|
||||||
|
// 组件卸载时自动清理
|
||||||
|
if (autoCleanup) {
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearAll()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
setTimeout,
|
||||||
|
clearTimeout,
|
||||||
|
clearAll,
|
||||||
|
getActiveCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定时器管理 Hook 的别名
|
||||||
|
* 便于语义化使用(如延迟执行、防抖等场景)
|
||||||
|
*/
|
||||||
|
export const useDelay = useTimeout
|
||||||
|
|
||||||
|
export default useTimeout
|
||||||
@@ -1,15 +1,19 @@
|
|||||||
import {createApp} from 'vue'
|
import { createApp } from 'vue'
|
||||||
import ArcoVue from '@arco-design/web-vue'
|
import { createPinia } from 'pinia'
|
||||||
|
// Arco Design 样式(组件按需自动引入)
|
||||||
import '@arco-design/web-vue/dist/arco.css'
|
import '@arco-design/web-vue/dist/arco.css'
|
||||||
import './style.css'
|
import './style.css'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import {initTheme} from './composables/useTheme'
|
import { useThemeStore } from './stores/theme'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
app.use(ArcoVue)
|
const pinia = createPinia()
|
||||||
|
|
||||||
// 在应用挂载前初始化主题
|
app.use(pinia)
|
||||||
initTheme()
|
|
||||||
|
// 在应用挂载前初始化主题(需要先初始化 Pinia)
|
||||||
|
const themeStore = useThemeStore()
|
||||||
|
themeStore.initTheme()
|
||||||
|
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
|
||||||
|
|||||||
187
web/src/stores/config.ts
Normal file
187
web/src/stores/config.ts
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { Message } from '@arco-design/web-vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tab 配置类型
|
||||||
|
*/
|
||||||
|
interface TabConfig {
|
||||||
|
key: string
|
||||||
|
title: string
|
||||||
|
visible: boolean
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用配置类型
|
||||||
|
*/
|
||||||
|
interface AppConfig {
|
||||||
|
tabs: TabConfig[]
|
||||||
|
visibleTabs: string[]
|
||||||
|
defaultTab: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用配置管理 Store
|
||||||
|
* 统一管理应用配置(标签页、默认页等)
|
||||||
|
*/
|
||||||
|
export const useConfigStore = defineStore('config', () => {
|
||||||
|
// ==================== 状态 ====================
|
||||||
|
const appConfig = ref<AppConfig>({
|
||||||
|
tabs: [],
|
||||||
|
visibleTabs: [],
|
||||||
|
defaultTab: 'file-system'
|
||||||
|
})
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
// ==================== 计算属性 ====================
|
||||||
|
/**
|
||||||
|
* 可见 Tabs(根据配置动态生成)
|
||||||
|
*/
|
||||||
|
const visibleTabs = computed(() => {
|
||||||
|
const tabs = appConfig.value.tabs
|
||||||
|
|
||||||
|
if (!tabs?.length) {
|
||||||
|
return [
|
||||||
|
{ key: 'file-system', title: '文件管理' },
|
||||||
|
{ key: 'db-cli', title: '数据库' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const { visibleTabs: order } = appConfig.value
|
||||||
|
return tabs
|
||||||
|
.filter(tab => tab.visible)
|
||||||
|
.sort((a, b) => order.indexOf(a.key) - order.indexOf(b.key))
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 所有可用 Tabs
|
||||||
|
*/
|
||||||
|
const allTabs = computed(() => appConfig.value.tabs)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认 Tab
|
||||||
|
*/
|
||||||
|
const defaultTab = computed(() => appConfig.value.defaultTab)
|
||||||
|
|
||||||
|
// ==================== 核心方法 ====================
|
||||||
|
/**
|
||||||
|
* 加载配置
|
||||||
|
*/
|
||||||
|
const loadConfig = async () => {
|
||||||
|
if (!window.go?.main?.App) {
|
||||||
|
console.warn('Wails 绑定未准备好,1秒后重试')
|
||||||
|
setTimeout(loadConfig, 1000)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await window.go.main.App.GetAppConfig()
|
||||||
|
if (!result.success) throw new Error(result.message)
|
||||||
|
|
||||||
|
const { tabs = [], visibleTabs = [], defaultTab = 'file-system' } = result.data
|
||||||
|
|
||||||
|
appConfig.value = {
|
||||||
|
tabs: tabs.map(tab => ({ ...tab, visible: visibleTabs.includes(tab.key) })),
|
||||||
|
visibleTabs,
|
||||||
|
defaultTab
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载配置失败:', error)
|
||||||
|
useDefaultConfig()
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用默认配置
|
||||||
|
*/
|
||||||
|
const useDefaultConfig = () => {
|
||||||
|
appConfig.value = {
|
||||||
|
tabs: [
|
||||||
|
{ key: 'file-system', title: '文件管理', visible: true, enabled: true },
|
||||||
|
{ key: 'db-cli', title: '数据库', visible: true, enabled: true }
|
||||||
|
],
|
||||||
|
visibleTabs: ['file-system', 'db-cli'],
|
||||||
|
defaultTab: 'file-system'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存配置
|
||||||
|
*/
|
||||||
|
const saveConfig = async (config: AppConfig) => {
|
||||||
|
if (!window.go?.main?.App) {
|
||||||
|
Message.error('Wails 绑定未准备好')
|
||||||
|
throw new Error('Wails binding not ready')
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await window.go.main.App.SaveAppConfig({
|
||||||
|
tabs: config.tabs,
|
||||||
|
visibleTabs: config.visibleTabs,
|
||||||
|
defaultTab: config.defaultTab
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
Message.error(result.message || '保存配置失败')
|
||||||
|
throw new Error(result.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新本地配置
|
||||||
|
appConfig.value = {
|
||||||
|
tabs: [...config.tabs],
|
||||||
|
visibleTabs: [...config.visibleTabs],
|
||||||
|
defaultTab: config.defaultTab
|
||||||
|
}
|
||||||
|
|
||||||
|
Message.success('配置保存成功')
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存配置失败:', error)
|
||||||
|
const message = error instanceof Error ? error.message : '保存配置失败'
|
||||||
|
Message.error('保存配置失败:' + message)
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查 Tab 是否可见
|
||||||
|
*/
|
||||||
|
const isTabVisible = (tabKey: string) => {
|
||||||
|
return appConfig.value.visibleTabs.includes(tabKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 Tab 配置
|
||||||
|
*/
|
||||||
|
const getTab = (tabKey: string) => {
|
||||||
|
return appConfig.value.tabs.find(tab => tab.key === tabKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 返回 ====================
|
||||||
|
return {
|
||||||
|
// 状态
|
||||||
|
appConfig,
|
||||||
|
loading,
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
visibleTabs,
|
||||||
|
allTabs,
|
||||||
|
defaultTab,
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
loadConfig,
|
||||||
|
saveConfig,
|
||||||
|
isTabVisible,
|
||||||
|
getTab
|
||||||
|
}
|
||||||
|
})
|
||||||
117
web/src/stores/theme.ts
Normal file
117
web/src/stores/theme.ts
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
|
type Theme = 'light' | 'dark'
|
||||||
|
|
||||||
|
const THEME_STORAGE_KEY = 'app-theme'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主题管理 Store
|
||||||
|
* 统一管理应用主题(亮色/暗色)及相关逻辑
|
||||||
|
*/
|
||||||
|
export const useThemeStore = defineStore('theme', () => {
|
||||||
|
// ==================== 状态 ====================
|
||||||
|
const theme = ref<Theme>('light')
|
||||||
|
let systemThemeListener: (() => void) | null = null
|
||||||
|
|
||||||
|
// ==================== 计算属性 ====================
|
||||||
|
const isDark = computed(() => theme.value === 'dark')
|
||||||
|
const isLight = computed(() => theme.value === 'light')
|
||||||
|
const tooltipText = computed(() =>
|
||||||
|
isDark.value ? '切换到亮色主题' : '切换到夜间主题'
|
||||||
|
)
|
||||||
|
|
||||||
|
// ==================== 核心方法 ====================
|
||||||
|
/**
|
||||||
|
* 应用主题到 DOM
|
||||||
|
*/
|
||||||
|
const applyTheme = (newTheme: Theme) => {
|
||||||
|
theme.value = newTheme
|
||||||
|
|
||||||
|
// 更新 DOM 属性
|
||||||
|
const method = newTheme === 'dark' ? 'setAttribute' : 'removeAttribute'
|
||||||
|
document.body[method]('arco-theme', 'dark')
|
||||||
|
|
||||||
|
// 持久化
|
||||||
|
localStorage.setItem(THEME_STORAGE_KEY, newTheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换主题
|
||||||
|
*/
|
||||||
|
const toggleTheme = () => {
|
||||||
|
const newTheme: Theme = theme.value === 'light' ? 'dark' : 'light'
|
||||||
|
applyTheme(newTheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置为亮色主题
|
||||||
|
*/
|
||||||
|
const setLightTheme = () => {
|
||||||
|
applyTheme('light')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置为暗色主题
|
||||||
|
*/
|
||||||
|
const setDarkTheme = () => {
|
||||||
|
applyTheme('dark')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化主题(应用启动时调用)
|
||||||
|
*/
|
||||||
|
const initTheme = () => {
|
||||||
|
// 加载保存的主题或使用系统偏好
|
||||||
|
const savedTheme = localStorage.getItem(THEME_STORAGE_KEY) as Theme
|
||||||
|
const isValidTheme = savedTheme === 'light' || savedTheme === 'dark'
|
||||||
|
|
||||||
|
if (isValidTheme) {
|
||||||
|
applyTheme(savedTheme)
|
||||||
|
} else {
|
||||||
|
const prefersDark = window.matchMedia?.('(prefers-color-scheme: dark)').matches
|
||||||
|
applyTheme(prefersDark ? 'dark' : 'light')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听系统主题变化(仅在未手动设置时)
|
||||||
|
if (!window.matchMedia) return
|
||||||
|
|
||||||
|
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||||
|
const handleChange = (e: MediaQueryListEvent) => {
|
||||||
|
if (!localStorage.getItem(THEME_STORAGE_KEY)) {
|
||||||
|
applyTheme(e.matches ? 'dark' : 'light')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaQuery.addEventListener('change', handleChange)
|
||||||
|
systemThemeListener = () => mediaQuery.removeEventListener('change', handleChange)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理系统主题监听器
|
||||||
|
*/
|
||||||
|
const removeSystemThemeListener = () => {
|
||||||
|
if (systemThemeListener) {
|
||||||
|
systemThemeListener()
|
||||||
|
systemThemeListener = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 返回 ====================
|
||||||
|
return {
|
||||||
|
// 状态
|
||||||
|
theme,
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
isDark,
|
||||||
|
isLight,
|
||||||
|
tooltipText,
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
toggleTheme,
|
||||||
|
setLightTheme,
|
||||||
|
setDarkTheme,
|
||||||
|
initTheme,
|
||||||
|
removeSystemThemeListener
|
||||||
|
}
|
||||||
|
})
|
||||||
288
web/src/stores/update.ts
Normal file
288
web/src/stores/update.ts
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref, reactive } from 'vue'
|
||||||
|
import { Message } from '@arco-design/web-vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新管理 Store
|
||||||
|
* 统一管理版本检查、下载、安装等更新相关逻辑
|
||||||
|
*/
|
||||||
|
export const useUpdateStore = defineStore('update', () => {
|
||||||
|
// ==================== 状态 ====================
|
||||||
|
const updateInfo = ref<UpdateInfo | null>(null)
|
||||||
|
const showUpdate = ref(false)
|
||||||
|
const checking = ref(false)
|
||||||
|
const downloading = ref(false)
|
||||||
|
const installing = ref(false)
|
||||||
|
const downloadProgress = ref(0)
|
||||||
|
const downloadStatus = ref<'active' | 'exception' | 'success'>('active')
|
||||||
|
const progressInfo = ref({
|
||||||
|
speed: 0,
|
||||||
|
downloaded: 0,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 节流:防止过度更新
|
||||||
|
let lastUpdateTime = 0
|
||||||
|
const UPDATE_THROTTLE = 100 // 100ms 最小更新间隔
|
||||||
|
|
||||||
|
// 最小显示时间:确保进度条至少显示 5 秒
|
||||||
|
let downloadStartTime = 0
|
||||||
|
const MIN_DISPLAY_TIME = 5000 // 5 秒最小显示时间
|
||||||
|
|
||||||
|
// ==================== 工具函数 ====================
|
||||||
|
const parseEventData = (event: unknown) => {
|
||||||
|
try {
|
||||||
|
return typeof event === 'string' ? JSON.parse(event) : (event as Record<string, unknown>)
|
||||||
|
} catch {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatFileSize = (bytes: number): string => {
|
||||||
|
if (!bytes || bytes < 0) return '0 B'
|
||||||
|
const k = 1024
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB']
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||||
|
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatSpeed = (bytesPerSecond: number): string => {
|
||||||
|
return formatFileSize(bytesPerSecond) + '/s'
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 核心方法 ====================
|
||||||
|
/**
|
||||||
|
* 检查更新
|
||||||
|
* @param silent 是否静默模式(不显示消息)
|
||||||
|
*/
|
||||||
|
const checkForUpdates = async (silent = false) => {
|
||||||
|
if (checking.value || !window.go?.main?.App) return
|
||||||
|
|
||||||
|
checking.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const configResult = await window.go.main.App.GetUpdateConfig()
|
||||||
|
if (!configResult.success) return
|
||||||
|
|
||||||
|
const { auto_check_enabled } = configResult.data || {}
|
||||||
|
if (!auto_check_enabled) return
|
||||||
|
|
||||||
|
const result = await window.go.main.App.CheckUpdate()
|
||||||
|
if (result.success && result.data?.has_update) {
|
||||||
|
updateInfo.value = result.data
|
||||||
|
showUpdate.value = true
|
||||||
|
|
||||||
|
if (!silent) {
|
||||||
|
Message.success('发现新版本!')
|
||||||
|
}
|
||||||
|
} else if (!silent) {
|
||||||
|
Message.success('已是最新版本')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (!silent) {
|
||||||
|
console.error('检查更新失败:', error)
|
||||||
|
Message.error('检查更新失败:' + (error as Error).message)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
checking.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载更新
|
||||||
|
*/
|
||||||
|
const downloadUpdate = async () => {
|
||||||
|
const url = updateInfo.value?.download_url
|
||||||
|
if (!url) {
|
||||||
|
Message.warning('下载地址不存在')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录开始时间
|
||||||
|
downloadStartTime = Date.now()
|
||||||
|
|
||||||
|
// 重置下载状态
|
||||||
|
downloading.value = true
|
||||||
|
downloadProgress.value = 1 // 设置为 1 而不是 0,确保进度条显示
|
||||||
|
downloadStatus.value = 'active'
|
||||||
|
progressInfo.value = { speed: 0, downloaded: 0, total: 0 }
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await window.go.main.App.DownloadUpdate(url)
|
||||||
|
if (!result.success) {
|
||||||
|
downloadStatus.value = 'exception'
|
||||||
|
downloading.value = false
|
||||||
|
Message.error(result.message || '下载启动失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Message.success('下载请求已发送,等待后端发送进度事件...')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('下载失败:', error)
|
||||||
|
downloadStatus.value = 'exception'
|
||||||
|
downloading.value = false
|
||||||
|
Message.error('下载失败:' + (error as Error).message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安装更新
|
||||||
|
*/
|
||||||
|
const installUpdate = async (filePath: string) => {
|
||||||
|
if (!filePath) {
|
||||||
|
Message.warning('请先下载更新包')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
installing.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await window.go.main.App.InstallUpdate(filePath, true)
|
||||||
|
if (result.success) {
|
||||||
|
Message.success({
|
||||||
|
content: '安装成功!应用将在几秒后重启...',
|
||||||
|
duration: 3000
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Message.error(result.message || '安装失败')
|
||||||
|
} catch (error) {
|
||||||
|
Message.error('安装失败:' + (error as Error).message)
|
||||||
|
} finally {
|
||||||
|
installing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载进度处理
|
||||||
|
*/
|
||||||
|
const onDownloadProgress = (event: unknown) => {
|
||||||
|
const now = Date.now()
|
||||||
|
if (now - lastUpdateTime < UPDATE_THROTTLE) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lastUpdateTime = now
|
||||||
|
const data = parseEventData(event)
|
||||||
|
|
||||||
|
progressInfo.value = {
|
||||||
|
speed: (data.speed as number) || 0,
|
||||||
|
downloaded: (data.downloaded as number) || 0,
|
||||||
|
total: (data.total as number) || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawProgress = Number(data.progress) || 0
|
||||||
|
const safeProgress = Math.min(100, Math.max(0, Math.round(rawProgress)))
|
||||||
|
downloadProgress.value = safeProgress
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载完成处理
|
||||||
|
*/
|
||||||
|
const onDownloadComplete = (event: unknown) => {
|
||||||
|
const data = parseEventData(event)
|
||||||
|
|
||||||
|
// 错误处理
|
||||||
|
if (data.error) {
|
||||||
|
console.error('下载失败:', data.error)
|
||||||
|
downloadStatus.value = 'exception'
|
||||||
|
downloading.value = false
|
||||||
|
Message.error('下载失败:' + data.error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数据验证
|
||||||
|
if (!data.success || !data.file_path) {
|
||||||
|
console.error('下载数据不完整:', data)
|
||||||
|
downloadStatus.value = 'exception'
|
||||||
|
downloading.value = false
|
||||||
|
Message.error('下载完成但数据不完整')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 完成下载
|
||||||
|
downloadProgress.value = 100
|
||||||
|
downloadStatus.value = 'success'
|
||||||
|
const fileSize = (data.file_size as number) || 0
|
||||||
|
progressInfo.value = {
|
||||||
|
speed: 0,
|
||||||
|
downloaded: fileSize,
|
||||||
|
total: fileSize
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算已经显示的时间
|
||||||
|
const elapsed = Date.now() - downloadStartTime
|
||||||
|
const remainingTime = Math.max(0, MIN_DISPLAY_TIME - elapsed)
|
||||||
|
|
||||||
|
// 确保进度条至少显示 3 秒
|
||||||
|
setTimeout(() => {
|
||||||
|
downloading.value = false // 安装前才关闭下载状态
|
||||||
|
installUpdate(data.file_path as string)
|
||||||
|
}, remainingTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置事件监听
|
||||||
|
*/
|
||||||
|
const setupEventListeners = () => {
|
||||||
|
if (!window.runtime?.EventsOn) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
window.runtime.EventsOn('download-progress', onDownloadProgress)
|
||||||
|
window.runtime.EventsOn('download-complete', onDownloadComplete)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除事件监听
|
||||||
|
*/
|
||||||
|
const removeEventListeners = () => {
|
||||||
|
if (!window.runtime?.EventsOff) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
window.runtime.EventsOff('download-progress')
|
||||||
|
window.runtime.EventsOff('download-complete')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭更新提示
|
||||||
|
*/
|
||||||
|
const closeUpdateNotification = () => {
|
||||||
|
showUpdate.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 返回 ====================
|
||||||
|
return {
|
||||||
|
// 状态
|
||||||
|
updateInfo,
|
||||||
|
showUpdate,
|
||||||
|
checking,
|
||||||
|
downloading,
|
||||||
|
installing,
|
||||||
|
downloadProgress,
|
||||||
|
downloadStatus,
|
||||||
|
progressInfo,
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
checkForUpdates,
|
||||||
|
downloadUpdate,
|
||||||
|
installUpdate,
|
||||||
|
setupEventListeners,
|
||||||
|
removeEventListeners,
|
||||||
|
closeUpdateNotification,
|
||||||
|
formatFileSize,
|
||||||
|
formatSpeed
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 类型定义 ====================
|
||||||
|
interface UpdateInfo {
|
||||||
|
has_update: boolean
|
||||||
|
current_version: string
|
||||||
|
latest_version: string
|
||||||
|
download_url: string
|
||||||
|
changelog: string
|
||||||
|
force_update: boolean
|
||||||
|
release_date: string
|
||||||
|
file_size: number
|
||||||
|
}
|
||||||
@@ -49,3 +49,38 @@ body {
|
|||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: var(--color-border-2, rgba(0, 0, 0, 0.1)) transparent;
|
scrollbar-color: var(--color-border-2, rgba(0, 0, 0, 0.1)) transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Markdown 标题锚点链接样式 */
|
||||||
|
.heading {
|
||||||
|
position: relative;
|
||||||
|
scroll-margin-top: 20px; /* 锚点跳转时的顶部偏移 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading-anchor {
|
||||||
|
opacity: 0;
|
||||||
|
margin-left: 8px;
|
||||||
|
color: rgb(var(--primary-6));
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.8em;
|
||||||
|
font-weight: normal;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading:hover .heading-anchor {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading-anchor:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading-anchor:focus {
|
||||||
|
opacity: 1;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 平滑滚动 */
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
@@ -14,9 +14,13 @@ export interface FileItem {
|
|||||||
/** 文件大小(字节) */
|
/** 文件大小(字节) */
|
||||||
size: number
|
size: number
|
||||||
/** 是否为目录 */
|
/** 是否为目录 */
|
||||||
is_dir: boolean
|
isDir: boolean
|
||||||
/** 修改时间 */
|
/** 修改时间 */
|
||||||
modified_time?: string
|
modified_time?: string
|
||||||
|
/** 是否被收藏(运行时属性) */
|
||||||
|
is_favorite?: boolean
|
||||||
|
/** 旧路径(仅重命名操作时存在) */
|
||||||
|
old_path?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
138
web/src/utils/codeMirrorLoader.js
Normal file
138
web/src/utils/codeMirrorLoader.js
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
/**
|
||||||
|
* CodeMirror 语言包动态加载器
|
||||||
|
* 按需加载语言支持,减少初始包体积和构建时间
|
||||||
|
*/
|
||||||
|
|
||||||
|
const languageCache = new Map()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动态加载 CodeMirror 语言扩展
|
||||||
|
* @param {string} language - 语言名称
|
||||||
|
* @returns {Promise<Extension|null>} CodeMirror 语言扩展
|
||||||
|
*/
|
||||||
|
export async function loadLanguageExtension(language) {
|
||||||
|
if (languageCache.has(language)) {
|
||||||
|
return languageCache.get(language)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let extension
|
||||||
|
|
||||||
|
// 现代语言包(直接返回扩展)
|
||||||
|
const modernLangs = {
|
||||||
|
javascript: ['@codemirror/lang-javascript', 'javascript', { jsx: true }],
|
||||||
|
typescript: ['@codemirror/lang-javascript', 'javascript', { jsx: true }],
|
||||||
|
json: ['@codemirror/lang-json', 'json'],
|
||||||
|
yaml: ['@codemirror/lang-yaml', 'yaml'],
|
||||||
|
html: ['@codemirror/lang-html', 'html'],
|
||||||
|
css: ['@codemirror/lang-css', 'css'],
|
||||||
|
cpp: ['@codemirror/lang-cpp', 'cpp'],
|
||||||
|
c: ['@codemirror/lang-cpp', 'cpp'],
|
||||||
|
rust: ['@codemirror/lang-rust', 'rust'],
|
||||||
|
go: ['@codemirror/lang-go', 'go'],
|
||||||
|
python: ['@codemirror/lang-python', 'python'],
|
||||||
|
php: ['@codemirror/lang-php', 'php'],
|
||||||
|
sql: ['@codemirror/lang-sql', 'sql'],
|
||||||
|
markdown: ['@codemirror/lang-markdown', 'markdown'],
|
||||||
|
java: ['@codemirror/lang-java', 'java']
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modernLangs[language]) {
|
||||||
|
const [path, method, ...args] = modernLangs[language]
|
||||||
|
const mod = await import(path)
|
||||||
|
extension = mod[method](...args)
|
||||||
|
} else {
|
||||||
|
// Legacy 语言包(需要 StreamLanguage 包装)
|
||||||
|
const legacyLangs = {
|
||||||
|
ruby: ['@codemirror/legacy-modes/mode/ruby', 'ruby'],
|
||||||
|
shell: ['@codemirror/legacy-modes/mode/shell', 'shell'],
|
||||||
|
bash: ['@codemirror/legacy-modes/mode/shell', 'shell'],
|
||||||
|
kotlin: ['@codemirror/legacy-modes/mode/clike', 'kotlin'],
|
||||||
|
csharp: ['@codemirror/legacy-modes/mode/clike', 'csharp'],
|
||||||
|
swift: ['@codemirror/legacy-modes/mode/swift', 'swift'],
|
||||||
|
r: ['@codemirror/legacy-modes/mode/r', 'r'],
|
||||||
|
perl: ['@codemirror/legacy-modes/mode/perl', 'perl'],
|
||||||
|
latex: ['@codemirror/legacy-modes/mode/stex', 'stex'],
|
||||||
|
tex: ['@codemirror/legacy-modes/mode/stex', 'stex'],
|
||||||
|
xml: ['@codemirror/legacy-modes/mode/xml', 'xml'],
|
||||||
|
svg: ['@codemirror/legacy-modes/mode/xml', 'xml'],
|
||||||
|
properties: ['@codemirror/legacy-modes/mode/properties', 'properties'],
|
||||||
|
ini: ['@codemirror/legacy-modes/mode/properties', 'properties'],
|
||||||
|
cfg: ['@codemirror/legacy-modes/mode/properties', 'properties'],
|
||||||
|
conf: ['@codemirror/legacy-modes/mode/properties', 'properties'],
|
||||||
|
dockerfile: ['@codemirror/legacy-modes/mode/dockerfile', 'dockerFile'],
|
||||||
|
matlab: ['@codemirror/legacy-modes/mode/octave', 'octave'],
|
||||||
|
octave: ['@codemirror/legacy-modes/mode/octave', 'octave']
|
||||||
|
}
|
||||||
|
|
||||||
|
if (legacyLangs[language]) {
|
||||||
|
const [path, method] = legacyLangs[language]
|
||||||
|
const [modeMod, { StreamLanguage }] = await Promise.all([
|
||||||
|
import(path),
|
||||||
|
import('@codemirror/language')
|
||||||
|
])
|
||||||
|
extension = StreamLanguage.define(modeMod[method])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extension) {
|
||||||
|
languageCache.set(language, extension)
|
||||||
|
}
|
||||||
|
return extension
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[CodeMirror] 加载语言包失败: ${language}`, error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据文件扩展名获取语言名称
|
||||||
|
* @param {string} extension - 文件扩展名
|
||||||
|
* @returns {string} 语言名称
|
||||||
|
*/
|
||||||
|
export function getLanguageFromExtension(extension) {
|
||||||
|
const ext = extension.toLowerCase()
|
||||||
|
|
||||||
|
const langMap = {
|
||||||
|
js: 'javascript', jsx: 'javascript', mjs: 'javascript', cjs: 'javascript',
|
||||||
|
ts: 'typescript', tsx: 'typescript',
|
||||||
|
json: 'json',
|
||||||
|
yaml: 'yaml', yml: 'yaml',
|
||||||
|
xml: 'xml', xhtml: 'xml', svg: 'svg',
|
||||||
|
html: 'html', htm: 'html',
|
||||||
|
css: 'css', scss: 'css', sass: 'css', less: 'css',
|
||||||
|
cpp: 'cpp', c: 'c', cc: 'cpp', cxx: 'cpp', h: 'cpp', hpp: 'cpp', hxx: 'cpp',
|
||||||
|
rust: 'rust', rs: 'rust',
|
||||||
|
go: 'go',
|
||||||
|
python: 'python', py: 'python', pyw: 'python',
|
||||||
|
php: 'php',
|
||||||
|
ruby: 'ruby', rb: 'ruby',
|
||||||
|
perl: 'perl', pl: 'perl', pm: 'perl',
|
||||||
|
shell: 'shell', sh: 'shell', bash: 'shell', zsh: 'shell',
|
||||||
|
bat: 'shell', cmd: 'shell', ps1: 'shell',
|
||||||
|
sql: 'sql',
|
||||||
|
java: 'java',
|
||||||
|
kotlin: 'kotlin', kt: 'kotlin', kts: 'kotlin',
|
||||||
|
csharp: 'csharp', cs: 'csharp', csx: 'csharp',
|
||||||
|
swift: 'swift',
|
||||||
|
markdown: 'markdown', md: 'markdown',
|
||||||
|
r: 'r',
|
||||||
|
matlab: 'matlab', m: 'matlab',
|
||||||
|
latex: 'latex', tex: 'latex',
|
||||||
|
dockerfile: 'dockerfile',
|
||||||
|
makefile: 'makefile', mk: 'makefile', gnumakefile: 'makefile',
|
||||||
|
ini: 'ini', cfg: 'ini', conf: 'ini', properties: 'properties',
|
||||||
|
gitignore: 'gitignore',
|
||||||
|
txt: 'text', text: 'text', log: 'text', csv: 'text'
|
||||||
|
}
|
||||||
|
|
||||||
|
return langMap[ext] || 'text'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预加载常用语言包
|
||||||
|
* 用于在应用启动时预热缓存
|
||||||
|
*/
|
||||||
|
export async function preloadCommonLanguages() {
|
||||||
|
await Promise.all(['javascript', 'json', 'markdown', 'python', 'sql'].map(loadLanguageExtension))
|
||||||
|
}
|
||||||
@@ -73,11 +73,23 @@ export const FILE_EXTENSIONS = {
|
|||||||
CODE: [
|
CODE: [
|
||||||
'js', 'ts', 'jsx', 'tsx', 'cts', 'mts', 'cjs', 'mjs',
|
'js', 'ts', 'jsx', 'tsx', 'cts', 'mts', 'cjs', 'mjs',
|
||||||
'vue', 'py', 'java', 'c', 'cpp', 'h', 'go', 'rs', 'php', 'rb', 'cs', 'swift', 'kt',
|
'vue', 'py', 'java', 'c', 'cpp', 'h', 'go', 'rs', 'php', 'rb', 'cs', 'swift', 'kt',
|
||||||
'scala', 'css', 'scss', 'sass', 'less', 'json', 'xml', 'yaml', 'yml', 'sql', 'sh', 'bat', 'ps1',
|
'scala', 'css', 'scss', 'sass', 'less', 'sql', 'sh', 'bat', 'ps1',
|
||||||
'flow', 'props', 'pch', 'cc', 'cxx', 'hpp', 'hxx', 'tcc', 'defs', 'makefile', 'mk', 'cmake',
|
'flow', 'pch', 'cc', 'cxx', 'hpp', 'hxx', 'tcc', 'defs', 'makefile', 'mk', 'cmake',
|
||||||
'tex', 'm', 'r', 'matlab', 'latex', 'rst', 'adoc'
|
'tex', 'm', 'r', 'matlab', 'latex', 'rst', 'adoc'
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// 配置文件(可编辑的文本格式)
|
||||||
|
CONFIG: [
|
||||||
|
// 数据格式
|
||||||
|
'json', 'xml', 'yaml', 'yml',
|
||||||
|
// 配置文件
|
||||||
|
'toml', 'ini', 'cfg', 'conf',
|
||||||
|
// 环境变量/属性
|
||||||
|
'props', 'env', 'dotenv',
|
||||||
|
// 其他
|
||||||
|
'manifest', 'lock', 'ignore'
|
||||||
|
],
|
||||||
|
|
||||||
// 纯文本文件
|
// 纯文本文件
|
||||||
TEXT: ['txt', 'text', 'log', 'md', 'markdown', 'rst', 'adoc', 'tex', 'msg', 'csv', 'tsv'],
|
TEXT: ['txt', 'text', 'log', 'md', 'markdown', 'rst', 'adoc', 'tex', 'msg', 'csv', 'tsv'],
|
||||||
|
|
||||||
@@ -193,6 +205,17 @@ const initIconMap = () => {
|
|||||||
// 代码文件(通用)
|
// 代码文件(通用)
|
||||||
FILE_EXTENSIONS.CODE.forEach(ext => FILE_ICON_MAP.set(ext, FILE_ICONS.CODE))
|
FILE_EXTENSIONS.CODE.forEach(ext => FILE_ICON_MAP.set(ext, FILE_ICONS.CODE))
|
||||||
|
|
||||||
|
// 配置文件(使用特定图标)
|
||||||
|
const configIcons = {
|
||||||
|
'json': FILE_ICONS.JSON,
|
||||||
|
'xml': FILE_ICONS.XML,
|
||||||
|
'yaml': FILE_ICONS.YAML,
|
||||||
|
'yml': FILE_ICONS.YAML
|
||||||
|
}
|
||||||
|
FILE_EXTENSIONS.CONFIG.forEach(ext => {
|
||||||
|
FILE_ICON_MAP.set(ext, configIcons[ext] || FILE_ICONS.YAML)
|
||||||
|
})
|
||||||
|
|
||||||
// 编程语言特定图标
|
// 编程语言特定图标
|
||||||
const langIcons = {
|
const langIcons = {
|
||||||
// Java
|
// Java
|
||||||
@@ -219,11 +242,6 @@ const initIconMap = () => {
|
|||||||
'scss': FILE_ICONS.CSS,
|
'scss': FILE_ICONS.CSS,
|
||||||
'sass': FILE_ICONS.CSS,
|
'sass': FILE_ICONS.CSS,
|
||||||
'less': FILE_ICONS.CSS,
|
'less': FILE_ICONS.CSS,
|
||||||
// Data
|
|
||||||
'json': FILE_ICONS.JSON,
|
|
||||||
'xml': FILE_ICONS.XML,
|
|
||||||
'yaml': FILE_ICONS.YAML,
|
|
||||||
'yml': FILE_ICONS.YAML,
|
|
||||||
// Shell
|
// Shell
|
||||||
'sh': FILE_ICONS.SHELL,
|
'sh': FILE_ICONS.SHELL,
|
||||||
'bash': FILE_ICONS.SHELL,
|
'bash': FILE_ICONS.SHELL,
|
||||||
|
|||||||
@@ -35,12 +35,13 @@ export const KNOWN_BINARY_TYPES = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文本可编辑类型
|
* 文本可编辑类型(包括代码、配置和文本文件)
|
||||||
* @type {string[]}
|
* @type {string[]}
|
||||||
*/
|
*/
|
||||||
export const TEXT_EDITABLE_TYPES = [
|
export const TEXT_EDITABLE_TYPES = [
|
||||||
...FILE_EXTENSIONS.CODE,
|
...FILE_EXTENSIONS.CODE,
|
||||||
'md', 'markdown', 'txt', 'json', 'xml', 'yaml', 'yml', 'toml', 'ini', 'cfg', 'conf'
|
...FILE_EXTENSIONS.CONFIG,
|
||||||
|
...FILE_EXTENSIONS.TEXT
|
||||||
]
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -133,6 +134,16 @@ export const isTextEditable = (path) => {
|
|||||||
return TEXT_EDITABLE_TYPES.includes(ext)
|
return TEXT_EDITABLE_TYPES.includes(ext)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否为配置文件
|
||||||
|
* @param {string} path - 文件路径
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export const isConfigFile = (path) => {
|
||||||
|
const ext = getExt(path)
|
||||||
|
return FILE_EXTENSIONS.CONFIG.includes(ext)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取文件类型分类
|
* 获取文件类型分类
|
||||||
* @param {string} path - 文件路径
|
* @param {string} path - 文件路径
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
* @description 提供文件相关的通用工具函数,避免代码重复
|
* @description 提供文件相关的通用工具函数,避免代码重复
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { normalizePathSeparators } from './pathHelpers.js'
|
||||||
import { FILE_ICON_MAP, FILE_ICONS, BYTE_UNITS, FILE_SIZE_FORMAT, FILE_EXTENSIONS } from './constants'
|
import { FILE_ICON_MAP, FILE_ICONS, BYTE_UNITS, FILE_SIZE_FORMAT, FILE_EXTENSIONS } from './constants'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -46,11 +47,8 @@ export function formatBytes(bytes) {
|
|||||||
export function getFileName(path) {
|
export function getFileName(path) {
|
||||||
if (!path) return ''
|
if (!path) return ''
|
||||||
|
|
||||||
// 统一分隔符为正斜杠
|
// 后端已统一返回 / 路径,直接分割
|
||||||
const normalizedPath = path.replace(/\\/g, '/')
|
const parts = path.split('/')
|
||||||
|
|
||||||
// 分割路径并取最后一部分
|
|
||||||
const parts = normalizedPath.split('/')
|
|
||||||
|
|
||||||
return parts[parts.length - 1] || path
|
return parts[parts.length - 1] || path
|
||||||
}
|
}
|
||||||
@@ -157,7 +155,7 @@ export function isPdfFile(path) {
|
|||||||
*/
|
*/
|
||||||
export function normalizeFilePath(path, encode = false) {
|
export function normalizeFilePath(path, encode = false) {
|
||||||
if (!path) return ''
|
if (!path) return ''
|
||||||
const normalized = path.replace(/\\/g, '/')
|
const normalized = normalizePathSeparators(path)
|
||||||
|
|
||||||
// 如果需要编码,则使用 encodeURIComponent
|
// 如果需要编码,则使用 encodeURIComponent
|
||||||
if (encode) {
|
if (encode) {
|
||||||
@@ -327,18 +325,17 @@ export function sanitizeFileName(filename, replacement = '_') {
|
|||||||
* @returns {Array} 排序后的文件列表
|
* @returns {Array} 排序后的文件列表
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* sortFileList([{name: 'b.txt', is_dir: false}, {name: 'a', is_dir: true}])
|
* sortFileList([{name: 'b.txt', isDir: false}, {name: 'a', isDir: true}])
|
||||||
* // [{name: 'a', is_dir: true}, {name: 'b.txt', is_dir: false}]
|
* // [{name: 'a', isDir: true}, {name: 'b.txt', isDir: false}]
|
||||||
*/
|
*/
|
||||||
export function sortFileList(fileList) {
|
export function sortFileList(fileList) {
|
||||||
if (!Array.isArray(fileList)) return fileList
|
if (!Array.isArray(fileList)) return fileList
|
||||||
|
|
||||||
return fileList.sort((a, b) => {
|
return fileList.sort((a, b) => {
|
||||||
// 如果都是目录或都是文件,按名称排序
|
// API 层已转换,直接使用 isDir
|
||||||
if (a.is_dir === b.is_dir) {
|
if (a.isDir === b.isDir) {
|
||||||
return a.name.localeCompare(b.name)
|
return a.name.localeCompare(b.name)
|
||||||
}
|
}
|
||||||
// 目录优先
|
return a.isDir ? -1 : 1
|
||||||
return a.is_dir ? -1 : 1
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,67 @@
|
|||||||
import { marked } from 'marked'
|
import { marked } from 'marked'
|
||||||
import hljs from 'highlight.js'
|
import hljs from 'highlight.js'
|
||||||
import mermaid from 'mermaid'
|
|
||||||
|
|
||||||
// 导入 highlight.js 核心和两种主题样式
|
|
||||||
import 'highlight.js/lib/common'
|
import 'highlight.js/lib/common'
|
||||||
import 'highlight.js/styles/github-dark.css'
|
import 'highlight.js/styles/github-dark.css'
|
||||||
import 'highlight.js/styles/github.css'
|
import 'highlight.js/styles/github.css'
|
||||||
|
|
||||||
// Mermaid 初始化
|
let mermaidInstance: typeof import('mermaid').default | null = null
|
||||||
mermaid.initialize({ startOnLoad: false, theme: 'default', securityLevel: 'loose' })
|
|
||||||
|
async function loadMermaid() {
|
||||||
|
if (mermaidInstance) return mermaidInstance
|
||||||
|
|
||||||
|
try {
|
||||||
|
const mermaid = await import('mermaid')
|
||||||
|
mermaid.default.initialize({
|
||||||
|
startOnLoad: false,
|
||||||
|
theme: 'default',
|
||||||
|
securityLevel: 'loose'
|
||||||
|
})
|
||||||
|
mermaidInstance = mermaid.default
|
||||||
|
return mermaidInstance
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 自定义 renderer
|
|
||||||
const renderer = new marked.Renderer()
|
const renderer = new marked.Renderer()
|
||||||
|
|
||||||
renderer.code = function(token: any) {
|
renderer.code = function(token: any) {
|
||||||
// Mermaid 代码块
|
|
||||||
if (token.lang === 'mermaid') {
|
if (token.lang === 'mermaid') {
|
||||||
return `<pre class="mermaid">${token.text}</pre>`
|
return `<pre class="mermaid">${token.text}</pre>`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 普通代码块 - 使用 highlight.js 高亮
|
|
||||||
const lang = hljs.getLanguage(token.lang) ? token.lang : 'plaintext'
|
const lang = hljs.getLanguage(token.lang) ? token.lang : 'plaintext'
|
||||||
const highlighted = hljs.highlight(token.text, { language: lang }).value
|
const highlighted = hljs.highlight(token.text, { language: lang }).value
|
||||||
return `<pre><code class="hljs language-${lang}" data-theme="auto">${highlighted}</code></pre>`
|
return `<pre><code class="hljs language-${lang}" data-theme="auto">${highlighted}</code></pre>`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderer.heading = function(token: any) {
|
||||||
|
const raw = token.raw || ''
|
||||||
|
const depth = token.depth || 1
|
||||||
|
const text = token.text || ''
|
||||||
|
|
||||||
|
const id = raw
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^\u4e00-\u9fa5a-z0-9\s-]/g, '')
|
||||||
|
.trim()
|
||||||
|
.replace(/\s+/g, '-')
|
||||||
|
.replace(/-+/g, '-')
|
||||||
|
.replace(/^-+|-+$/g, '') || `heading-${Math.random().toString(36).slice(2, 11)}`
|
||||||
|
|
||||||
|
return `<h${depth} id="${id}" class="heading">
|
||||||
|
${text}<a href="#${id}" class="heading-anchor" aria-hidden="true" title="跳转到此标题">#</a>
|
||||||
|
</h${depth}>`
|
||||||
|
}
|
||||||
|
|
||||||
marked.use({ renderer, breaks: true, gfm: true })
|
marked.use({ renderer, breaks: true, gfm: true })
|
||||||
|
|
||||||
export { marked }
|
export { marked }
|
||||||
|
|
||||||
export async function renderMermaidDiagrams() {
|
export async function renderMermaidDiagrams() {
|
||||||
await mermaid.run()
|
const mermaid = await loadMermaid()
|
||||||
|
if (mermaid) {
|
||||||
|
await mermaid.run()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -50,18 +50,33 @@ export const getFileName = (path) => {
|
|||||||
* @example
|
* @example
|
||||||
* getParentPath('C:\\Users\\file.txt') // 'C:\\Users'
|
* getParentPath('C:\\Users\\file.txt') // 'C:\\Users'
|
||||||
* getParentPath('/home/user/file.txt') // '/home/user'
|
* getParentPath('/home/user/file.txt') // '/home/user'
|
||||||
|
* getParentPath('E:/file.txt') // 'E:/'
|
||||||
*/
|
*/
|
||||||
export const getParentPath = (path) => {
|
export const getParentPath = (path) => {
|
||||||
if (!path) return ''
|
if (!path) return ''
|
||||||
|
|
||||||
// 查找最后一个分隔符的位置
|
// 规范化路径分隔符
|
||||||
const lastSep = Math.max(
|
const normalizedPath = path.replace(/\\/g, '/')
|
||||||
path.lastIndexOf('/'),
|
|
||||||
path.lastIndexOf('\\')
|
|
||||||
)
|
|
||||||
|
|
||||||
if (lastSep <= 0) return path
|
// 查找最后一个分隔符的位置
|
||||||
return path.substring(0, lastSep)
|
const lastSep = normalizedPath.lastIndexOf('/')
|
||||||
|
|
||||||
|
if (lastSep <= 0) {
|
||||||
|
// 没有分隔符或分隔符在开头,返回根目录(对于盘符情况)
|
||||||
|
if (/^[A-Za-z]:$/.test(normalizedPath)) {
|
||||||
|
return normalizedPath + '/' // E: 转换为 E:/
|
||||||
|
}
|
||||||
|
return normalizedPath
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentPath = normalizedPath.substring(0, lastSep)
|
||||||
|
|
||||||
|
// 特殊处理:如果是盘符根目录下的文件(E:/file.txt -> E:/)
|
||||||
|
if (/^[A-Za-z]:$/.test(parentPath)) {
|
||||||
|
return parentPath + '/' // 确保根目录带斜杠
|
||||||
|
}
|
||||||
|
|
||||||
|
return parentPath || '/'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -747,14 +747,6 @@ const updateResultTableHeight = () => {
|
|||||||
const maxHeight = availableHeight > 0 ? availableHeight : 400
|
const maxHeight = availableHeight > 0 ? availableHeight : 400
|
||||||
|
|
||||||
tableScrollHeight.value = Math.max(minHeight, maxHeight)
|
tableScrollHeight.value = Math.max(minHeight, maxHeight)
|
||||||
|
|
||||||
console.log('表格高度计算:', {
|
|
||||||
containerHeight,
|
|
||||||
paginationHeight,
|
|
||||||
tableHeaderHeight,
|
|
||||||
availableHeight,
|
|
||||||
final: tableScrollHeight.value
|
|
||||||
})
|
|
||||||
}, 150)
|
}, 150)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,11 +34,8 @@ export function useStructureState() {
|
|||||||
dbType: 'mysql' | 'mongo' | 'redis',
|
dbType: 'mysql' | 'mongo' | 'redis',
|
||||||
nodeType: string
|
nodeType: string
|
||||||
) => {
|
) => {
|
||||||
console.log('🟢 loadStructure 开始:', { connectionId, database, tableName, dbType, nodeType })
|
|
||||||
|
|
||||||
// 对于连接和数据库节点,不需要加载结构
|
// 对于连接和数据库节点,不需要加载结构
|
||||||
if (nodeType === 'connection' || nodeType === 'database') {
|
if (nodeType === 'connection' || nodeType === 'database') {
|
||||||
console.log('🟡 跳过:节点类型为连接或数据库')
|
|
||||||
structureInfo.value = {
|
structureInfo.value = {
|
||||||
connectionId,
|
connectionId,
|
||||||
database,
|
database,
|
||||||
@@ -52,7 +49,6 @@ export function useStructureState() {
|
|||||||
|
|
||||||
// 如果没有表名,不加载(但保留 structureInfo 用于显示提示)
|
// 如果没有表名,不加载(但保留 structureInfo 用于显示提示)
|
||||||
if (!tableName) {
|
if (!tableName) {
|
||||||
console.log('🟡 跳过:表名为空')
|
|
||||||
structureInfo.value = {
|
structureInfo.value = {
|
||||||
connectionId,
|
connectionId,
|
||||||
database,
|
database,
|
||||||
@@ -78,12 +74,6 @@ export function useStructureState() {
|
|||||||
tableName
|
tableName
|
||||||
)
|
)
|
||||||
|
|
||||||
console.log('表结构加载成功:', { connectionId, database, tableName, result })
|
|
||||||
console.log('返回数据类型:', typeof result)
|
|
||||||
console.log('返回数据 keys:', result ? Object.keys(result) : 'null')
|
|
||||||
console.log('返回数据 type 字段:', result?.type)
|
|
||||||
console.log('返回数据 columns 字段:', result?.columns)
|
|
||||||
|
|
||||||
structureData.value = result
|
structureData.value = result
|
||||||
|
|
||||||
// 确保 structureInfo 也设置了
|
// 确保 structureInfo 也设置了
|
||||||
@@ -94,20 +84,6 @@ export function useStructureState() {
|
|||||||
dbType,
|
dbType,
|
||||||
nodeType
|
nodeType
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确保 structureInfo 也设置了
|
|
||||||
structureInfo.value = {
|
|
||||||
connectionId,
|
|
||||||
database,
|
|
||||||
tableName,
|
|
||||||
dbType,
|
|
||||||
nodeType
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('✅ 设置完成 - structureData:', structureData.value)
|
|
||||||
console.log('✅ 设置完成 - structureInfo:', structureInfo.value)
|
|
||||||
console.log('✅ structureData 是否为 null:', structureData.value === null)
|
|
||||||
console.log('✅ structureInfo 是否为 null:', structureInfo.value === null)
|
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
console.error('加载表结构失败:', error)
|
console.error('加载表结构失败:', error)
|
||||||
const errorMessage = error instanceof Error ? error.message : '加载表结构失败'
|
const errorMessage = error instanceof Error ? error.message : '加载表结构失败'
|
||||||
|
|||||||
131
web/src/wailsjs/wailsjs/go/main/App.d.ts
vendored
Normal file
131
web/src/wailsjs/wailsjs/go/main/App.d.ts
vendored
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
import {filesystem} from '../models';
|
||||||
|
import {main} from '../models';
|
||||||
|
import {api} from '../models';
|
||||||
|
|
||||||
|
export function CheckUpdate():Promise<Record<string, any>>;
|
||||||
|
|
||||||
|
export function ClearCache():Promise<void>;
|
||||||
|
|
||||||
|
export function CreateDir(arg1:string):Promise<filesystem.FileOperationResult>;
|
||||||
|
|
||||||
|
export function CreateFile(arg1:string):Promise<filesystem.FileOperationResult>;
|
||||||
|
|
||||||
|
export function DeleteDbConnection(arg1:number):Promise<void>;
|
||||||
|
|
||||||
|
export function DeletePath(arg1:string):Promise<filesystem.FileOperationResult>;
|
||||||
|
|
||||||
|
export function DeletePermanently(arg1:string):Promise<void>;
|
||||||
|
|
||||||
|
export function DeleteResultHistory(arg1:number):Promise<void>;
|
||||||
|
|
||||||
|
export function DetectFileTypeByContent(arg1:string):Promise<Record<string, any>>;
|
||||||
|
|
||||||
|
export function DownloadUpdate(arg1:string):Promise<Record<string, any>>;
|
||||||
|
|
||||||
|
export function EmptyRecycleBin():Promise<void>;
|
||||||
|
|
||||||
|
export function ExecuteSQL(arg1:number,arg2:string,arg3:string):Promise<Record<string, any>>;
|
||||||
|
|
||||||
|
export function ExtractFileFromZip(arg1:string,arg2:string):Promise<string>;
|
||||||
|
|
||||||
|
export function ExtractFileFromZipToTemp(arg1:string,arg2:string):Promise<string>;
|
||||||
|
|
||||||
|
export function GetAppConfig():Promise<Record<string, any>>;
|
||||||
|
|
||||||
|
export function GetAuditLogs(arg1:number):Promise<Array<Record<string, any>>>;
|
||||||
|
|
||||||
|
export function GetCPUInfo():Promise<Record<string, any>>;
|
||||||
|
|
||||||
|
export function GetCommonPaths():Promise<Record<string, string>>;
|
||||||
|
|
||||||
|
export function GetCurrentVersion():Promise<Record<string, any>>;
|
||||||
|
|
||||||
|
export function GetDatabases(arg1:number):Promise<Array<string>>;
|
||||||
|
|
||||||
|
export function GetDiskInfo():Promise<Array<Record<string, any>>>;
|
||||||
|
|
||||||
|
export function GetEnvVars():Promise<Record<string, string>>;
|
||||||
|
|
||||||
|
export function GetFileInfo(arg1:string):Promise<Record<string, any>>;
|
||||||
|
|
||||||
|
export function GetFileServerURL():Promise<string>;
|
||||||
|
|
||||||
|
export function GetIndexes(arg1:number,arg2:string,arg3:string):Promise<Array<Record<string, any>>>;
|
||||||
|
|
||||||
|
export function GetMemoryInfo():Promise<Record<string, any>>;
|
||||||
|
|
||||||
|
export function GetRecycleBinEntries():Promise<Array<Record<string, any>>>;
|
||||||
|
|
||||||
|
export function GetResultHistory(arg1:any,arg2:string,arg3:number,arg4:number):Promise<Record<string, any>>;
|
||||||
|
|
||||||
|
export function GetResultHistoryByID(arg1:number):Promise<Record<string, any>>;
|
||||||
|
|
||||||
|
export function GetSystemInfo():Promise<Record<string, any>>;
|
||||||
|
|
||||||
|
export function GetTableStructure(arg1:number,arg2:string,arg3:string):Promise<Record<string, any>>;
|
||||||
|
|
||||||
|
export function GetTables(arg1:number,arg2:string):Promise<Array<string>>;
|
||||||
|
|
||||||
|
export function GetUpdateConfig():Promise<Record<string, any>>;
|
||||||
|
|
||||||
|
export function GetZipFileInfo(arg1:string,arg2:string):Promise<Record<string, any>>;
|
||||||
|
|
||||||
|
export function Greet(arg1:string):Promise<string>;
|
||||||
|
|
||||||
|
export function InstallUpdate(arg1:string,arg2:boolean):Promise<Record<string, any>>;
|
||||||
|
|
||||||
|
export function InstallUpdateWithHash(arg1:string,arg2:boolean,arg3:string,arg4:string):Promise<Record<string, any>>;
|
||||||
|
|
||||||
|
export function ListDbConnections():Promise<Array<Record<string, any>>>;
|
||||||
|
|
||||||
|
export function ListDir(arg1:string):Promise<Array<Record<string, any>>>;
|
||||||
|
|
||||||
|
export function ListSqlTabs():Promise<Array<Record<string, any>>>;
|
||||||
|
|
||||||
|
export function ListZipContents(arg1:string):Promise<Array<Record<string, any>>>;
|
||||||
|
|
||||||
|
export function OpenPath(arg1:string):Promise<void>;
|
||||||
|
|
||||||
|
export function PreviewTableStructure(arg1:number,arg2:string,arg3:string,arg4:Record<string, any>):Promise<Array<string>>;
|
||||||
|
|
||||||
|
export function QueryUsers(arg1:string,arg2:number,arg3:number,arg4:number,arg5:number,arg6:number,arg7:string,arg8:string):Promise<Record<string, any>>;
|
||||||
|
|
||||||
|
export function ReadFile(arg1:string):Promise<string>;
|
||||||
|
|
||||||
|
export function Reload():Promise<void>;
|
||||||
|
|
||||||
|
export function RenamePath(arg1:main.RenamePathRequest):Promise<filesystem.FileOperationResult>;
|
||||||
|
|
||||||
|
export function ResolveShortcut(arg1:string):Promise<Record<string, any>>;
|
||||||
|
|
||||||
|
export function RestoreFromRecycleBin(arg1:string):Promise<void>;
|
||||||
|
|
||||||
|
export function SaveAppConfig(arg1:main.SaveAppConfigRequest):Promise<Record<string, any>>;
|
||||||
|
|
||||||
|
export function SaveDbConnection(arg1:api.SaveConnectionRequest):Promise<void>;
|
||||||
|
|
||||||
|
export function SaveResult(arg1:number,arg2:string,arg3:string,arg4:string,arg5:any,arg6:Array<string>,arg7:number,arg8:number):Promise<Record<string, any>>;
|
||||||
|
|
||||||
|
export function SaveSqlTabs(arg1:Array<Record<string, any>>):Promise<void>;
|
||||||
|
|
||||||
|
export function SetUpdateConfig(arg1:boolean,arg2:number,arg3:string):Promise<Record<string, any>>;
|
||||||
|
|
||||||
|
export function TestDbConnection(arg1:number):Promise<void>;
|
||||||
|
|
||||||
|
export function TestDbConnectionWithParams(arg1:api.TestConnectionRequest):Promise<void>;
|
||||||
|
|
||||||
|
export function UpdateTableStructure(arg1:number,arg2:string,arg3:string,arg4:Record<string, any>):Promise<Array<string>>;
|
||||||
|
|
||||||
|
export function VerifyUpdateFile(arg1:string,arg2:string,arg3:string):Promise<Record<string, any>>;
|
||||||
|
|
||||||
|
export function WindowClose():Promise<void>;
|
||||||
|
|
||||||
|
export function WindowIsMaximized():Promise<boolean>;
|
||||||
|
|
||||||
|
export function WindowMaximize():Promise<void>;
|
||||||
|
|
||||||
|
export function WindowMinimize():Promise<void>;
|
||||||
|
|
||||||
|
export function WriteFile(arg1:main.WriteFileRequest):Promise<void>;
|
||||||
255
web/src/wailsjs/wailsjs/go/main/App.js
Normal file
255
web/src/wailsjs/wailsjs/go/main/App.js
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
// @ts-check
|
||||||
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
|
export function CheckUpdate() {
|
||||||
|
return window['go']['main']['App']['CheckUpdate']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ClearCache() {
|
||||||
|
return window['go']['main']['App']['ClearCache']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CreateDir(arg1) {
|
||||||
|
return window['go']['main']['App']['CreateDir'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CreateFile(arg1) {
|
||||||
|
return window['go']['main']['App']['CreateFile'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeleteDbConnection(arg1) {
|
||||||
|
return window['go']['main']['App']['DeleteDbConnection'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeletePath(arg1) {
|
||||||
|
return window['go']['main']['App']['DeletePath'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeletePermanently(arg1) {
|
||||||
|
return window['go']['main']['App']['DeletePermanently'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeleteResultHistory(arg1) {
|
||||||
|
return window['go']['main']['App']['DeleteResultHistory'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DetectFileTypeByContent(arg1) {
|
||||||
|
return window['go']['main']['App']['DetectFileTypeByContent'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DownloadUpdate(arg1) {
|
||||||
|
return window['go']['main']['App']['DownloadUpdate'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EmptyRecycleBin() {
|
||||||
|
return window['go']['main']['App']['EmptyRecycleBin']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ExecuteSQL(arg1, arg2, arg3) {
|
||||||
|
return window['go']['main']['App']['ExecuteSQL'](arg1, arg2, arg3);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ExtractFileFromZip(arg1, arg2) {
|
||||||
|
return window['go']['main']['App']['ExtractFileFromZip'](arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ExtractFileFromZipToTemp(arg1, arg2) {
|
||||||
|
return window['go']['main']['App']['ExtractFileFromZipToTemp'](arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetAppConfig() {
|
||||||
|
return window['go']['main']['App']['GetAppConfig']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetAuditLogs(arg1) {
|
||||||
|
return window['go']['main']['App']['GetAuditLogs'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetCPUInfo() {
|
||||||
|
return window['go']['main']['App']['GetCPUInfo']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetCommonPaths() {
|
||||||
|
return window['go']['main']['App']['GetCommonPaths']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetCurrentVersion() {
|
||||||
|
return window['go']['main']['App']['GetCurrentVersion']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetDatabases(arg1) {
|
||||||
|
return window['go']['main']['App']['GetDatabases'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetDiskInfo() {
|
||||||
|
return window['go']['main']['App']['GetDiskInfo']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetEnvVars() {
|
||||||
|
return window['go']['main']['App']['GetEnvVars']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetFileInfo(arg1) {
|
||||||
|
return window['go']['main']['App']['GetFileInfo'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetFileServerURL() {
|
||||||
|
return window['go']['main']['App']['GetFileServerURL']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetIndexes(arg1, arg2, arg3) {
|
||||||
|
return window['go']['main']['App']['GetIndexes'](arg1, arg2, arg3);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetMemoryInfo() {
|
||||||
|
return window['go']['main']['App']['GetMemoryInfo']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetRecycleBinEntries() {
|
||||||
|
return window['go']['main']['App']['GetRecycleBinEntries']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetResultHistory(arg1, arg2, arg3, arg4) {
|
||||||
|
return window['go']['main']['App']['GetResultHistory'](arg1, arg2, arg3, arg4);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetResultHistoryByID(arg1) {
|
||||||
|
return window['go']['main']['App']['GetResultHistoryByID'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetSystemInfo() {
|
||||||
|
return window['go']['main']['App']['GetSystemInfo']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetTableStructure(arg1, arg2, arg3) {
|
||||||
|
return window['go']['main']['App']['GetTableStructure'](arg1, arg2, arg3);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetTables(arg1, arg2) {
|
||||||
|
return window['go']['main']['App']['GetTables'](arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetUpdateConfig() {
|
||||||
|
return window['go']['main']['App']['GetUpdateConfig']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetZipFileInfo(arg1, arg2) {
|
||||||
|
return window['go']['main']['App']['GetZipFileInfo'](arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Greet(arg1) {
|
||||||
|
return window['go']['main']['App']['Greet'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InstallUpdate(arg1, arg2) {
|
||||||
|
return window['go']['main']['App']['InstallUpdate'](arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InstallUpdateWithHash(arg1, arg2, arg3, arg4) {
|
||||||
|
return window['go']['main']['App']['InstallUpdateWithHash'](arg1, arg2, arg3, arg4);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ListDbConnections() {
|
||||||
|
return window['go']['main']['App']['ListDbConnections']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ListDir(arg1) {
|
||||||
|
return window['go']['main']['App']['ListDir'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ListSqlTabs() {
|
||||||
|
return window['go']['main']['App']['ListSqlTabs']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ListZipContents(arg1) {
|
||||||
|
return window['go']['main']['App']['ListZipContents'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function OpenPath(arg1) {
|
||||||
|
return window['go']['main']['App']['OpenPath'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PreviewTableStructure(arg1, arg2, arg3, arg4) {
|
||||||
|
return window['go']['main']['App']['PreviewTableStructure'](arg1, arg2, arg3, arg4);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function QueryUsers(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8) {
|
||||||
|
return window['go']['main']['App']['QueryUsers'](arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ReadFile(arg1) {
|
||||||
|
return window['go']['main']['App']['ReadFile'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Reload() {
|
||||||
|
return window['go']['main']['App']['Reload']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RenamePath(arg1) {
|
||||||
|
return window['go']['main']['App']['RenamePath'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ResolveShortcut(arg1) {
|
||||||
|
return window['go']['main']['App']['ResolveShortcut'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RestoreFromRecycleBin(arg1) {
|
||||||
|
return window['go']['main']['App']['RestoreFromRecycleBin'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SaveAppConfig(arg1) {
|
||||||
|
return window['go']['main']['App']['SaveAppConfig'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SaveDbConnection(arg1) {
|
||||||
|
return window['go']['main']['App']['SaveDbConnection'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SaveResult(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8) {
|
||||||
|
return window['go']['main']['App']['SaveResult'](arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SaveSqlTabs(arg1) {
|
||||||
|
return window['go']['main']['App']['SaveSqlTabs'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SetUpdateConfig(arg1, arg2, arg3) {
|
||||||
|
return window['go']['main']['App']['SetUpdateConfig'](arg1, arg2, arg3);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TestDbConnection(arg1) {
|
||||||
|
return window['go']['main']['App']['TestDbConnection'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TestDbConnectionWithParams(arg1) {
|
||||||
|
return window['go']['main']['App']['TestDbConnectionWithParams'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UpdateTableStructure(arg1, arg2, arg3, arg4) {
|
||||||
|
return window['go']['main']['App']['UpdateTableStructure'](arg1, arg2, arg3, arg4);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VerifyUpdateFile(arg1, arg2, arg3) {
|
||||||
|
return window['go']['main']['App']['VerifyUpdateFile'](arg1, arg2, arg3);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowClose() {
|
||||||
|
return window['go']['main']['App']['WindowClose']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowIsMaximized() {
|
||||||
|
return window['go']['main']['App']['WindowIsMaximized']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowMaximize() {
|
||||||
|
return window['go']['main']['App']['WindowMaximize']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowMinimize() {
|
||||||
|
return window['go']['main']['App']['WindowMinimize']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WriteFile(arg1) {
|
||||||
|
return window['go']['main']['App']['WriteFile'](arg1);
|
||||||
|
}
|
||||||
177
web/src/wailsjs/wailsjs/go/models.ts
Normal file
177
web/src/wailsjs/wailsjs/go/models.ts
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
export namespace api {
|
||||||
|
|
||||||
|
export class AppTabDefinition {
|
||||||
|
key: string;
|
||||||
|
title: string;
|
||||||
|
visible: boolean;
|
||||||
|
enabled: boolean;
|
||||||
|
|
||||||
|
static createFrom(source: any = {}) {
|
||||||
|
return new AppTabDefinition(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(source: any = {}) {
|
||||||
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
|
this.key = source["key"];
|
||||||
|
this.title = source["title"];
|
||||||
|
this.visible = source["visible"];
|
||||||
|
this.enabled = source["enabled"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class SaveConnectionRequest {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
database: string;
|
||||||
|
options: string;
|
||||||
|
|
||||||
|
static createFrom(source: any = {}) {
|
||||||
|
return new SaveConnectionRequest(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(source: any = {}) {
|
||||||
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
|
this.id = source["id"];
|
||||||
|
this.name = source["name"];
|
||||||
|
this.type = source["type"];
|
||||||
|
this.host = source["host"];
|
||||||
|
this.port = source["port"];
|
||||||
|
this.username = source["username"];
|
||||||
|
this.password = source["password"];
|
||||||
|
this.database = source["database"];
|
||||||
|
this.options = source["options"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class TestConnectionRequest {
|
||||||
|
id: number;
|
||||||
|
type: string;
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
database: string;
|
||||||
|
options: string;
|
||||||
|
|
||||||
|
static createFrom(source: any = {}) {
|
||||||
|
return new TestConnectionRequest(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(source: any = {}) {
|
||||||
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
|
this.id = source["id"];
|
||||||
|
this.type = source["type"];
|
||||||
|
this.host = source["host"];
|
||||||
|
this.port = source["port"];
|
||||||
|
this.username = source["username"];
|
||||||
|
this.password = source["password"];
|
||||||
|
this.database = source["database"];
|
||||||
|
this.options = source["options"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace filesystem {
|
||||||
|
|
||||||
|
export class FileOperationResult {
|
||||||
|
path: string;
|
||||||
|
name: string;
|
||||||
|
size: number;
|
||||||
|
size_str?: string;
|
||||||
|
is_dir: boolean;
|
||||||
|
mod_time?: string;
|
||||||
|
mode?: string;
|
||||||
|
old_path?: string;
|
||||||
|
deleted?: boolean;
|
||||||
|
|
||||||
|
static createFrom(source: any = {}) {
|
||||||
|
return new FileOperationResult(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(source: any = {}) {
|
||||||
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
|
this.path = source["path"];
|
||||||
|
this.name = source["name"];
|
||||||
|
this.size = source["size"];
|
||||||
|
this.size_str = source["size_str"];
|
||||||
|
this.is_dir = source["is_dir"];
|
||||||
|
this.mod_time = source["mod_time"];
|
||||||
|
this.mode = source["mode"];
|
||||||
|
this.old_path = source["old_path"];
|
||||||
|
this.deleted = source["deleted"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace main {
|
||||||
|
|
||||||
|
export class RenamePathRequest {
|
||||||
|
oldPath: string;
|
||||||
|
newPath: string;
|
||||||
|
|
||||||
|
static createFrom(source: any = {}) {
|
||||||
|
return new RenamePathRequest(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(source: any = {}) {
|
||||||
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
|
this.oldPath = source["oldPath"];
|
||||||
|
this.newPath = source["newPath"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class SaveAppConfigRequest {
|
||||||
|
tabs: api.AppTabDefinition[];
|
||||||
|
visibleTabs: string[];
|
||||||
|
defaultTab: string;
|
||||||
|
|
||||||
|
static createFrom(source: any = {}) {
|
||||||
|
return new SaveAppConfigRequest(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(source: any = {}) {
|
||||||
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
|
this.tabs = this.convertValues(source["tabs"], api.AppTabDefinition);
|
||||||
|
this.visibleTabs = source["visibleTabs"];
|
||||||
|
this.defaultTab = source["defaultTab"];
|
||||||
|
}
|
||||||
|
|
||||||
|
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||||
|
if (!a) {
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
if (a.slice && a.map) {
|
||||||
|
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||||
|
} else if ("object" === typeof a) {
|
||||||
|
if (asMap) {
|
||||||
|
for (const key of Object.keys(a)) {
|
||||||
|
a[key] = new classs(a[key]);
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
return new classs(a);
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class WriteFileRequest {
|
||||||
|
path: string;
|
||||||
|
content: string;
|
||||||
|
|
||||||
|
static createFrom(source: any = {}) {
|
||||||
|
return new WriteFileRequest(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(source: any = {}) {
|
||||||
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
|
this.path = source["path"];
|
||||||
|
this.content = source["content"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
24
web/src/wailsjs/wailsjs/runtime/package.json
Normal file
24
web/src/wailsjs/wailsjs/runtime/package.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "@wailsapp/runtime",
|
||||||
|
"version": "2.0.0",
|
||||||
|
"description": "Wails Javascript runtime library",
|
||||||
|
"main": "runtime.js",
|
||||||
|
"types": "runtime.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/wailsapp/wails.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"Wails",
|
||||||
|
"Javascript",
|
||||||
|
"Go"
|
||||||
|
],
|
||||||
|
"author": "Lea Anthony <lea.anthony@gmail.com>",
|
||||||
|
"license": "MIT",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/wailsapp/wails/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/wailsapp/wails#readme"
|
||||||
|
}
|
||||||
249
web/src/wailsjs/wailsjs/runtime/runtime.d.ts
vendored
Normal file
249
web/src/wailsjs/wailsjs/runtime/runtime.d.ts
vendored
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
/*
|
||||||
|
_ __ _ __
|
||||||
|
| | / /___ _(_) /____
|
||||||
|
| | /| / / __ `/ / / ___/
|
||||||
|
| |/ |/ / /_/ / / (__ )
|
||||||
|
|__/|__/\__,_/_/_/____/
|
||||||
|
The electron alternative for Go
|
||||||
|
(c) Lea Anthony 2019-present
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface Position {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Size {
|
||||||
|
w: number;
|
||||||
|
h: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Screen {
|
||||||
|
isCurrent: boolean;
|
||||||
|
isPrimary: boolean;
|
||||||
|
width : number
|
||||||
|
height : number
|
||||||
|
}
|
||||||
|
|
||||||
|
// Environment information such as platform, buildtype, ...
|
||||||
|
export interface EnvironmentInfo {
|
||||||
|
buildType: string;
|
||||||
|
platform: string;
|
||||||
|
arch: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit)
|
||||||
|
// emits the given event. Optional data may be passed with the event.
|
||||||
|
// This will trigger any event listeners.
|
||||||
|
export function EventsEmit(eventName: string, ...data: any): void;
|
||||||
|
|
||||||
|
// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.
|
||||||
|
export function EventsOn(eventName: string, callback: (...data: any) => void): () => void;
|
||||||
|
|
||||||
|
// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)
|
||||||
|
// sets up a listener for the given event name, but will only trigger a given number times.
|
||||||
|
export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void;
|
||||||
|
|
||||||
|
// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)
|
||||||
|
// sets up a listener for the given event name, but will only trigger once.
|
||||||
|
export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void;
|
||||||
|
|
||||||
|
// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff)
|
||||||
|
// unregisters the listener for the given event name.
|
||||||
|
export function EventsOff(eventName: string, ...additionalEventNames: string[]): void;
|
||||||
|
|
||||||
|
// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
|
||||||
|
// unregisters all listeners.
|
||||||
|
export function EventsOffAll(): void;
|
||||||
|
|
||||||
|
// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
|
||||||
|
// logs the given message as a raw message
|
||||||
|
export function LogPrint(message: string): void;
|
||||||
|
|
||||||
|
// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace)
|
||||||
|
// logs the given message at the `trace` log level.
|
||||||
|
export function LogTrace(message: string): void;
|
||||||
|
|
||||||
|
// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug)
|
||||||
|
// logs the given message at the `debug` log level.
|
||||||
|
export function LogDebug(message: string): void;
|
||||||
|
|
||||||
|
// [LogError](https://wails.io/docs/reference/runtime/log#logerror)
|
||||||
|
// logs the given message at the `error` log level.
|
||||||
|
export function LogError(message: string): void;
|
||||||
|
|
||||||
|
// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal)
|
||||||
|
// logs the given message at the `fatal` log level.
|
||||||
|
// The application will quit after calling this method.
|
||||||
|
export function LogFatal(message: string): void;
|
||||||
|
|
||||||
|
// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo)
|
||||||
|
// logs the given message at the `info` log level.
|
||||||
|
export function LogInfo(message: string): void;
|
||||||
|
|
||||||
|
// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning)
|
||||||
|
// logs the given message at the `warning` log level.
|
||||||
|
export function LogWarning(message: string): void;
|
||||||
|
|
||||||
|
// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload)
|
||||||
|
// Forces a reload by the main application as well as connected browsers.
|
||||||
|
export function WindowReload(): void;
|
||||||
|
|
||||||
|
// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp)
|
||||||
|
// Reloads the application frontend.
|
||||||
|
export function WindowReloadApp(): void;
|
||||||
|
|
||||||
|
// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop)
|
||||||
|
// Sets the window AlwaysOnTop or not on top.
|
||||||
|
export function WindowSetAlwaysOnTop(b: boolean): void;
|
||||||
|
|
||||||
|
// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme)
|
||||||
|
// *Windows only*
|
||||||
|
// Sets window theme to system default (dark/light).
|
||||||
|
export function WindowSetSystemDefaultTheme(): void;
|
||||||
|
|
||||||
|
// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme)
|
||||||
|
// *Windows only*
|
||||||
|
// Sets window to light theme.
|
||||||
|
export function WindowSetLightTheme(): void;
|
||||||
|
|
||||||
|
// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme)
|
||||||
|
// *Windows only*
|
||||||
|
// Sets window to dark theme.
|
||||||
|
export function WindowSetDarkTheme(): void;
|
||||||
|
|
||||||
|
// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter)
|
||||||
|
// Centers the window on the monitor the window is currently on.
|
||||||
|
export function WindowCenter(): void;
|
||||||
|
|
||||||
|
// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle)
|
||||||
|
// Sets the text in the window title bar.
|
||||||
|
export function WindowSetTitle(title: string): void;
|
||||||
|
|
||||||
|
// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen)
|
||||||
|
// Makes the window full screen.
|
||||||
|
export function WindowFullscreen(): void;
|
||||||
|
|
||||||
|
// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen)
|
||||||
|
// Restores the previous window dimensions and position prior to full screen.
|
||||||
|
export function WindowUnfullscreen(): void;
|
||||||
|
|
||||||
|
// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen)
|
||||||
|
// Returns the state of the window, i.e. whether the window is in full screen mode or not.
|
||||||
|
export function WindowIsFullscreen(): Promise<boolean>;
|
||||||
|
|
||||||
|
// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
|
||||||
|
// Sets the width and height of the window.
|
||||||
|
export function WindowSetSize(width: number, height: number): void;
|
||||||
|
|
||||||
|
// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
|
||||||
|
// Gets the width and height of the window.
|
||||||
|
export function WindowGetSize(): Promise<Size>;
|
||||||
|
|
||||||
|
// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize)
|
||||||
|
// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions.
|
||||||
|
// Setting a size of 0,0 will disable this constraint.
|
||||||
|
export function WindowSetMaxSize(width: number, height: number): void;
|
||||||
|
|
||||||
|
// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize)
|
||||||
|
// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions.
|
||||||
|
// Setting a size of 0,0 will disable this constraint.
|
||||||
|
export function WindowSetMinSize(width: number, height: number): void;
|
||||||
|
|
||||||
|
// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition)
|
||||||
|
// Sets the window position relative to the monitor the window is currently on.
|
||||||
|
export function WindowSetPosition(x: number, y: number): void;
|
||||||
|
|
||||||
|
// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition)
|
||||||
|
// Gets the window position relative to the monitor the window is currently on.
|
||||||
|
export function WindowGetPosition(): Promise<Position>;
|
||||||
|
|
||||||
|
// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide)
|
||||||
|
// Hides the window.
|
||||||
|
export function WindowHide(): void;
|
||||||
|
|
||||||
|
// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow)
|
||||||
|
// Shows the window, if it is currently hidden.
|
||||||
|
export function WindowShow(): void;
|
||||||
|
|
||||||
|
// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise)
|
||||||
|
// Maximises the window to fill the screen.
|
||||||
|
export function WindowMaximise(): void;
|
||||||
|
|
||||||
|
// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise)
|
||||||
|
// Toggles between Maximised and UnMaximised.
|
||||||
|
export function WindowToggleMaximise(): void;
|
||||||
|
|
||||||
|
// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise)
|
||||||
|
// Restores the window to the dimensions and position prior to maximising.
|
||||||
|
export function WindowUnmaximise(): void;
|
||||||
|
|
||||||
|
// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised)
|
||||||
|
// Returns the state of the window, i.e. whether the window is maximised or not.
|
||||||
|
export function WindowIsMaximised(): Promise<boolean>;
|
||||||
|
|
||||||
|
// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)
|
||||||
|
// Minimises the window.
|
||||||
|
export function WindowMinimise(): void;
|
||||||
|
|
||||||
|
// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise)
|
||||||
|
// Restores the window to the dimensions and position prior to minimising.
|
||||||
|
export function WindowUnminimise(): void;
|
||||||
|
|
||||||
|
// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised)
|
||||||
|
// Returns the state of the window, i.e. whether the window is minimised or not.
|
||||||
|
export function WindowIsMinimised(): Promise<boolean>;
|
||||||
|
|
||||||
|
// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal)
|
||||||
|
// Returns the state of the window, i.e. whether the window is normal or not.
|
||||||
|
export function WindowIsNormal(): Promise<boolean>;
|
||||||
|
|
||||||
|
// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)
|
||||||
|
// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.
|
||||||
|
export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;
|
||||||
|
|
||||||
|
// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall)
|
||||||
|
// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.
|
||||||
|
export function ScreenGetAll(): Promise<Screen[]>;
|
||||||
|
|
||||||
|
// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl)
|
||||||
|
// Opens the given URL in the system browser.
|
||||||
|
export function BrowserOpenURL(url: string): void;
|
||||||
|
|
||||||
|
// [Environment](https://wails.io/docs/reference/runtime/intro#environment)
|
||||||
|
// Returns information about the environment
|
||||||
|
export function Environment(): Promise<EnvironmentInfo>;
|
||||||
|
|
||||||
|
// [Quit](https://wails.io/docs/reference/runtime/intro#quit)
|
||||||
|
// Quits the application.
|
||||||
|
export function Quit(): void;
|
||||||
|
|
||||||
|
// [Hide](https://wails.io/docs/reference/runtime/intro#hide)
|
||||||
|
// Hides the application.
|
||||||
|
export function Hide(): void;
|
||||||
|
|
||||||
|
// [Show](https://wails.io/docs/reference/runtime/intro#show)
|
||||||
|
// Shows the application.
|
||||||
|
export function Show(): void;
|
||||||
|
|
||||||
|
// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
|
||||||
|
// Returns the current text stored on clipboard
|
||||||
|
export function ClipboardGetText(): Promise<string>;
|
||||||
|
|
||||||
|
// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
|
||||||
|
// Sets a text on the clipboard
|
||||||
|
export function ClipboardSetText(text: string): Promise<boolean>;
|
||||||
|
|
||||||
|
// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop)
|
||||||
|
// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
|
||||||
|
export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void
|
||||||
|
|
||||||
|
// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff)
|
||||||
|
// OnFileDropOff removes the drag and drop listeners and handlers.
|
||||||
|
export function OnFileDropOff() :void
|
||||||
|
|
||||||
|
// Check if the file path resolver is available
|
||||||
|
export function CanResolveFilePaths(): boolean;
|
||||||
|
|
||||||
|
// Resolves file paths for an array of files
|
||||||
|
export function ResolveFilePaths(files: File[]): void
|
||||||
242
web/src/wailsjs/wailsjs/runtime/runtime.js
Normal file
242
web/src/wailsjs/wailsjs/runtime/runtime.js
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
/*
|
||||||
|
_ __ _ __
|
||||||
|
| | / /___ _(_) /____
|
||||||
|
| | /| / / __ `/ / / ___/
|
||||||
|
| |/ |/ / /_/ / / (__ )
|
||||||
|
|__/|__/\__,_/_/_/____/
|
||||||
|
The electron alternative for Go
|
||||||
|
(c) Lea Anthony 2019-present
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function LogPrint(message) {
|
||||||
|
window.runtime.LogPrint(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogTrace(message) {
|
||||||
|
window.runtime.LogTrace(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogDebug(message) {
|
||||||
|
window.runtime.LogDebug(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogInfo(message) {
|
||||||
|
window.runtime.LogInfo(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogWarning(message) {
|
||||||
|
window.runtime.LogWarning(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogError(message) {
|
||||||
|
window.runtime.LogError(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogFatal(message) {
|
||||||
|
window.runtime.LogFatal(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EventsOnMultiple(eventName, callback, maxCallbacks) {
|
||||||
|
return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EventsOn(eventName, callback) {
|
||||||
|
return EventsOnMultiple(eventName, callback, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EventsOff(eventName, ...additionalEventNames) {
|
||||||
|
return window.runtime.EventsOff(eventName, ...additionalEventNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EventsOffAll() {
|
||||||
|
return window.runtime.EventsOffAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EventsOnce(eventName, callback) {
|
||||||
|
return EventsOnMultiple(eventName, callback, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EventsEmit(eventName) {
|
||||||
|
let args = [eventName].slice.call(arguments);
|
||||||
|
return window.runtime.EventsEmit.apply(null, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowReload() {
|
||||||
|
window.runtime.WindowReload();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowReloadApp() {
|
||||||
|
window.runtime.WindowReloadApp();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetAlwaysOnTop(b) {
|
||||||
|
window.runtime.WindowSetAlwaysOnTop(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetSystemDefaultTheme() {
|
||||||
|
window.runtime.WindowSetSystemDefaultTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetLightTheme() {
|
||||||
|
window.runtime.WindowSetLightTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetDarkTheme() {
|
||||||
|
window.runtime.WindowSetDarkTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowCenter() {
|
||||||
|
window.runtime.WindowCenter();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetTitle(title) {
|
||||||
|
window.runtime.WindowSetTitle(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowFullscreen() {
|
||||||
|
window.runtime.WindowFullscreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowUnfullscreen() {
|
||||||
|
window.runtime.WindowUnfullscreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowIsFullscreen() {
|
||||||
|
return window.runtime.WindowIsFullscreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowGetSize() {
|
||||||
|
return window.runtime.WindowGetSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetSize(width, height) {
|
||||||
|
window.runtime.WindowSetSize(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetMaxSize(width, height) {
|
||||||
|
window.runtime.WindowSetMaxSize(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetMinSize(width, height) {
|
||||||
|
window.runtime.WindowSetMinSize(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetPosition(x, y) {
|
||||||
|
window.runtime.WindowSetPosition(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowGetPosition() {
|
||||||
|
return window.runtime.WindowGetPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowHide() {
|
||||||
|
window.runtime.WindowHide();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowShow() {
|
||||||
|
window.runtime.WindowShow();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowMaximise() {
|
||||||
|
window.runtime.WindowMaximise();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowToggleMaximise() {
|
||||||
|
window.runtime.WindowToggleMaximise();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowUnmaximise() {
|
||||||
|
window.runtime.WindowUnmaximise();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowIsMaximised() {
|
||||||
|
return window.runtime.WindowIsMaximised();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowMinimise() {
|
||||||
|
window.runtime.WindowMinimise();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowUnminimise() {
|
||||||
|
window.runtime.WindowUnminimise();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetBackgroundColour(R, G, B, A) {
|
||||||
|
window.runtime.WindowSetBackgroundColour(R, G, B, A);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ScreenGetAll() {
|
||||||
|
return window.runtime.ScreenGetAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowIsMinimised() {
|
||||||
|
return window.runtime.WindowIsMinimised();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowIsNormal() {
|
||||||
|
return window.runtime.WindowIsNormal();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BrowserOpenURL(url) {
|
||||||
|
window.runtime.BrowserOpenURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Environment() {
|
||||||
|
return window.runtime.Environment();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Quit() {
|
||||||
|
window.runtime.Quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Hide() {
|
||||||
|
window.runtime.Hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Show() {
|
||||||
|
window.runtime.Show();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ClipboardGetText() {
|
||||||
|
return window.runtime.ClipboardGetText();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ClipboardSetText(text) {
|
||||||
|
return window.runtime.ClipboardSetText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @callback OnFileDropCallback
|
||||||
|
* @param {number} x - x coordinate of the drop
|
||||||
|
* @param {number} y - y coordinate of the drop
|
||||||
|
* @param {string[]} paths - A list of file paths.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
|
||||||
|
* @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target)
|
||||||
|
*/
|
||||||
|
export function OnFileDrop(callback, useDropTarget) {
|
||||||
|
return window.runtime.OnFileDrop(callback, useDropTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OnFileDropOff removes the drag and drop listeners and handlers.
|
||||||
|
*/
|
||||||
|
export function OnFileDropOff() {
|
||||||
|
return window.runtime.OnFileDropOff();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CanResolveFilePaths() {
|
||||||
|
return window.runtime.CanResolveFilePaths();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ResolveFilePaths(files) {
|
||||||
|
return window.runtime.ResolveFilePaths(files);
|
||||||
|
}
|
||||||
@@ -1,60 +1,74 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import { resolve } from 'path'
|
import { resolve } from 'path'
|
||||||
|
import AutoImport from 'unplugin-auto-import/vite'
|
||||||
|
import Components from 'unplugin-vue-components/vite'
|
||||||
|
import { ArcoResolver } from 'unplugin-vue-components/resolvers'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [vue()],
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
AutoImport({
|
||||||
|
imports: ['vue', 'vue-router'],
|
||||||
|
dts: 'src/auto-imports.d.ts',
|
||||||
|
}),
|
||||||
|
Components({
|
||||||
|
resolvers: [ArcoResolver({ sideEffect: true })],
|
||||||
|
dts: 'src/components.d.ts',
|
||||||
|
})
|
||||||
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: { '@': resolve(__dirname, 'src') }
|
||||||
'@': resolve(__dirname, 'src')
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
outDir: 'dist',
|
outDir: 'dist',
|
||||||
emptyOutDir: true,
|
emptyOutDir: true,
|
||||||
sourcemap: false, // 生产环境禁用 source map,减小打包体积
|
sourcemap: false,
|
||||||
|
minify: 'esbuild',
|
||||||
|
cssCodeSplit: true,
|
||||||
|
chunkSizeWarningLimit: 1000,
|
||||||
|
esbuild: {
|
||||||
|
target: 'es2020',
|
||||||
|
drop: ['console', 'debugger']
|
||||||
|
},
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
output: {
|
output: {
|
||||||
manualChunks: {
|
manualChunks: (id) => {
|
||||||
'codemirror': [
|
if (!id.includes('node_modules')) return
|
||||||
'@codemirror/view',
|
|
||||||
'@codemirror/state',
|
if (id.includes('@codemirror')) {
|
||||||
'@codemirror/language',
|
if (id.includes('lang-') || id.includes('legacy-modes')) {
|
||||||
'@codemirror/commands',
|
return 'vendor-codemirror-langs'
|
||||||
'@codemirror/lang-javascript',
|
}
|
||||||
'@codemirror/lang-java',
|
return 'vendor-codemirror-core'
|
||||||
'@codemirror/lang-python',
|
}
|
||||||
'@codemirror/lang-html',
|
|
||||||
'@codemirror/lang-css',
|
if (id.includes('@arco-design')) return 'vendor-arco'
|
||||||
'@codemirror/lang-markdown',
|
if (id.includes('mermaid')) return 'vendor-mermaid'
|
||||||
'@codemirror/lang-sql'
|
if (id.includes('marked') || id.includes('highlight.js')) return 'vendor-markdown'
|
||||||
]
|
if (id.includes('vue') || id.includes('pinia')) return 'vendor-vue'
|
||||||
}
|
|
||||||
|
return 'vendor'
|
||||||
|
},
|
||||||
|
chunkFileNames: 'assets/js/[name]-[hash].js',
|
||||||
|
entryFileNames: 'assets/js/[name]-[hash].js',
|
||||||
|
assetFileNames: 'assets/[ext]/[name]-[hash].[ext]'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
include: [
|
include: [
|
||||||
'@codemirror/view',
|
'vue', 'pinia', '@arco-design/web-vue', 'marked', 'highlight.js',
|
||||||
'@codemirror/state',
|
'@codemirror/view', '@codemirror/state', '@codemirror/language', '@codemirror/commands',
|
||||||
'@codemirror/language',
|
'@codemirror/lang-javascript', '@codemirror/lang-json', '@codemirror/lang-yaml',
|
||||||
'@codemirror/commands',
|
'@codemirror/lang-html', '@codemirror/lang-css', '@codemirror/lang-markdown',
|
||||||
'@codemirror/lang-javascript',
|
'@codemirror/lang-sql', '@codemirror/lang-java', '@codemirror/lang-python',
|
||||||
'@codemirror/lang-java',
|
'@codemirror/lang-php', '@codemirror/lang-rust', '@codemirror/lang-go', '@codemirror/lang-cpp',
|
||||||
'@codemirror/lang-python',
|
'@codemirror/legacy-modes/mode/clike', '@codemirror/legacy-modes/mode/ruby',
|
||||||
'@codemirror/lang-html',
|
'@codemirror/legacy-modes/mode/shell', '@codemirror/legacy-modes/mode/xml'
|
||||||
'@codemirror/lang-css',
|
|
||||||
'@codemirror/lang-markdown',
|
|
||||||
'@codemirror/lang-sql',
|
|
||||||
'@codemirror/legacy-modes/mode/go',
|
|
||||||
'@codemirror/legacy-modes/mode/clike',
|
|
||||||
'@codemirror/legacy-modes/mode/ruby',
|
|
||||||
'@codemirror/legacy-modes/mode/rust',
|
|
||||||
'@codemirror/legacy-modes/mode/shell',
|
|
||||||
'@codemirror/legacy-modes/mode/yaml',
|
|
||||||
'@codemirror/legacy-modes/mode/xml'
|
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
cacheDir: 'node_modules/.vite'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user