Private
Public Access
1
0

新增:文件系统导航面包屑

功能:
- 新增 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:
2026-02-05 00:17:32 +08:00
parent ce2698f245
commit f7d648ea52
48 changed files with 3930 additions and 1380 deletions

32
app.go
View File

@@ -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("[文件服务器] 已关闭")
}
}

View File

@@ -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"),

View File

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

View File

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

View File

@@ -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,

View File

@@ -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,

View File

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

View File

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

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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"
}
}

View File

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

View File

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

View File

@@ -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>

View File

@@ -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) {

View 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>

View File

@@ -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

View 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>

View File

@@ -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"

View File

@@ -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 {

View File

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

View File

@@ -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)
}
/**

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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-100Arco 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()

View File

@@ -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>

View File

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

View 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

View File

@@ -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
View 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
View 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
View 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
}

View File

@@ -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;
}

View File

@@ -14,7 +14,7 @@ export interface FileItem {
/** 文件大小(字节) */
size: number
/** 是否为目录 */
is_dir: boolean
isDir: boolean
/** 修改时间 */
modified_time?: string
/** 是否被收藏(运行时属性) */

View 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))
}

View File

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

View File

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

View File

@@ -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 || '/'
}
/**

View File

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

View File

@@ -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
View 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>;

View 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);
}

View 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"];
}
}
}

View 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"
}

View 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

View 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);
}

View File

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