新增:文件系统导航面包屑
功能: - 新增 PathBreadcrumb 组件,支持路径快速跳转 - 新增 DropdownItem 通用下拉菜单组件 优化: - 版本升级流程优化(Pinia 状态管理、进度节流、完整下载验证) - 模块延迟初始化(数据库、文件系统按需启动) - API 数据格式统一(蛇形转驼峰) - CodeMirror 语言包按需动态加载 - Markdown 渲染增强(支持锚点跳转) 重构: - 迁移到 Pinia 状态管理(stores/config.ts、stores/theme.ts、stores/update.ts) - 简化 UpdatePanel、UpdateNotification、ThemeToggle 逻辑 - 优化表结构加载逻辑 清理: - 删除测试组件 index-simple.vue - 删除旧的 useTheme.ts
This commit is contained in:
32
app.go
32
app.go
@@ -14,6 +14,7 @@ import (
|
||||
"u-desk/internal/common"
|
||||
"u-desk/internal/database"
|
||||
"u-desk/internal/filesystem"
|
||||
"u-desk/internal/service"
|
||||
"u-desk/internal/storage"
|
||||
"u-desk/internal/system"
|
||||
|
||||
@@ -59,7 +60,11 @@ func (a *App) Startup(ctx context.Context) {
|
||||
// 2.5. 迁移旧配置
|
||||
_ = a.configAPI.MigrateTabConfig()
|
||||
|
||||
// 3. 读取配置,获取可见的 Tabs
|
||||
// 3. 初始化版本号(提前触发缓存,避免后续重复计算)
|
||||
version := service.GetCurrentVersion()
|
||||
fmt.Printf("[启动] 当前版本: %s\n", version)
|
||||
|
||||
// 4. 读取配置,获取可见的 Tabs
|
||||
visibleTabs := a.getVisibleTabs()
|
||||
fmt.Printf("[启动] 可用的模块: %v\n", visibleTabs)
|
||||
|
||||
@@ -173,28 +178,31 @@ func (a *App) startFileServer() {
|
||||
return
|
||||
}
|
||||
|
||||
// 创建一个占位服务器用于保持引用(实际服务器由 StartLocalFileServer 管理)
|
||||
a.fileServer = &http.Server{
|
||||
Addr: "localhost:18765",
|
||||
}
|
||||
|
||||
fmt.Println("[文件服务器] 启动在 http://localhost:18765")
|
||||
}
|
||||
|
||||
// Shutdown 应用关闭时调用
|
||||
func (a *App) Shutdown(ctx context.Context) {
|
||||
// 关闭文件系统服务(优雅关闭,释放资源)
|
||||
// 创建带超时的上下文(5秒超时)
|
||||
shutdownCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 1. 关闭文件系统服务(优雅关闭,释放资源)
|
||||
if a.filesystem != nil {
|
||||
fmt.Println("[文件系统服务] 正在关闭...")
|
||||
if err := a.filesystem.Close(ctx); err != nil {
|
||||
if err := a.filesystem.Close(shutdownCtx); err != nil {
|
||||
fmt.Printf("[文件系统服务] 关闭失败: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("[文件系统服务] 已关闭")
|
||||
}
|
||||
}
|
||||
|
||||
// 停止文件服务器
|
||||
if a.fileServer != nil {
|
||||
fmt.Println("[文件服务器] 正在关闭...")
|
||||
a.fileServer.Shutdown(ctx)
|
||||
// 2. 停止文件服务器(使用全局服务器的关闭方法)
|
||||
fmt.Println("[文件服务器] 正在关闭...")
|
||||
if err := filesystem.ShutdownLocalFileServer(); err != nil {
|
||||
fmt.Printf("[文件服务器] 关闭失败: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("[文件服务器] 已关闭")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,13 +50,6 @@ func (api *UpdateAPI) CheckUpdate() (map[string]interface{}, error) {
|
||||
// GetCurrentVersion 获取当前版本号
|
||||
func (api *UpdateAPI) GetCurrentVersion() (map[string]interface{}, error) {
|
||||
version := service.GetCurrentVersion()
|
||||
|
||||
// 同步配置中的版本号
|
||||
if config, err := service.LoadUpdateConfig(); err == nil && config.CurrentVersion != version {
|
||||
config.CurrentVersion = version
|
||||
service.SaveUpdateConfig(config)
|
||||
}
|
||||
|
||||
return successResponse(map[string]interface{}{
|
||||
"version": version,
|
||||
}), nil
|
||||
@@ -69,13 +62,6 @@ func (api *UpdateAPI) GetUpdateConfig() (map[string]interface{}, error) {
|
||||
return errorResponse(err.Error()), nil
|
||||
}
|
||||
|
||||
// 同步最新版本号
|
||||
latestVersion := service.GetCurrentVersion()
|
||||
if config.CurrentVersion != latestVersion {
|
||||
config.CurrentVersion = latestVersion
|
||||
service.SaveUpdateConfig(config)
|
||||
}
|
||||
|
||||
return successResponse(map[string]interface{}{
|
||||
"current_version": config.CurrentVersion,
|
||||
"last_check_time": config.LastCheckTime.Format("2006-01-02 15:04:05"),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package filesystem
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"log"
|
||||
@@ -10,12 +11,14 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LocalFileServer 本地文件服务器(独立的 HTTP 服务器)
|
||||
type LocalFileServer struct {
|
||||
server *http.Server
|
||||
addr string
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -258,3 +261,35 @@ func HandleLocalFile(w http.ResponseWriter, r *http.Request) {
|
||||
func isAllowedFileType(ext string) bool {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -262,7 +262,7 @@ func (s *FileSystemService) DeletePathWithContext(ctx context.Context, path stri
|
||||
|
||||
// 返回被删除的文件信息,用于前端更新
|
||||
return &FileOperationResult{
|
||||
Path: path,
|
||||
Path: filepath.ToSlash(path), // 统一使用正斜杠
|
||||
Name: info.Name(),
|
||||
Size: info.Size(),
|
||||
SizeStr: formatBytes(info.Size()),
|
||||
@@ -297,7 +297,7 @@ func (s *FileSystemService) ListDir(path string) ([]map[string]interface{}, erro
|
||||
fullPath := filepath.Join(path, entry.Name())
|
||||
result = append(result, map[string]interface{}{
|
||||
"name": entry.Name(),
|
||||
"path": fullPath,
|
||||
"path": filepath.ToSlash(fullPath), // 统一使用正斜杠
|
||||
"is_dir": entry.IsDir(),
|
||||
"size": info.Size(),
|
||||
"mod_time": info.ModTime().Format("2006-01-02 15:04:05"),
|
||||
@@ -338,14 +338,14 @@ func (s *FileSystemService) CreateDir(path string) (*FileOperationResult, error)
|
||||
if err != nil {
|
||||
// 创建成功但获取信息失败,返回基本信息
|
||||
return &FileOperationResult{
|
||||
Path: path,
|
||||
Path: filepath.ToSlash(path), // 统一使用正斜杠
|
||||
Name: filepath.Base(path),
|
||||
IsDir: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &FileOperationResult{
|
||||
Path: path,
|
||||
Path: filepath.ToSlash(path), // 统一使用正斜杠
|
||||
Name: info.Name(),
|
||||
Size: info.Size(),
|
||||
SizeStr: formatBytes(info.Size()),
|
||||
@@ -385,7 +385,7 @@ func (s *FileSystemService) CreateFile(path string) (*FileOperationResult, error
|
||||
if err != nil {
|
||||
// 创建成功但获取信息失败,返回基本信息
|
||||
return &FileOperationResult{
|
||||
Path: path,
|
||||
Path: filepath.ToSlash(path), // 统一使用正斜杠
|
||||
Name: filepath.Base(path),
|
||||
IsDir: false,
|
||||
Size: 0,
|
||||
@@ -393,7 +393,7 @@ func (s *FileSystemService) CreateFile(path string) (*FileOperationResult, error
|
||||
}
|
||||
|
||||
return &FileOperationResult{
|
||||
Path: path,
|
||||
Path: filepath.ToSlash(path), // 统一使用正斜杠
|
||||
Name: info.Name(),
|
||||
Size: info.Size(),
|
||||
SizeStr: formatBytes(info.Size()),
|
||||
@@ -424,7 +424,7 @@ func (s *FileSystemService) GetFileInfo(path string) (map[string]interface{}, er
|
||||
|
||||
return map[string]interface{}{
|
||||
"name": info.Name(),
|
||||
"path": path,
|
||||
"path": filepath.ToSlash(path), // 统一使用正斜杠
|
||||
"size": info.Size(),
|
||||
"size_str": formatBytes(info.Size()),
|
||||
"is_dir": info.IsDir(),
|
||||
@@ -472,21 +472,21 @@ func (s *FileSystemService) RenamePath(oldPath, newPath string) (*FileOperationR
|
||||
if err != nil {
|
||||
// 重命名成功但获取信息失败,返回基本信息
|
||||
return &FileOperationResult{
|
||||
Path: newPath,
|
||||
Path: filepath.ToSlash(newPath), // 统一使用正斜杠
|
||||
Name: filepath.Base(newPath),
|
||||
OldPath: oldPath,
|
||||
OldPath: filepath.ToSlash(oldPath),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &FileOperationResult{
|
||||
Path: newPath,
|
||||
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: oldPath,
|
||||
OldPath: filepath.ToSlash(oldPath),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -181,7 +181,7 @@ func ListZipContents(zipPath string) ([]map[string]interface{}, error) {
|
||||
|
||||
entry := map[string]interface{}{
|
||||
"name": name,
|
||||
"path": file.Name, // zip 中的完整路径
|
||||
"path": file.Name, // zip 中的完整路径(已使用 /)
|
||||
"is_dir": isDir,
|
||||
"size": file.UncompressedSize64,
|
||||
"compressed": file.CompressedSize64,
|
||||
|
||||
@@ -103,7 +103,7 @@ func getCompressionMethodString(method uint16) string {
|
||||
func createFileInfoMap(file *zip.File, includeExtra ...bool) map[string]interface{} {
|
||||
info := map[string]interface{}{
|
||||
"name": filepath.Base(file.Name),
|
||||
"path": file.Name,
|
||||
"path": file.Name, // zip 中的路径(已使用 /)
|
||||
"is_dir": file.Mode().IsDir(),
|
||||
"size": file.UncompressedSize64,
|
||||
"compressed": file.CompressedSize64,
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -62,20 +61,13 @@ func NewUpdateService(checkURL string) *UpdateService {
|
||||
|
||||
// CheckUpdate 检查更新
|
||||
func (s *UpdateService) CheckUpdate() (*UpdateCheckResult, error) {
|
||||
log.Printf("[更新检查] 开始检查更新,检查地址: %s", s.checkURL)
|
||||
|
||||
config, err := LoadUpdateConfig()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("加载配置失败: %v", err)
|
||||
}
|
||||
|
||||
// 同步版本号
|
||||
currentVersionStr, err := s.syncConfigVersion(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
currentVersion, err := ParseVersion(currentVersionStr)
|
||||
// 获取当前版本(使用缓存)
|
||||
currentVersion, err := ParseVersion(GetCurrentVersion())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析当前版本失败: %v", err)
|
||||
}
|
||||
@@ -86,14 +78,6 @@ func (s *UpdateService) CheckUpdate() (*UpdateCheckResult, error) {
|
||||
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)
|
||||
if err != nil {
|
||||
@@ -102,55 +86,30 @@ func (s *UpdateService) CheckUpdate() (*UpdateCheckResult, error) {
|
||||
|
||||
// 比较版本
|
||||
hasUpdate := remoteVersion.IsNewerThan(currentVersion)
|
||||
log.Printf("[更新检查] 版本比较: 当前=%s, 远程=%s, 有更新=%v",
|
||||
currentVersion.String(), remoteVersion.String(), hasUpdate)
|
||||
|
||||
// 更新最后检查时间
|
||||
config.UpdateLastCheckTime()
|
||||
|
||||
result := &UpdateCheckResult{
|
||||
return &UpdateCheckResult{
|
||||
HasUpdate: hasUpdate,
|
||||
CurrentVersion: currentVersionStr,
|
||||
CurrentVersion: GetCurrentVersion(),
|
||||
LatestVersion: remoteInfo.Version,
|
||||
DownloadURL: remoteInfo.DownloadURL,
|
||||
Changelog: remoteInfo.Changelog,
|
||||
ForceUpdate: remoteInfo.ForceUpdate,
|
||||
ReleaseDate: remoteInfo.ReleaseDate,
|
||||
FileSize: remoteInfo.FileSize,
|
||||
}
|
||||
|
||||
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
|
||||
}, nil
|
||||
}
|
||||
|
||||
// fetchRemoteVersionInfo 获取远程版本信息
|
||||
func (s *UpdateService) fetchRemoteVersionInfo() (*RemoteVersionInfo, error) {
|
||||
if s.checkURL == "" {
|
||||
log.Printf("[远程版本] 版本检查 URL 未配置")
|
||||
return nil, fmt.Errorf("版本检查 URL 未配置,请先设置检查地址")
|
||||
}
|
||||
|
||||
log.Printf("[远程版本] 请求远程版本信息: %s", s.checkURL)
|
||||
|
||||
// 添加时间戳参数防止缓存
|
||||
timestamp := time.Now().UnixMilli() // 使用毫秒级时间戳
|
||||
timestamp := time.Now().UnixMilli()
|
||||
var requestURL string
|
||||
if strings.Contains(s.checkURL, "?") {
|
||||
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)
|
||||
}
|
||||
|
||||
log.Printf("[远程版本] 实际请求URL: %s", requestURL)
|
||||
|
||||
// 创建 HTTP 客户端,设置超时
|
||||
client := &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
@@ -168,12 +125,10 @@ func (s *UpdateService) fetchRemoteVersionInfo() (*RemoteVersionInfo, error) {
|
||||
// 发送请求
|
||||
resp, err := client.Get(requestURL)
|
||||
if err != nil {
|
||||
log.Printf("[远程版本] 网络请求失败: %v", err)
|
||||
return nil, fmt.Errorf("网络请求失败: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
log.Printf("[远程版本] HTTP 响应状态码: %d", resp.StatusCode)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("服务器返回错误状态码: %d", resp.StatusCode)
|
||||
}
|
||||
@@ -181,25 +136,19 @@ func (s *UpdateService) fetchRemoteVersionInfo() (*RemoteVersionInfo, error) {
|
||||
// 读取响应
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Printf("[远程版本] 读取响应失败: %v", err)
|
||||
return nil, fmt.Errorf("读取响应失败: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("[远程版本] 响应内容长度: %d 字节", len(body))
|
||||
|
||||
// 解析 JSON
|
||||
var remoteInfo RemoteVersionInfo
|
||||
if err := json.Unmarshal(body, &remoteInfo); err != nil {
|
||||
log.Printf("[远程版本] 解析 JSON 失败: %v, 响应内容: %s", err, string(body))
|
||||
return nil, fmt.Errorf("解析响应失败: %v", err)
|
||||
}
|
||||
|
||||
if remoteInfo.Version == "" {
|
||||
log.Printf("[远程版本] 远程版本信息不完整,响应内容: %s", string(body))
|
||||
return nil, fmt.Errorf("远程版本信息不完整")
|
||||
}
|
||||
|
||||
log.Printf("[远程版本] 成功获取远程版本信息: %+v", remoteInfo)
|
||||
return &remoteInfo, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package service
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"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 == "" {
|
||||
config.CheckURL = "https://img.1216.top/u-desk/last-version.json"
|
||||
}
|
||||
|
||||
// 确保版本号不为空(使用缓存的版本号)
|
||||
if config.CurrentVersion == "" {
|
||||
config.CurrentVersion = GetCurrentVersion()
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// ==================== 常量定义 ====================
|
||||
@@ -15,6 +16,12 @@ import (
|
||||
// AppVersion 应用版本号(发布时直接修改此处)
|
||||
const AppVersion = "0.3.0"
|
||||
|
||||
// 版本号缓存
|
||||
var (
|
||||
cachedVersion string
|
||||
versionOnce sync.Once
|
||||
)
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
|
||||
// Version 版本号结构
|
||||
@@ -100,22 +107,25 @@ func (v *Version) IsOlderThan(other *Version) bool {
|
||||
|
||||
// ==================== 版本号获取 ====================
|
||||
|
||||
// GetCurrentVersion 获取当前版本号
|
||||
// GetCurrentVersion 获取当前版本号(带缓存)
|
||||
// 优先级:硬编码版本号 > wails.json(开发模式)> 默认值
|
||||
func GetCurrentVersion() string {
|
||||
if AppVersion != "" {
|
||||
log.Printf("[版本] 使用硬编码版本号: %s", AppVersion)
|
||||
return AppVersion
|
||||
}
|
||||
versionOnce.Do(func() {
|
||||
if AppVersion != "" {
|
||||
cachedVersion = AppVersion
|
||||
return
|
||||
}
|
||||
|
||||
version := getVersionFromWailsJSON()
|
||||
if version != "" {
|
||||
log.Printf("[版本] 从 wails.json 获取版本号: %s", version)
|
||||
return version
|
||||
}
|
||||
version := getVersionFromWailsJSON()
|
||||
if version != "" {
|
||||
cachedVersion = version
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[版本] 使用默认版本号: 0.0.1")
|
||||
return "0.0.1"
|
||||
cachedVersion = "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",
|
||||
"marked": "^17.0.1",
|
||||
"mermaid": "^11.12.2",
|
||||
"pinia": "^3.0.4",
|
||||
"vue": "^3.5.26"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.3",
|
||||
"unplugin-vue-components": "^0.27.4",
|
||||
"unplugin-auto-import": "^0.18.3",
|
||||
"vite": "^7.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
242
web/src/App.vue
242
web/src/App.vue
@@ -58,29 +58,30 @@
|
||||
<!-- 设置抽屉 -->
|
||||
<SettingsPanel
|
||||
v-model="showSettings"
|
||||
:config="appConfig"
|
||||
:config="configStore.appConfig"
|
||||
@save="handleSaveConfig"
|
||||
/>
|
||||
|
||||
<!-- 升级提示弹窗 -->
|
||||
<UpdateNotification
|
||||
v-model="showUpdateNotification"
|
||||
:update-info="updateInfo"
|
||||
@install="handleUpdateInstall"
|
||||
@skip="handleUpdateSkip"
|
||||
v-model="updateStore.showUpdate"
|
||||
:update-info="updateStore.updateInfo"
|
||||
@install="updateStore.installUpdate"
|
||||
/>
|
||||
</a-layout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {computed, onMounted, ref, watch} from 'vue'
|
||||
import {IconSettings} from '@arco-design/web-vue/es/icon'
|
||||
import {Message} from '@arco-design/web-vue'
|
||||
import { computed, watch, ref, onMounted, onUnmounted } from 'vue'
|
||||
import { IconSettings } from '@arco-design/web-vue/es/icon'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import DbCli from './views/db-cli/index.vue'
|
||||
import ThemeToggle from './components/ThemeToggle.vue'
|
||||
import FileSystem from './components/FileSystem/index.vue'
|
||||
import SettingsPanel from './components/SettingsPanel.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'
|
||||
@@ -91,124 +92,39 @@ const activeTab = ref((savedTab === 'user' ? 'file-system' : savedTab) || 'file-
|
||||
const showSettings = ref(false)
|
||||
const isMaximized = ref(false)
|
||||
|
||||
// 更新相关状态
|
||||
const showUpdateNotification = ref(false)
|
||||
const updateInfo = ref(null)
|
||||
const checkedUpdate = ref(false)
|
||||
// 使用 stores
|
||||
const updateStore = useUpdateStore()
|
||||
const configStore = useConfigStore()
|
||||
|
||||
// 应用配置
|
||||
const appConfig = ref({
|
||||
tabs: [],
|
||||
visibleTabs: [],
|
||||
defaultTab: 'file-system'
|
||||
})
|
||||
// 应用配置(从 store 获取)
|
||||
const appConfig = computed(() => configStore.appConfig)
|
||||
|
||||
// 可见 Tabs(根据配置动态生成)
|
||||
const visibleTabs = computed(() => {
|
||||
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'
|
||||
}
|
||||
}
|
||||
// 可见 Tabs(从 store 获取)
|
||||
const visibleTabs = computed(() => configStore.visibleTabs)
|
||||
|
||||
// 保存配置
|
||||
const handleSaveConfig = async (config) => {
|
||||
try {
|
||||
const result = await window.go.main.App.SaveAppConfig({
|
||||
tabs: config.tabs,
|
||||
visibleTabs: config.visibleTabs,
|
||||
defaultTab: config.defaultTab
|
||||
})
|
||||
await configStore.saveConfig(config)
|
||||
showSettings.value = false
|
||||
|
||||
if (result.success) {
|
||||
// 更新本地配置
|
||||
appConfig.value = {
|
||||
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)
|
||||
// 如果当前激活的 Tab 被隐藏,切换到默认 Tab
|
||||
if (!config.visibleTabs.includes(activeTab.value)) {
|
||||
activeTab.value = config.defaultTab
|
||||
}
|
||||
} catch (error) {
|
||||
// 错误已在 store 中处理
|
||||
console.error('保存配置失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 加载配置(调用 store 方法)
|
||||
const loadConfig = async () => {
|
||||
await configStore.loadConfig()
|
||||
// 设置默认 Tab
|
||||
activeTab.value = configStore.defaultTab
|
||||
}
|
||||
|
||||
// 获取组件
|
||||
const getComponent = (key) => {
|
||||
const components = {
|
||||
@@ -218,75 +134,22 @@ const getComponent = (key) => {
|
||||
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(() => {
|
||||
loadConfig()
|
||||
// 延迟检查更新,避免阻塞应用启动
|
||||
|
||||
// 设置更新事件监听
|
||||
updateStore.setupEventListeners()
|
||||
|
||||
// 延迟检查更新(启动后 3 秒,静默模式)
|
||||
setTimeout(() => {
|
||||
if (!checkedUpdate.value) {
|
||||
checkForUpdates()
|
||||
}
|
||||
updateStore.checkForUpdates(true)
|
||||
}, 3000)
|
||||
})
|
||||
|
||||
// 监听 activeTab 变化,自动保存到 localStorage
|
||||
watch(activeTab, (newTab) => {
|
||||
localStorage.setItem(ACTIVE_TAB_STORAGE_KEY, newTab)
|
||||
// 组件卸载时清理事件监听
|
||||
onUnmounted(() => {
|
||||
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
|
||||
watch(activeTab, (newTab) => {
|
||||
// 保存到 localStorage
|
||||
@@ -351,8 +191,8 @@ watch(activeTab, (newTab) => {
|
||||
|
||||
// 检查 Tab 是否在可见列表中
|
||||
const isVisible = appConfig.value.visibleTabs.includes(newTab)
|
||||
if (!isVisible && appConfig.value.visibleTabs.length > 0) {
|
||||
// 切换到默认 Tab
|
||||
if (!isVisible && appConfig.value.visibleTabs.length > 0 && newTab !== appConfig.value.defaultTab) {
|
||||
// 切换到默认 Tab(避免重复触发)
|
||||
activeTab.value = appConfig.value.defaultTab
|
||||
}
|
||||
})
|
||||
|
||||
@@ -4,6 +4,24 @@
|
||||
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -135,14 +155,12 @@ export async function getEnvVars(): Promise<Record<string, string>> {
|
||||
* 列出 zip 文件内容
|
||||
*/
|
||||
export async function listZipContents(zipPath: string): Promise<File[]> {
|
||||
console.log('[API] listZipContents 调用:', zipPath)
|
||||
if (!window.go?.main?.App?.ListZipContents) {
|
||||
throw new Error('ListZipContents API 不可用')
|
||||
}
|
||||
try {
|
||||
const result = await window.go.main.App.ListZipContents(zipPath)
|
||||
console.log('[API] listZipContents 结果:', result?.length || 0, '个文件')
|
||||
return result
|
||||
return transformFileList(result)
|
||||
} catch (error) {
|
||||
console.error('[API] listZipContents 错误:', error)
|
||||
throw error
|
||||
@@ -153,13 +171,11 @@ export async function listZipContents(zipPath: string): Promise<File[]> {
|
||||
* 从 zip 文件中提取单个文件内容
|
||||
*/
|
||||
export async function extractFileFromZip(zipPath: string, filePath: string): Promise<string> {
|
||||
console.log('[API] extractFileFromZip 调用:', { zipPath, filePath })
|
||||
if (!window.go?.main?.App?.ExtractFileFromZip) {
|
||||
throw new Error('ExtractFileFromZip API 不可用')
|
||||
}
|
||||
try {
|
||||
const result = await window.go.main.App.ExtractFileFromZip(zipPath, filePath)
|
||||
console.log('[API] extractFileFromZip 成功, 内容长度:', result?.length || 0)
|
||||
return result
|
||||
} catch (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> {
|
||||
console.log('[API] extractFileFromZipToTemp 调用:', { zipPath, filePath })
|
||||
if (!window.go?.main?.App?.ExtractFileFromZipToTemp) {
|
||||
throw new Error('ExtractFileFromZipToTemp API 不可用')
|
||||
}
|
||||
try {
|
||||
const result = await window.go.main.App.ExtractFileFromZipToTemp(zipPath, filePath)
|
||||
console.log('[API] extractFileFromZipToTemp 成功, 临时文件路径:', result)
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('[API] extractFileFromZipToTemp 错误:', error)
|
||||
@@ -190,14 +204,12 @@ export async function extractFileFromZipToTemp(zipPath: string, filePath: string
|
||||
* 获取 zip 文件中特定文件的信息
|
||||
*/
|
||||
export async function getZipFileInfo(zipPath: string, filePath: string): Promise<File> {
|
||||
console.log('[API] getZipFileInfo 调用:', { zipPath, filePath })
|
||||
if (!window.go?.main?.App?.GetZipFileInfo) {
|
||||
throw new Error('GetZipFileInfo API 不可用')
|
||||
}
|
||||
try {
|
||||
const result = await window.go.main.App.GetZipFileInfo(zipPath, filePath)
|
||||
console.log('[API] getZipFileInfo 结果:', result)
|
||||
return result
|
||||
return transformFile(result)
|
||||
} catch (error) {
|
||||
console.error('[API] getZipFileInfo 错误:', error)
|
||||
throw error
|
||||
@@ -208,13 +220,11 @@ export async function getZipFileInfo(zipPath: string, filePath: string): Promise
|
||||
* 使用系统默认程序打开文件或目录
|
||||
*/
|
||||
export async function openPath(path: string): Promise<void> {
|
||||
console.log('[API] openPath 调用:', path)
|
||||
if (!window.go?.main?.App?.OpenPath) {
|
||||
throw new Error('OpenPath API 不可用')
|
||||
}
|
||||
try {
|
||||
await window.go.main.App.OpenPath(path)
|
||||
console.log('[API] openPath 成功')
|
||||
} catch (error) {
|
||||
console.error('[API] openPath 错误:', error)
|
||||
throw error
|
||||
@@ -242,13 +252,11 @@ export async function resolveShortcut(lnkPath: string): Promise<{
|
||||
targetAccessible?: boolean
|
||||
targetInfo?: any
|
||||
}> {
|
||||
console.log('[API] resolveShortcut 调用:', lnkPath)
|
||||
if (!window.go?.main?.App?.ResolveShortcut) {
|
||||
throw new Error('ResolveShortcut API 不可用')
|
||||
}
|
||||
try {
|
||||
const result = await window.go.main.App.ResolveShortcut(lnkPath)
|
||||
console.log('[API] resolveShortcut 结果:', result)
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('[API] resolveShortcut 错误:', error)
|
||||
|
||||
@@ -3,121 +3,32 @@
|
||||
</template>
|
||||
|
||||
<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 { EditorState } from '@codemirror/state'
|
||||
import { defaultKeymap, history, historyKeymap } 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 { defaultKeymap, history } from '@codemirror/commands'
|
||||
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({
|
||||
modelValue: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
fileExtension: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
modelValue: { type: String, required: true },
|
||||
fileExtension: { type: String, default: '' }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
// ==================== State ====================
|
||||
const { isDark } = useTheme()
|
||||
const themeStore = useThemeStore()
|
||||
const editorContainer = ref(null)
|
||||
let view = null
|
||||
|
||||
// ==================== Editor Management ====================
|
||||
/**
|
||||
* 创建编辑器扩展配置
|
||||
*/
|
||||
const createExtensions = () => {
|
||||
const createExtensions = async () => {
|
||||
const extensions = [
|
||||
lineNumbers(),
|
||||
highlightActiveLineGutter(),
|
||||
history(),
|
||||
keymap.of(defaultKeymap),
|
||||
// 不使用 historyKeymap,避免 Ctrl+Z 与外部重置功能冲突
|
||||
// 用户可以通过外部的重置按钮或 Ctrl+Z(全局快捷键)恢复原始内容
|
||||
bracketMatching(),
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (update.docChanged) {
|
||||
@@ -125,202 +36,64 @@ const createExtensions = () => {
|
||||
}
|
||||
}),
|
||||
EditorView.theme({
|
||||
'&': {
|
||||
height: '100%',
|
||||
fontSize: '13px'
|
||||
},
|
||||
'.cm-scroller': {
|
||||
overflow: 'auto',
|
||||
fontFamily: 'Consolas, Monaco, Courier New, monospace'
|
||||
},
|
||||
'.cm-content': {
|
||||
padding: '8px',
|
||||
minHeight: '100%'
|
||||
},
|
||||
'.cm-line': {
|
||||
padding: '0 0'
|
||||
},
|
||||
'&.cm-focused': {
|
||||
outline: 'none'
|
||||
}
|
||||
'&': { height: '100%', fontSize: '13px' },
|
||||
'.cm-scroller': { overflow: 'auto', fontFamily: 'Consolas, Monaco, Courier New, monospace' },
|
||||
'.cm-content': { padding: '8px', minHeight: '100%' },
|
||||
'.cm-line': { padding: '0 0' },
|
||||
'&.cm-focused': { outline: 'none' }
|
||||
})
|
||||
]
|
||||
|
||||
// 主题
|
||||
if (isDark.value) {
|
||||
if (themeStore.isDark) {
|
||||
extensions.push(oneDark)
|
||||
}
|
||||
|
||||
// 语言支持
|
||||
const ext = props.fileExtension.toLowerCase()
|
||||
|
||||
// JavaScript/TypeScript
|
||||
if (LANGUAGE_MAP.javascript.includes(ext) || LANGUAGE_MAP.typescript.includes(ext)) {
|
||||
extensions.push(javascript({ jsx: true }))
|
||||
const language = getLanguageFromExtension(props.fileExtension)
|
||||
if (language !== 'text') {
|
||||
const langExtension = await loadLanguageExtension(language)
|
||||
if (langExtension) {
|
||||
extensions.push(langExtension)
|
||||
}
|
||||
}
|
||||
// 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
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建编辑器实例
|
||||
*/
|
||||
const createEditor = (docContent = '') => {
|
||||
const createEditor = async (docContent = '') => {
|
||||
if (!editorContainer.value) return
|
||||
|
||||
const state = EditorState.create({
|
||||
doc: docContent,
|
||||
extensions: createExtensions()
|
||||
})
|
||||
|
||||
view = new EditorView({
|
||||
state,
|
||||
parent: editorContainer.value
|
||||
})
|
||||
const extensions = await createExtensions()
|
||||
const state = EditorState.create({ doc: docContent, extensions })
|
||||
view = new EditorView({ state, parent: editorContainer.value })
|
||||
}
|
||||
|
||||
/**
|
||||
* 重建编辑器(保留内容)
|
||||
*/
|
||||
const recreateEditor = () => {
|
||||
const recreateEditor = async () => {
|
||||
if (!view) return
|
||||
const currentDoc = view.state.doc.toString()
|
||||
view.destroy()
|
||||
createEditor(currentDoc)
|
||||
await createEditor(currentDoc)
|
||||
}
|
||||
|
||||
// ==================== Lifecycle ====================
|
||||
onMounted(() => {
|
||||
createEditor(props.modelValue || '')
|
||||
onMounted(async () => {
|
||||
await createEditor(props.modelValue || '')
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (view) {
|
||||
view.destroy()
|
||||
}
|
||||
view?.destroy()
|
||||
})
|
||||
|
||||
// ==================== Watchers ====================
|
||||
// 监听外部内容变化
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
if (view && newValue !== view.state.doc.toString()) {
|
||||
view.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: view.state.doc.length,
|
||||
insert: newValue || ''
|
||||
}
|
||||
changes: { from: 0, to: view.state.doc.length, insert: newValue || '' }
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 监听主题或文件扩展名变化,重建编辑器
|
||||
// 使用 nextTick 确保 DOM 更新完成后再重建,避免视觉抖动
|
||||
import { nextTick } from 'vue'
|
||||
const isDark = computed(() => themeStore.isDark)
|
||||
watch([isDark, () => props.fileExtension], async () => {
|
||||
await nextTick()
|
||||
recreateEditor()
|
||||
await recreateEditor()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -235,7 +235,7 @@ const {
|
||||
deleteFile,
|
||||
} = useFileOperations({
|
||||
onSuccess: (operation, data) => {
|
||||
console.log(`[DeviceTest] ${operation} 成功:`, data)
|
||||
// 成功回调
|
||||
},
|
||||
onError: (operation, error) => {
|
||||
console.error(`[DeviceTest] ${operation} 失败:`, error)
|
||||
@@ -271,7 +271,6 @@ const { storedValue: pathHistory } = useLocalStorage(
|
||||
try {
|
||||
const oldContent = localStorage.getItem(STORAGE_KEYS.DEVICE_TEST.FILE_CONTENT)
|
||||
if (oldContent) {
|
||||
console.log('[DeviceTest] 清理旧的文件内容缓存')
|
||||
localStorage.removeItem(STORAGE_KEYS.DEVICE_TEST.FILE_CONTENT)
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
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">
|
||||
<CodeEditor
|
||||
<AsyncCodeEditor
|
||||
:model-value="config.fileContent"
|
||||
:file-extension="config.currentFileExtension"
|
||||
@update:model-value="handleContentUpdate"
|
||||
@@ -185,7 +185,7 @@
|
||||
|
||||
<!-- 编辑模式 -->
|
||||
<div v-else class="markdown-edit-wrapper">
|
||||
<CodeEditor
|
||||
<AsyncCodeEditor
|
||||
:model-value="config.fileContent"
|
||||
:file-extension="config.currentFileExtension"
|
||||
@update:model-value="handleContentUpdate"
|
||||
@@ -238,7 +238,7 @@
|
||||
</a-tooltip>
|
||||
</div>
|
||||
|
||||
<CodeEditor
|
||||
<AsyncCodeEditor
|
||||
:model-value="config.fileContent"
|
||||
:file-extension="config.currentFileExtension"
|
||||
@update:model-value="handleContentUpdate"
|
||||
@@ -254,14 +254,20 @@
|
||||
</template>
|
||||
|
||||
<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 { IconSave, IconEdit, IconEye, IconUndo, IconCopy } from '@arco-design/web-vue/es/icon'
|
||||
import CodeEditor from '@/components/CodeEditor.vue'
|
||||
import { getFileName } from '@/utils/fileUtils'
|
||||
import type { FileEditorPanelConfig } from '@/types/file-system'
|
||||
import { renderMermaidDiagrams } from '@/utils/markedExtensions'
|
||||
|
||||
// 异步加载 CodeEditor 组件,减少初始包大小
|
||||
const AsyncCodeEditor = defineAsyncComponent({
|
||||
loader: () => import('@/components/CodeEditor.vue'),
|
||||
delay: 200,
|
||||
timeout: 10000
|
||||
})
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
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)"
|
||||
@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>
|
||||
<a-button
|
||||
type="text"
|
||||
|
||||
@@ -25,25 +25,19 @@
|
||||
退出 ZIP
|
||||
</a-button>
|
||||
</div>
|
||||
<!-- 正常模式:路径输入 -->
|
||||
<a-auto-complete
|
||||
v-else
|
||||
:model-value="normalizedPath"
|
||||
:data="normalizedPathHistory"
|
||||
placeholder="输入路径 (如: C:/Users)"
|
||||
class="path-input"
|
||||
@select="handlePathSelect"
|
||||
@pressEnter="handlePathSelect"
|
||||
@update:model-value="handlePathUpdate"
|
||||
>
|
||||
<template #append>
|
||||
<a-tooltip content="复制路径" position="top">
|
||||
<div class="copy-icon-wrapper" @click="handleCopyPath">
|
||||
<icon-copy />
|
||||
</div>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</a-auto-complete>
|
||||
<!-- 正常模式:面包屑导航 -->
|
||||
<div v-else class="path-breadcrumb-wrapper">
|
||||
<PathBreadcrumb
|
||||
:path="config.filePath"
|
||||
@navigate="handleGoToPath"
|
||||
@openFile="handleOpenFile"
|
||||
/>
|
||||
<a-tooltip content="复制路径" position="top">
|
||||
<div class="copy-icon-wrapper" @click="handleCopyPath">
|
||||
<icon-copy />
|
||||
</div>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -119,6 +113,7 @@
|
||||
import { computed } from 'vue'
|
||||
import { IconForward, IconHistory, IconRefresh, IconMenu, IconClose, IconRight, IconCopy } from '@arco-design/web-vue/es/icon'
|
||||
import type { ToolbarConfig } from '@/types/file-system'
|
||||
import PathBreadcrumb from './PathBreadcrumb.vue'
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
@@ -134,21 +129,13 @@ interface Emits {
|
||||
(e: 'refresh'): void
|
||||
(e: 'exitZip'): void
|
||||
(e: 'goToPath', path: string): void
|
||||
(e: 'openFile', path: string): void
|
||||
(e: 'navigateToZipDirectory', path: string): void
|
||||
(e: 'showMessage', message: string, type: 'success' | 'error' | 'warning' | 'info'): void
|
||||
}
|
||||
|
||||
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) => {
|
||||
emit('update:filePath', path)
|
||||
@@ -162,6 +149,10 @@ const handleGoToPath = (path: string) => {
|
||||
emit('goToPath', path)
|
||||
}
|
||||
|
||||
const handleOpenFile = (path: string) => {
|
||||
emit('openFile', path)
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
emit('refresh')
|
||||
}
|
||||
@@ -235,22 +226,34 @@ const handleCopyPath = async () => {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 覆盖 Arco 输入框 append 的默认 padding */
|
||||
.path-input-wrapper :deep(.arco-input-append) {
|
||||
padding: 0 !important;
|
||||
.path-breadcrumb-wrapper {
|
||||
display: flex;
|
||||
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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 8px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 4px 8px;
|
||||
cursor: pointer;
|
||||
color: var(--color-text-3);
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
border-radius: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.copy-icon-wrapper:hover {
|
||||
|
||||
@@ -25,7 +25,13 @@ export function useFavorites() {
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEYS.FAVORITE_FILES)
|
||||
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) {
|
||||
console.error('加载收藏列表失败:', error)
|
||||
@@ -62,10 +68,10 @@ export function useFavorites() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准化路径用于比较(处理正斜杠/反斜杠不一致)
|
||||
* 标准化路径用于比较(后端已统一为 /,直接转小写)
|
||||
*/
|
||||
const normalizePath = (path: string): string => {
|
||||
return path.replace(/\\/g, '/').toLowerCase()
|
||||
return path.toLowerCase()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { STORAGE_KEYS } from '@/utils/constants'
|
||||
import { normalizePathSeparators } from '@/utils/pathHelpers'
|
||||
import type { PathHistory } from '@/types/file-system'
|
||||
|
||||
export interface UsePathNavigationOptions {
|
||||
@@ -18,6 +19,10 @@ export interface UsePathNavigationOptions {
|
||||
const restoreLastPath = (): string | null => {
|
||||
try {
|
||||
const lastPath = localStorage.getItem(STORAGE_KEYS.FILESYSTEM.FILE_PATH)
|
||||
if (lastPath) {
|
||||
// 规范化旧路径(可能包含反斜杠)
|
||||
return normalizePathSeparators(lastPath)
|
||||
}
|
||||
return lastPath
|
||||
} catch (error) {
|
||||
console.error('恢复路径失败:', error)
|
||||
@@ -56,8 +61,8 @@ export function usePathNavigation(options: UsePathNavigationOptions = {}) {
|
||||
if (!path || path === filePath.value) return
|
||||
|
||||
try {
|
||||
// 路径规范化
|
||||
const normalizedPath = normalizePath(path)
|
||||
// 路径规范化(处理反斜杠并统一为正斜杠)
|
||||
const normalizedPath = normalizePathSeparators(path)
|
||||
filePath.value = normalizedPath
|
||||
|
||||
// 添加到历史记录
|
||||
@@ -177,11 +182,10 @@ export function usePathNavigation(options: UsePathNavigationOptions = {}) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 路径规范化(统一分隔符)
|
||||
* 路径规范化(统一为正斜杠)
|
||||
*/
|
||||
const normalizePath = (path: string): string => {
|
||||
if (!path) return ''
|
||||
return path.replace(/\\/g, '/')
|
||||
return normalizePathSeparators(path)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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"
|
||||
@exit-zip="handleExitZip"
|
||||
@go-to-path="handleGoToPath"
|
||||
@open-file="handleOpenFile"
|
||||
@navigate-to-zip-directory="handleNavigateToZipDirectory"
|
||||
@show-message="handleShowMessage"
|
||||
/>
|
||||
@@ -118,7 +119,9 @@ import { useCommonPaths } from './composables/useCommonPaths'
|
||||
|
||||
// 导入工具函数
|
||||
import { getFileName, sortFileList } from '@/utils/fileUtils'
|
||||
import { getParentPath } from '@/utils/pathHelpers'
|
||||
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'
|
||||
|
||||
// 导入类型
|
||||
@@ -345,6 +348,35 @@ const handleGoToPath = async (path: string) => {
|
||||
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) => {
|
||||
// 暂时不处理 ZIP
|
||||
}
|
||||
@@ -359,7 +391,7 @@ const handleShowMessage = (message: string, type: 'success' | 'error' | 'warning
|
||||
|
||||
// 侧边栏事件
|
||||
const handleOpenFavorite = async (file: FavoriteFile) => {
|
||||
if (file.is_dir) {
|
||||
if (file.isDir) {
|
||||
await navigate(file.path)
|
||||
} else {
|
||||
await selectFile(file.path)
|
||||
@@ -416,7 +448,7 @@ const handleFileClick = async (file: FileItem) => {
|
||||
*/
|
||||
|
||||
// 正常文件系统浏览
|
||||
if (file.is_dir) {
|
||||
if (file.isDir) {
|
||||
// 目录:使用 navigate 函数,确保历史记录正确更新
|
||||
await navigate(file.path)
|
||||
} else {
|
||||
@@ -427,7 +459,7 @@ const handleFileClick = async (file: FileItem) => {
|
||||
}
|
||||
|
||||
const handleFileDoubleClick = async (file: FileItem) => {
|
||||
if (file.is_dir) {
|
||||
if (file.isDir) {
|
||||
await navigate(file.path)
|
||||
} else {
|
||||
// 检查是否为 ZIP 文件 - 暂时禁用
|
||||
@@ -535,7 +567,7 @@ const handleSaveEditing = async (oldPath: string, newName: string) => {
|
||||
// 如果重命名的是当前打开的文件,先关闭编辑器和预览
|
||||
if (selectedFileItem.value?.path === oldPath) {
|
||||
// 如果是文件(不是文件夹),才需要关闭编辑器
|
||||
if (!selectedFileItem.value.is_dir) {
|
||||
if (!selectedFileItem.value.isDir) {
|
||||
// 清空编辑器内容
|
||||
await clearContent()
|
||||
|
||||
@@ -580,7 +612,7 @@ const handleSaveEditing = async (oldPath: string, newName: string) => {
|
||||
errorMsg.includes('being used by another process') ||
|
||||
errorMsg.includes('被另一个进程占用')) {
|
||||
errorMsg = '文件正在被其他程序使用,请先关闭该文件后重试'
|
||||
if (selectedFileItem.value?.is_dir) {
|
||||
if (selectedFileItem.value?.isDir) {
|
||||
errorMsg = '文件夹正在被其他程序使用(如文件管理器、终端等),请先关闭后重试'
|
||||
}
|
||||
} else if (errorMsg.includes('access is denied') ||
|
||||
@@ -728,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) {
|
||||
Message.error(`文件 "${fileName}" 已存在`)
|
||||
// 重新显示对话框
|
||||
@@ -784,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) {
|
||||
Message.error(`文件夹 "${folderName}" 已存在`)
|
||||
// 重新显示对话框
|
||||
@@ -822,7 +854,7 @@ const validateFileName = (name: string): boolean => {
|
||||
*/
|
||||
const handleDeleteFile = async (file: FileItem) => {
|
||||
const targetPath = file.path
|
||||
const isDirectory = file.is_dir
|
||||
const isDirectory = file.isDir
|
||||
const fileName = file.name || targetPath
|
||||
|
||||
// 根据类型显示不同的确认信息
|
||||
@@ -919,12 +951,12 @@ const isMediaPreviewable = (filename: string): boolean => {
|
||||
}
|
||||
|
||||
const selectFile = async (path: string) => {
|
||||
// 标准化路径进行比较(处理正斜杠/反斜杠不一致的问题)
|
||||
const normalizedPath = path.replace(/\\/g, '/').toLowerCase()
|
||||
// 后端已统一返回 / 路径,直接比较
|
||||
const normalizedPath = path.toLowerCase()
|
||||
|
||||
// 尝试在当前文件列表中查找
|
||||
const file = fileList.value.find(f => {
|
||||
const normalizedFilePath = f.path.replace(/\\/g, '/').toLowerCase()
|
||||
const normalizedFilePath = f.path.toLowerCase()
|
||||
return normalizedFilePath === normalizedPath
|
||||
})
|
||||
|
||||
@@ -938,7 +970,7 @@ const selectFile = async (path: string) => {
|
||||
selectedFileItem.value = {
|
||||
path,
|
||||
name: fileName,
|
||||
is_dir: false,
|
||||
isDir: false,
|
||||
size: 0,
|
||||
mod_time: '',
|
||||
is_favorite: isFavorite(path)
|
||||
@@ -1055,7 +1087,7 @@ const loadZipDirectoryContents = async (zipPath: string, currentDir: string): Pr
|
||||
const result = filtered.map((f: any) => ({
|
||||
name: f.name,
|
||||
path: f.path,
|
||||
is_dir: f.is_dir,
|
||||
isDir: f.isDir,
|
||||
size: f.size || 0,
|
||||
mod_time: f.mod_time || '',
|
||||
is_favorite: false
|
||||
|
||||
@@ -1,26 +1,21 @@
|
||||
<template>
|
||||
<a-tooltip :content="tooltipText" position="bottom">
|
||||
<a-tooltip :content="themeStore.tooltipText" position="bottom">
|
||||
<div
|
||||
class="theme-toggle-btn"
|
||||
@click="handleToggle"
|
||||
>
|
||||
{{ isDark ? '🌙' : '☀️' }}
|
||||
{{ themeStore.isDark ? '🌙' : '☀️' }}
|
||||
</div>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useTheme } from '../composables/useTheme'
|
||||
import { useThemeStore } from '../stores/theme'
|
||||
|
||||
const { isDark, toggleTheme } = useTheme()
|
||||
|
||||
const tooltipText = computed(() => {
|
||||
return isDark.value ? '切换到亮色主题' : '切换到夜间主题'
|
||||
})
|
||||
const themeStore = useThemeStore()
|
||||
|
||||
const handleToggle = () => {
|
||||
toggleTheme()
|
||||
themeStore.toggleTheme()
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
</template>
|
||||
|
||||
<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 { useUpdateStore } from '../stores/update'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
@@ -17,21 +18,10 @@ const props = defineProps({
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'skip'])
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
// State
|
||||
const downloading = ref(false)
|
||||
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 最小更新间隔
|
||||
// 使用更新管理 store
|
||||
const updateStore = useUpdateStore()
|
||||
|
||||
// 模态框实例
|
||||
let confirmModalInstance = null
|
||||
@@ -53,22 +43,6 @@ watch(() => props.modelValue, (val) => {
|
||||
})
|
||||
|
||||
// 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) => {
|
||||
if (!dateStr) return ''
|
||||
try {
|
||||
@@ -126,7 +100,7 @@ const showUpdateModal = () => {
|
||||
metadata.push(formatDate(props.updateInfo.release_date))
|
||||
}
|
||||
if (props.updateInfo?.file_size) {
|
||||
metadata.push(formatFileSize(props.updateInfo.file_size))
|
||||
metadata.push(updateStore.formatFileSize(props.updateInfo.file_size))
|
||||
}
|
||||
if (metadata.length > 0) {
|
||||
elements.push(
|
||||
@@ -164,7 +138,6 @@ const showUpdateModal = () => {
|
||||
onCancel: () => {
|
||||
confirmModalInstance = null
|
||||
emit('update:modelValue', false)
|
||||
emit('skip')
|
||||
},
|
||||
onBeforeCancel: () => {
|
||||
if (forceUpdate.value) {
|
||||
@@ -178,47 +151,51 @@ const showUpdateModal = () => {
|
||||
|
||||
// 生成进度弹窗内容
|
||||
const getProgressModalContent = () => {
|
||||
if (downloading.value) {
|
||||
// 后端返回的 progress 是 0-100,Arco Progress 组件期望 0-1
|
||||
const progressValue = Number(Math.min(100, Math.max(0, downloadProgress.value || 0)))
|
||||
// 下载中状态
|
||||
if (updateStore.downloading) {
|
||||
const progressValue = Math.min(100, Math.max(0, updateStore.downloadProgress || 0))
|
||||
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 [
|
||||
h('div', { style: { marginBottom: '16px' } }, [
|
||||
h('div', { style: { marginBottom: '8px', fontSize: '14px', color: 'var(--color-text-2)' } }, '正在下载更新包...')
|
||||
]),
|
||||
h('div', { style: { marginBottom: '8px' } }, [
|
||||
h(Progress, {
|
||||
percent: finalProgress,
|
||||
showText: true
|
||||
})
|
||||
h(Progress, { percent: finalProgress, showText: true })
|
||||
]),
|
||||
h('div', { style: { fontSize: '12px', color: 'var(--color-text-3)', marginTop: '8px' } }, [
|
||||
progressInfo.value.total > 0
|
||||
? `${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
|
||||
h('div', { style: { fontSize: '12px', color: 'var(--color-text-3)', marginTop: '8px' } }, sizeText),
|
||||
speedElement
|
||||
]
|
||||
} else if (installing.value) {
|
||||
}
|
||||
|
||||
// 安装中状态
|
||||
if (updateStore.installing) {
|
||||
return [
|
||||
h('div', { style: { marginBottom: '16px' } }, [
|
||||
h('div', { style: { marginBottom: '8px', fontSize: '14px', color: 'var(--color-text-2)' } }, '正在安装更新...')
|
||||
]),
|
||||
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
|
||||
}
|
||||
|
||||
downloading.value = true
|
||||
installing.value = false
|
||||
downloadProgress.value = 0
|
||||
progressInfo.value = { speed: 0, downloaded: 0, total: 0 }
|
||||
|
||||
progressModalInstance = Modal.info({
|
||||
title: '更新进度',
|
||||
content: () => getProgressModalContent(),
|
||||
@@ -252,8 +224,16 @@ const showProgressModal = async () => {
|
||||
|
||||
await nextTick()
|
||||
|
||||
// 监听 store 状态变化
|
||||
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 () => {
|
||||
await nextTick(updateProgressModal)
|
||||
},
|
||||
@@ -292,112 +272,49 @@ const handleDownload = async () => {
|
||||
|
||||
try {
|
||||
const result = await window.go.main.App.DownloadUpdate(props.updateInfo.download_url)
|
||||
if (!result.success) {
|
||||
closeProgressModal()
|
||||
Message.error(result.message || '下载启动失败')
|
||||
downloading.value = false
|
||||
}
|
||||
if (result.success) return
|
||||
|
||||
closeProgressModal()
|
||||
Message.error(result.message || '下载启动失败')
|
||||
} catch (error) {
|
||||
console.error('下载失败:', error)
|
||||
closeProgressModal()
|
||||
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 data = parseEventData(event)
|
||||
const data = typeof event === 'string' ? JSON.parse(event) : event
|
||||
|
||||
if (data.error) {
|
||||
closeProgressModal()
|
||||
Message.error('下载失败:' + data.error)
|
||||
downloading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
if (!data.success || !data.file_path) {
|
||||
closeProgressModal()
|
||||
Message.error('下载完成但数据不完整')
|
||||
downloading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
downloadProgress.value = Math.min(100, Math.max(0, 100))
|
||||
progressInfo.value.downloaded = data.file_size || 0
|
||||
progressInfo.value.total = data.file_size || 0
|
||||
await nextTick(updateProgressModal)
|
||||
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))
|
||||
}
|
||||
// 等待安装完成
|
||||
await new Promise(resolve => setTimeout(resolve, 3000))
|
||||
closeProgressModal()
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
// 监听下载完成事件(仅用于关闭弹窗)
|
||||
if (window.runtime?.EventsOn) {
|
||||
window.runtime.EventsOn('download-progress', onDownloadProgress)
|
||||
window.runtime.EventsOn('download-complete', onDownloadComplete)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (window.runtime?.EventsOff) {
|
||||
window.runtime.EventsOff('download-progress')
|
||||
window.runtime.EventsOff('download-complete')
|
||||
}
|
||||
closeModals()
|
||||
|
||||
@@ -79,10 +79,22 @@
|
||||
</div>
|
||||
</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 style="font-size: 11px; color: #999; margin-bottom: 8px;">
|
||||
进度条已显示:downloadProgress={{ downloadProgress }}, downloading={{ downloading }}
|
||||
</div>
|
||||
<a-progress
|
||||
:percent="downloadProgress / 100"
|
||||
:percent="downloadProgress"
|
||||
:status="downloadStatus"
|
||||
/>
|
||||
<div class="progress-info">
|
||||
@@ -109,66 +121,37 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from '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'
|
||||
|
||||
// 工具函数:解析事件数据
|
||||
const parseEventData = (event) => {
|
||||
try {
|
||||
return typeof event === 'string' ? JSON.parse(event) : event
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
// 使用更新管理 store
|
||||
const updateStore = useUpdateStore()
|
||||
|
||||
// 状态
|
||||
// 使用 storeToRefs 解构以保持响应性
|
||||
const { checking, downloading, installing, downloadProgress, downloadStatus, progressInfo, updateInfo } = storeToRefs(updateStore)
|
||||
|
||||
// 本地状态
|
||||
const currentVersion = 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 downloadProgress = ref(0)
|
||||
const downloadStatus = ref('active')
|
||||
const downloadedFile = ref(null)
|
||||
|
||||
// 配置
|
||||
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) => {
|
||||
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]
|
||||
return updateStore.formatFileSize(bytes)
|
||||
}
|
||||
|
||||
// 格式化速度
|
||||
const formatSpeed = (bytesPerSecond) => {
|
||||
return formatFileSize(bytesPerSecond) + '/s'
|
||||
return updateStore.formatSpeed(bytesPerSecond)
|
||||
}
|
||||
|
||||
// 加载当前版本
|
||||
const loadCurrentVersion = async () => {
|
||||
try {
|
||||
const result = await window.go.main.App.GetCurrentVersion()
|
||||
if (result.success) {
|
||||
currentVersion.value = result.data?.version || '-'
|
||||
}
|
||||
if (!result.success) return
|
||||
|
||||
currentVersion.value = result.data?.version || '-'
|
||||
} catch (error) {
|
||||
console.error('获取版本失败:', error)
|
||||
}
|
||||
@@ -178,114 +161,30 @@ const loadCurrentVersion = async () => {
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
const result = await window.go.main.App.GetUpdateConfig()
|
||||
if (result.success) {
|
||||
config.value = {
|
||||
auto_check_enabled: result.data.auto_check_enabled || false,
|
||||
check_interval_minutes: result.data.check_interval_minutes || 60,
|
||||
check_url: result.data.check_url || ''
|
||||
}
|
||||
lastCheckTime.value = result.data.last_check_time || '-'
|
||||
}
|
||||
if (!result.success) return
|
||||
|
||||
const { last_check_time = '-' } = result.data || {}
|
||||
lastCheckTime.value = last_check_time
|
||||
} catch (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 () => {
|
||||
checking.value = true
|
||||
updateInfo.value = null
|
||||
installResult.value = null
|
||||
|
||||
try {
|
||||
const result = await window.go.main.App.CheckUpdate()
|
||||
if (result.success) {
|
||||
updateInfo.value = result.data
|
||||
if (result.data.has_update) {
|
||||
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()
|
||||
}
|
||||
// 使用 store 的检查方法(非静默模式,显示消息)
|
||||
await updateStore.checkForUpdates(false)
|
||||
|
||||
// 刷新最后检查时间
|
||||
await loadConfig()
|
||||
}
|
||||
|
||||
// 下载更新
|
||||
const handleDownload = async () => {
|
||||
if (!updateInfo.value?.download_url) {
|
||||
Message.warning('下载地址不存在')
|
||||
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
|
||||
}
|
||||
// 使用 store 的下载方法,会自动管理状态和事件监听
|
||||
await updateStore.downloadUpdate()
|
||||
}
|
||||
|
||||
// 安装更新
|
||||
@@ -295,7 +194,6 @@ const handleInstall = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
// 确认对话框
|
||||
Modal.confirm({
|
||||
title: '确认安装',
|
||||
content: '安装更新后应用将自动重启,是否继续?',
|
||||
@@ -304,27 +202,24 @@ const handleInstall = async () => {
|
||||
installResult.value = null
|
||||
|
||||
try {
|
||||
const result = await window.go.main.App.InstallUpdate(
|
||||
downloadedFile.value,
|
||||
true // 自动重启
|
||||
)
|
||||
const result = await window.go.main.App.InstallUpdate(downloadedFile.value, true)
|
||||
installResult.value = result.data || result
|
||||
|
||||
if (result.success || result.data?.success) {
|
||||
Message.success({
|
||||
content: '安装成功!应用将在几秒后重启...',
|
||||
duration: 3000
|
||||
})
|
||||
} else {
|
||||
const success = result.success || result.data?.success
|
||||
if (!success) {
|
||||
Message.error(result.message || '安装失败')
|
||||
return
|
||||
}
|
||||
|
||||
Message.success({
|
||||
content: '安装成功!应用将在几秒后重启...',
|
||||
duration: 3000
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('安装失败:', error)
|
||||
installResult.value = {
|
||||
success: false,
|
||||
message: '安装失败:' + (error.message || error)
|
||||
}
|
||||
Message.error('安装失败:' + (error.message || error))
|
||||
const errorMsg = '安装失败:' + (error.message || error)
|
||||
installResult.value = { success: false, message: errorMsg }
|
||||
Message.error(errorMsg)
|
||||
} finally {
|
||||
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) => {
|
||||
downloading.value = false
|
||||
const data = parseEventData(event)
|
||||
const data = typeof event === 'string' ? JSON.parse(event) : event
|
||||
|
||||
if (data.error) {
|
||||
downloadStatus.value = 'exception'
|
||||
Message.error('下载失败:' + data.error)
|
||||
} else if (data.success) {
|
||||
downloadStatus.value = 'success'
|
||||
downloadProgress.value = 100
|
||||
if (data.success && data.file_path) {
|
||||
downloadedFile.value = data.file_path
|
||||
Message.success('下载完成!文件已保存到:' + data.file_path)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,9 +240,8 @@ onMounted(async () => {
|
||||
await loadCurrentVersion()
|
||||
await loadConfig()
|
||||
|
||||
// 监听下载进度事件
|
||||
// 监听下载完成事件(仅用于记录文件路径)
|
||||
if (window.runtime?.EventsOn) {
|
||||
window.runtime.EventsOn('download-progress', onDownloadProgress)
|
||||
window.runtime.EventsOn('download-complete', onDownloadComplete)
|
||||
}
|
||||
})
|
||||
@@ -377,14 +249,8 @@ onMounted(async () => {
|
||||
onUnmounted(() => {
|
||||
// 取消事件监听
|
||||
if (window.runtime?.EventsOff) {
|
||||
window.runtime.EventsOff('download-progress')
|
||||
window.runtime.EventsOff('download-complete')
|
||||
}
|
||||
|
||||
// 清除定时器
|
||||
if (saveTimer) {
|
||||
clearTimeout(saveTimer)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -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 ArcoVue from '@arco-design/web-vue'
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
// Arco Design 样式(组件按需自动引入)
|
||||
import '@arco-design/web-vue/dist/arco.css'
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
import {initTheme} from './composables/useTheme'
|
||||
import { useThemeStore } from './stores/theme'
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(ArcoVue)
|
||||
const pinia = createPinia()
|
||||
|
||||
// 在应用挂载前初始化主题
|
||||
initTheme()
|
||||
app.use(pinia)
|
||||
|
||||
// 在应用挂载前初始化主题(需要先初始化 Pinia)
|
||||
const themeStore = useThemeStore()
|
||||
themeStore.initTheme()
|
||||
|
||||
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
|
||||
}
|
||||
@@ -48,4 +48,39 @@ body {
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
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,7 +14,7 @@ export interface FileItem {
|
||||
/** 文件大小(字节) */
|
||||
size: number
|
||||
/** 是否为目录 */
|
||||
is_dir: boolean
|
||||
isDir: boolean
|
||||
/** 修改时间 */
|
||||
modified_time?: 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))
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
* @description 提供文件相关的通用工具函数,避免代码重复
|
||||
*/
|
||||
|
||||
import { normalizePathSeparators } from './pathHelpers.js'
|
||||
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) {
|
||||
if (!path) return ''
|
||||
|
||||
// 统一分隔符为正斜杠
|
||||
const normalizedPath = path.replace(/\\/g, '/')
|
||||
|
||||
// 分割路径并取最后一部分
|
||||
const parts = normalizedPath.split('/')
|
||||
// 后端已统一返回 / 路径,直接分割
|
||||
const parts = path.split('/')
|
||||
|
||||
return parts[parts.length - 1] || path
|
||||
}
|
||||
@@ -157,7 +155,7 @@ export function isPdfFile(path) {
|
||||
*/
|
||||
export function normalizeFilePath(path, encode = false) {
|
||||
if (!path) return ''
|
||||
const normalized = path.replace(/\\/g, '/')
|
||||
const normalized = normalizePathSeparators(path)
|
||||
|
||||
// 如果需要编码,则使用 encodeURIComponent
|
||||
if (encode) {
|
||||
@@ -327,18 +325,17 @@ export function sanitizeFileName(filename, replacement = '_') {
|
||||
* @returns {Array} 排序后的文件列表
|
||||
*
|
||||
* @example
|
||||
* sortFileList([{name: 'b.txt', is_dir: false}, {name: 'a', is_dir: true}])
|
||||
* // [{name: 'a', is_dir: true}, {name: 'b.txt', is_dir: false}]
|
||||
* sortFileList([{name: 'b.txt', isDir: false}, {name: 'a', isDir: true}])
|
||||
* // [{name: 'a', isDir: true}, {name: 'b.txt', isDir: false}]
|
||||
*/
|
||||
export function sortFileList(fileList) {
|
||||
if (!Array.isArray(fileList)) return fileList
|
||||
|
||||
return fileList.sort((a, b) => {
|
||||
// 如果都是目录或都是文件,按名称排序
|
||||
if (a.is_dir === b.is_dir) {
|
||||
// API 层已转换,直接使用 isDir
|
||||
if (a.isDir === b.isDir) {
|
||||
return a.name.localeCompare(b.name)
|
||||
}
|
||||
// 目录优先
|
||||
return a.is_dir ? -1 : 1
|
||||
return a.isDir ? -1 : 1
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,35 +1,67 @@
|
||||
import { marked } from 'marked'
|
||||
import hljs from 'highlight.js'
|
||||
import mermaid from 'mermaid'
|
||||
|
||||
// 导入 highlight.js 核心和两种主题样式
|
||||
import 'highlight.js/lib/common'
|
||||
import 'highlight.js/styles/github-dark.css'
|
||||
import 'highlight.js/styles/github.css'
|
||||
|
||||
// Mermaid 初始化
|
||||
mermaid.initialize({ startOnLoad: false, theme: 'default', securityLevel: 'loose' })
|
||||
let mermaidInstance: typeof import('mermaid').default | null = null
|
||||
|
||||
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()
|
||||
|
||||
renderer.code = function(token: any) {
|
||||
// Mermaid 代码块
|
||||
if (token.lang === 'mermaid') {
|
||||
return `<pre class="mermaid">${token.text}</pre>`
|
||||
}
|
||||
|
||||
// 普通代码块 - 使用 highlight.js 高亮
|
||||
const lang = hljs.getLanguage(token.lang) ? token.lang : 'plaintext'
|
||||
const highlighted = hljs.highlight(token.text, { language: lang }).value
|
||||
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 })
|
||||
|
||||
export { marked }
|
||||
|
||||
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
|
||||
* getParentPath('C:\\Users\\file.txt') // 'C:\\Users'
|
||||
* getParentPath('/home/user/file.txt') // '/home/user'
|
||||
* getParentPath('E:/file.txt') // 'E:/'
|
||||
*/
|
||||
export const getParentPath = (path) => {
|
||||
if (!path) return ''
|
||||
|
||||
// 查找最后一个分隔符的位置
|
||||
const lastSep = Math.max(
|
||||
path.lastIndexOf('/'),
|
||||
path.lastIndexOf('\\')
|
||||
)
|
||||
// 规范化路径分隔符
|
||||
const normalizedPath = path.replace(/\\/g, '/')
|
||||
|
||||
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
|
||||
|
||||
tableScrollHeight.value = Math.max(minHeight, maxHeight)
|
||||
|
||||
console.log('表格高度计算:', {
|
||||
containerHeight,
|
||||
paginationHeight,
|
||||
tableHeaderHeight,
|
||||
availableHeight,
|
||||
final: tableScrollHeight.value
|
||||
})
|
||||
}, 150)
|
||||
}
|
||||
|
||||
|
||||
@@ -34,11 +34,8 @@ export function useStructureState() {
|
||||
dbType: 'mysql' | 'mongo' | 'redis',
|
||||
nodeType: string
|
||||
) => {
|
||||
console.log('🟢 loadStructure 开始:', { connectionId, database, tableName, dbType, nodeType })
|
||||
|
||||
// 对于连接和数据库节点,不需要加载结构
|
||||
if (nodeType === 'connection' || nodeType === 'database') {
|
||||
console.log('🟡 跳过:节点类型为连接或数据库')
|
||||
structureInfo.value = {
|
||||
connectionId,
|
||||
database,
|
||||
@@ -52,7 +49,6 @@ export function useStructureState() {
|
||||
|
||||
// 如果没有表名,不加载(但保留 structureInfo 用于显示提示)
|
||||
if (!tableName) {
|
||||
console.log('🟡 跳过:表名为空')
|
||||
structureInfo.value = {
|
||||
connectionId,
|
||||
database,
|
||||
@@ -78,14 +74,8 @@ export function useStructureState() {
|
||||
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
|
||||
|
||||
|
||||
// 确保 structureInfo 也设置了
|
||||
structureInfo.value = {
|
||||
connectionId,
|
||||
@@ -94,20 +84,6 @@ export function useStructureState() {
|
||||
dbType,
|
||||
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) {
|
||||
console.error('加载表结构失败:', error)
|
||||
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 vue from '@vitejs/plugin-vue'
|
||||
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({
|
||||
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: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src')
|
||||
}
|
||||
alias: { '@': resolve(__dirname, 'src') }
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
sourcemap: false, // 生产环境禁用 source map,减小打包体积
|
||||
sourcemap: false,
|
||||
minify: 'esbuild',
|
||||
cssCodeSplit: true,
|
||||
chunkSizeWarningLimit: 1000,
|
||||
esbuild: {
|
||||
target: 'es2020',
|
||||
drop: ['console', 'debugger']
|
||||
},
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
'codemirror': [
|
||||
'@codemirror/view',
|
||||
'@codemirror/state',
|
||||
'@codemirror/language',
|
||||
'@codemirror/commands',
|
||||
'@codemirror/lang-javascript',
|
||||
'@codemirror/lang-java',
|
||||
'@codemirror/lang-python',
|
||||
'@codemirror/lang-html',
|
||||
'@codemirror/lang-css',
|
||||
'@codemirror/lang-markdown',
|
||||
'@codemirror/lang-sql'
|
||||
]
|
||||
}
|
||||
manualChunks: (id) => {
|
||||
if (!id.includes('node_modules')) return
|
||||
|
||||
if (id.includes('@codemirror')) {
|
||||
if (id.includes('lang-') || id.includes('legacy-modes')) {
|
||||
return 'vendor-codemirror-langs'
|
||||
}
|
||||
return 'vendor-codemirror-core'
|
||||
}
|
||||
|
||||
if (id.includes('@arco-design')) return 'vendor-arco'
|
||||
if (id.includes('mermaid')) return 'vendor-mermaid'
|
||||
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: {
|
||||
include: [
|
||||
'@codemirror/view',
|
||||
'@codemirror/state',
|
||||
'@codemirror/language',
|
||||
'@codemirror/commands',
|
||||
'@codemirror/lang-javascript',
|
||||
'@codemirror/lang-java',
|
||||
'@codemirror/lang-python',
|
||||
'@codemirror/lang-html',
|
||||
'@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'
|
||||
'vue', 'pinia', '@arco-design/web-vue', 'marked', 'highlight.js',
|
||||
'@codemirror/view', '@codemirror/state', '@codemirror/language', '@codemirror/commands',
|
||||
'@codemirror/lang-javascript', '@codemirror/lang-json', '@codemirror/lang-yaml',
|
||||
'@codemirror/lang-html', '@codemirror/lang-css', '@codemirror/lang-markdown',
|
||||
'@codemirror/lang-sql', '@codemirror/lang-java', '@codemirror/lang-python',
|
||||
'@codemirror/lang-php', '@codemirror/lang-rust', '@codemirror/lang-go', '@codemirror/lang-cpp',
|
||||
'@codemirror/legacy-modes/mode/clike', '@codemirror/legacy-modes/mode/ruby',
|
||||
'@codemirror/legacy-modes/mode/shell', '@codemirror/legacy-modes/mode/xml'
|
||||
]
|
||||
}
|
||||
},
|
||||
cacheDir: 'node_modules/.vite'
|
||||
})
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user