Private
Public Access
1
0

优化:代码质量提升,修复重复逻辑和语法高亮支持

- 简化计算属性,删除重复代码
- 优化文件扩展名获取逻辑
- 新增文件工具函数库 fileHelpers.js
- 增强 CodeEditor 语法高亮(支持 30+ 语言)
- 修复 Office 文档文件服务器访问权限
- 添加特殊文件名支持(Dockerfile、Makefile 等)
This commit is contained in:
2026-01-30 02:24:09 +08:00
parent b849e6cc46
commit eb2cbad17b
15 changed files with 962 additions and 761 deletions

View File

@@ -3,43 +3,24 @@ package common
import ( import (
"os" "os"
"path/filepath" "path/filepath"
"runtime"
) )
const ( const (
// AppName 应用名称 // AppName 应用名称
AppName = "u-desk" AppName = "u-desk"
// AppDataDir 应用数据目录名称(带点号,表示隐藏目录)
AppDataDir = ".u-desk"
) )
// GetUserDataDir 获取用户数据目录 // GetUserDataDir 获取用户数据目录
// 跨平台支持Windows、macOS、Linux // 跨平台支持Windows、macOS、Linux
// 所有平台统一使用: ~/.u-desk
func GetUserDataDir() string { func GetUserDataDir() string {
var basePath string homeDir, err := os.UserHomeDir()
if err != nil {
switch runtime.GOOS { return "."
case "windows":
// Windows: %LOCALAPPDATA% 或 %APPDATA%
basePath = os.Getenv("LOCALAPPDATA")
if basePath == "" {
basePath = os.Getenv("APPDATA")
}
case "darwin":
// macOS: ~/Library/Application Support
homeDir, err := os.UserHomeDir()
if err == nil {
basePath = filepath.Join(homeDir, "Library", "Application Support")
}
default:
// Linux: ~/.config
homeDir, err := os.UserHomeDir()
if err == nil {
basePath = filepath.Join(homeDir, ".config")
}
} }
if basePath == "" { return filepath.Join(homeDir, AppDataDir)
basePath = "."
}
return filepath.Join(basePath, AppName)
} }

View File

@@ -276,7 +276,13 @@ func getAllowedExtensions() map[string]bool {
".wav": true, ".wav": true,
".ogg": true, ".ogg": true,
// 文档 // 文档
".pdf": true, ".pdf": true,
".doc": true,
".docx": true,
".xls": true,
".xlsx": true,
".ppt": true,
".pptx": true,
// 文本 // 文本
".txt": true, ".txt": true,
".md": true, ".md": true,
@@ -346,10 +352,20 @@ func getMIMETypeMapping() map[string]string {
".wav": "audio/wav", ".wav": "audio/wav",
".ogg": "audio/ogg", ".ogg": "audio/ogg",
".pdf": "application/pdf", ".pdf": "application/pdf",
// Office 文档
".doc": "application/msword",
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
".xls": "application/vnd.ms-excel",
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
".ppt": "application/vnd.ms-powerpoint",
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
// 文本
".txt": "text/plain; charset=utf-8", ".txt": "text/plain; charset=utf-8",
".html": "text/html; charset=utf-8", ".html": "text/html; charset=utf-8",
".css": "text/css", ".css": "text/css",
".js": "application/javascript", ".js": "application/javascript",
".json": "application/json", ".json": "application/json",
".xml": "application/xml",
".md": "text/markdown",
} }
} }

View File

@@ -12,6 +12,8 @@ import (
"runtime" "runtime"
"strings" "strings"
"time" "time"
"u-desk/internal/common"
) )
// ==================== 类型定义 ==================== // ==================== 类型定义 ====================
@@ -409,18 +411,13 @@ func BackupApplication() (string, error) {
return "", err return "", err
} }
homeDir, err := os.UserHomeDir() backupDir := filepath.Join(common.GetUserDataDir(), "backups")
if err != nil {
return "", fmt.Errorf("获取用户目录失败: %v", err)
}
backupDir := filepath.Join(homeDir, ".go-desk", "backups")
if err := os.MkdirAll(backupDir, 0755); err != nil { if err := os.MkdirAll(backupDir, 0755); err != nil {
return "", fmt.Errorf("创建备份目录失败: %v", err) return "", fmt.Errorf("创建备份目录失败: %v", err)
} }
timestamp := time.Now().Format("20060102-150405") timestamp := time.Now().Format("20060102-150405")
backupFileName := fmt.Sprintf("go-desk-backup-%s%s", timestamp, filepath.Ext(execPath)) backupFileName := fmt.Sprintf("u-desk-backup-%s%s", timestamp, filepath.Ext(execPath))
backupPath := filepath.Join(backupDir, backupFileName) backupPath := filepath.Join(backupDir, backupFileName)
if err := copyFile(execPath, backupPath); err != nil { if err := copyFile(execPath, backupPath); err != nil {

View File

@@ -7,6 +7,8 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"time" "time"
"u-desk/internal/common"
) )
// UpdateConfig 更新配置 // UpdateConfig 更新配置
@@ -20,17 +22,12 @@ type UpdateConfig struct {
// GetUpdateConfigPath 获取更新配置文件路径 // GetUpdateConfigPath 获取更新配置文件路径
func GetUpdateConfigPath() (string, error) { func GetUpdateConfigPath() (string, error) {
homeDir, err := os.UserHomeDir() dataDir := common.GetUserDataDir()
if err != nil { if err := os.MkdirAll(dataDir, 0755); err != nil {
return "", fmt.Errorf("获取用户目录失败: %v", err)
}
configDir := filepath.Join(homeDir, ".go-desk")
if err := os.MkdirAll(configDir, 0755); err != nil {
return "", fmt.Errorf("创建配置目录失败: %v", err) return "", fmt.Errorf("创建配置目录失败: %v", err)
} }
return filepath.Join(configDir, "update_config.json"), nil return filepath.Join(dataDir, "update_config.json"), nil
} }
// LoadUpdateConfig 加载更新配置 // LoadUpdateConfig 加载更新配置

View File

@@ -5,12 +5,15 @@ import (
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"hash"
"io" "io"
"log" "log"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"time" "time"
"u-desk/internal/common"
) )
// ==================== 类型定义 ==================== // ==================== 类型定义 ====================
@@ -33,12 +36,7 @@ func DownloadUpdate(downloadURL string, progressCallback DownloadProgress) (*Dow
log.Printf("[下载] 开始下载URL: %s", downloadURL) log.Printf("[下载] 开始下载URL: %s", downloadURL)
// 获取下载目录 // 获取下载目录
homeDir, err := os.UserHomeDir() downloadDir := filepath.Join(common.GetUserDataDir(), "downloads")
if err != nil {
return nil, fmt.Errorf("获取用户目录失败: %v", err)
}
downloadDir := filepath.Join(homeDir, ".go-desk", "downloads")
if err := os.MkdirAll(downloadDir, 0755); err != nil { if err := os.MkdirAll(downloadDir, 0755); err != nil {
return nil, fmt.Errorf("创建下载目录失败: %v", err) return nil, fmt.Errorf("创建下载目录失败: %v", err)
} }
@@ -283,7 +281,33 @@ func normalizeProgress(progress float64) float64 {
return progress return progress
} }
// calculateFileHashes 计算文件的 MD5 和 SHA256 哈希值 // calculateHash 计算文件的哈希值(通用函数)
func calculateHash(filePath string, hashType string) (string, error) {
file, err := os.Open(filePath)
if err != nil {
return "", err
}
defer file.Close()
var hash hash.Hash
switch hashType {
case "md5":
hash = md5.New()
case "sha256":
hash = sha256.New()
default:
return "", fmt.Errorf("不支持的哈希类型: %s", hashType)
}
if _, err := io.Copy(hash, file); err != nil {
return "", err
}
return hex.EncodeToString(hash.Sum(nil)), nil
}
// calculateFileHashes 计算文件的 MD5 和 SHA256 哈希值(优化版,使用 MultiWriter
func calculateFileHashes(filePath string) (string, string, error) { func calculateFileHashes(filePath string) (string, string, error) {
file, err := os.Open(filePath) file, err := os.Open(filePath)
if err != nil { if err != nil {
@@ -294,7 +318,7 @@ func calculateFileHashes(filePath string) (string, string, error) {
md5Hash := md5.New() md5Hash := md5.New()
sha256Hash := sha256.New() sha256Hash := sha256.New()
// 使用 MultiWriter 同时计算两个哈希 // 使用 MultiWriter 同时计算两个哈希,只读取文件一次
writer := io.MultiWriter(md5Hash, sha256Hash) writer := io.MultiWriter(md5Hash, sha256Hash)
if _, err := io.Copy(writer, file); err != nil { if _, err := io.Copy(writer, file); err != nil {
@@ -309,33 +333,9 @@ func calculateFileHashes(filePath string) (string, string, error) {
// VerifyFileHash 验证文件哈希值 // VerifyFileHash 验证文件哈希值
func VerifyFileHash(filePath string, expectedHash string, hashType string) (bool, error) { func VerifyFileHash(filePath string, expectedHash string, hashType string) (bool, error) {
file, err := os.Open(filePath) calculatedHash, err := calculateHash(filePath, hashType)
if err != nil { if err != nil {
return false, err return false, err
} }
defer file.Close()
var hash []byte
var calculatedHash string
switch hashType {
case "md5":
md5Hash := md5.New()
if _, err := io.Copy(md5Hash, file); err != nil {
return false, err
}
hash = md5Hash.Sum(nil)
calculatedHash = hex.EncodeToString(hash)
case "sha256":
sha256Hash := sha256.New()
if _, err := io.Copy(sha256Hash, file); err != nil {
return false, err
}
hash = sha256Hash.Sum(nil)
calculatedHash = hex.EncodeToString(hash)
default:
return false, fmt.Errorf("不支持的哈希类型: %s", hashType)
}
return calculatedHash == expectedHash, nil return calculatedHash == expectedHash, nil
} }

View File

@@ -1,6 +1,8 @@
package storage package storage
import ( import (
"fmt"
"u-desk/internal/common"
"u-desk/internal/storage/models" "u-desk/internal/storage/models"
"os" "os"
"path/filepath" "path/filepath"
@@ -25,17 +27,13 @@ func InitFast() (*gorm.DB, error) {
return globalDB, nil return globalDB, nil
} }
homeDir, err := os.UserHomeDir() // 使用统一的数据目录
if err != nil { dataDir := common.GetUserDataDir()
return nil, err
}
dataDir := filepath.Join(homeDir, ".go-desk")
if err := os.MkdirAll(dataDir, 0755); err != nil { if err := os.MkdirAll(dataDir, 0755); err != nil {
return nil, err return nil, err
} }
dbPath := filepath.Join(dataDir, "db-cli.db") dbPath := filepath.Join(dataDir, "app.db")
// 极限性能优化参数: // 极限性能优化参数:
// - journal_mode=WAL: 写前日志,大幅提升并发性能 // - journal_mode=WAL: 写前日志,大幅提升并发性能
@@ -53,7 +51,10 @@ func InitFast() (*gorm.DB, error) {
return nil, err return nil, err
} }
sqlDB, _ := db.DB() sqlDB, err := db.DB()
if err != nil {
return nil, fmt.Errorf("获取底层SQL数据库失败: %v", err)
}
sqlDB.SetMaxOpenConns(1) // SQLite 只需要一个连接 sqlDB.SetMaxOpenConns(1) // SQLite 只需要一个连接
sqlDB.SetMaxIdleConns(1) sqlDB.SetMaxIdleConns(1)
sqlDB.SetConnMaxLifetime(time.Hour) sqlDB.SetConnMaxLifetime(time.Hour)

View File

@@ -49,11 +49,10 @@
</a-layout-header> </a-layout-header>
<a-layout-content class="content"> <a-layout-content class="content">
<!-- 动态渲染 Tab 内容 --> <!-- 动态渲染 Tab 内容 -->
<template v-for="tab in visibleTabs" :key="tab.key"> <!-- 使用 KeepAlive 缓存组件状态避免切换时重新加载 -->
<KeepAlive> <KeepAlive include="FileSystem,DbCli,DeviceTest">
<component :is="getComponent(tab.key)" v-if="activeTab === tab.key" /> <component :is="getComponent(activeTab)" />
</KeepAlive> </KeepAlive>
</template>
</a-layout-content> </a-layout-content>
<!-- 设置抽屉 --> <!-- 设置抽屉 -->

View File

@@ -3,106 +3,91 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, watch, onBeforeUnmount } from 'vue' import { ref, onMounted, watch, onBeforeUnmount, computed } from 'vue'
import { EditorView, lineNumbers, highlightActiveLineGutter } from '@codemirror/view' import { EditorView, lineNumbers, highlightActiveLineGutter, keymap } from '@codemirror/view'
import { EditorState } from '@codemirror/state' import { EditorState } from '@codemirror/state'
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands' import { defaultKeymap, history, historyKeymap } from '@codemirror/commands'
import { javascript } from '@codemirror/lang-javascript' import { javascript } from '@codemirror/lang-javascript'
import { java } from '@codemirror/lang-java'
import { python } from '@codemirror/lang-python'
import { go } from '@codemirror/lang-go'
import { cpp } from '@codemirror/lang-cpp'
import { rust } from '@codemirror/lang-rust'
import { php } from '@codemirror/lang-php'
import { json } from '@codemirror/lang-json' import { json } from '@codemirror/lang-json'
import { markdown } from '@codemirror/lang-markdown' import { cpp } from '@codemirror/lang-cpp'
import { html } from '@codemirror/lang-html'
import { css } from '@codemirror/lang-css' 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 { sql } from '@codemirror/lang-sql'
import { yaml } from '@codemirror/lang-yaml' import { yaml } from '@codemirror/lang-yaml'
import { oneDark } from '@codemirror/theme-one-dark'
import { keymap } from '@codemirror/view'
import { bracketMatching } from '@codemirror/language'
import { StreamLanguage } from '@codemirror/language' import { StreamLanguage } from '@codemirror/language'
import { shell } from '@codemirror/legacy-modes/mode/shell' import { oneDark } from '@codemirror/theme-one-dark'
import { powerShell } from '@codemirror/legacy-modes/mode/powershell' import { bracketMatching } from '@codemirror/language'
import { useTheme } from '@/composables/useTheme' import { useTheme } from '@/composables/useTheme'
// 使用主题系统 // Legacy modes for languages without dedicated packages
const { isDark } = useTheme() 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 = { const LANGUAGE_MAP = {
// JavaScript/TypeScript // JavaScript/TypeScript (使用 javascript 包)
'js': javascript(), javascript: ['js', 'jsx', 'mjs', 'cjs'],
'jsx': javascript({ jsx: true }), typescript: ['ts', 'tsx'],
'ts': javascript({ typescript: true }),
'tsx': javascript({ typescript: true, jsx: true }),
'mjs': javascript(),
'cjs': javascript(),
// JSON // 数据格式
'json': json(), json: ['json'],
yaml: ['yaml', 'yml'],
xml: ['xml', 'xhtml', 'svg'],
// Java // Web
'java': java(), html: ['html', 'htm'],
css: ['css', 'scss', 'sass', 'less'],
// Python // 系统编程
'py': python(), cpp: ['cpp', 'c', 'cc', 'cxx', 'h', 'hpp'],
rust: ['rs'],
go: ['go'],
// Go // 脚本语言
'go': go(), python: ['py', 'pyw'],
php: ['php'],
ruby: ['rb'],
perl: ['pl', 'pm'],
shell: ['sh', 'bash', 'zsh', 'fish', 'cmd', 'bat'],
sql: ['sql'],
// C/C++ // JVM 语言
'c': cpp(), java: ['java'],
'cpp': cpp(), kotlin: ['kt', 'kts'],
'cc': cpp(), csharp: ['cs', 'csx'],
'cxx': cpp(),
'h': cpp(),
'hpp': cpp(),
'hxx': cpp(),
// Rust // 其他语言
'rs': rust(), 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'],
// PHP // 纯文本(未知类型)
'php': php(), text: ['txt', 'text', 'log', 'csv']
// Markdown
'md': markdown(),
'markdown': markdown(),
// HTML
'html': html(),
'htm': html(),
// CSS
'css': css(),
'scss': css(),
'sass': css(),
'less': css(),
// SQL
'sql': sql(),
// YAML
'yml': yaml(),
'yaml': yaml(),
// Shell/Bash
'sh': StreamLanguage.define(shell),
'bash': StreamLanguage.define(shell),
'zsh': StreamLanguage.define(shell),
'fish': StreamLanguage.define(shell),
// Windows Batch/PowerShell
'bat': StreamLanguage.define(powerShell),
'cmd': StreamLanguage.define(powerShell),
'ps1': StreamLanguage.define(powerShell),
} }
// ==================== Props & Emits ====================
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
type: String, type: String,
@@ -116,10 +101,15 @@ const props = defineProps({
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
// ==================== State ====================
const { isDark } = useTheme()
const editorContainer = ref(null) const editorContainer = ref(null)
let view = null let view = null
// 根据主题动态创建编辑器配置 // ==================== Editor Management ====================
/**
* 创建编辑器扩展配置
*/
const createExtensions = () => { const createExtensions = () => {
const extensions = [ const extensions = [
lineNumbers(), lineNumbers(),
@@ -155,25 +145,131 @@ const createExtensions = () => {
}) })
] ]
// 根据主题添加 One Dark // 主题
if (isDark.value) { if (isDark.value) {
extensions.push(oneDark) extensions.push(oneDark)
} }
// 添加语言支持 // 语言支持
const langSupport = LANGUAGE_MAP[props.fileExtension] const ext = props.fileExtension.toLowerCase()
if (langSupport) {
extensions.push(langSupport) // JavaScript/TypeScript
if (LANGUAGE_MAP.javascript.includes(ext) || LANGUAGE_MAP.typescript.includes(ext)) {
extensions.push(javascript({ jsx: true }))
} }
// JSON
else if (LANGUAGE_MAP.json.includes(ext)) {
extensions.push(json())
}
// YAML
else if (LANGUAGE_MAP.yaml.includes(ext)) {
extensions.push(yaml())
}
// HTML
else if (LANGUAGE_MAP.html.includes(ext)) {
extensions.push(html())
}
// CSS (including SCSS, SASS, LESS)
else if (LANGUAGE_MAP.css.includes(ext)) {
extensions.push(css())
}
// C/C++
else if (LANGUAGE_MAP.cpp.includes(ext)) {
extensions.push(cpp())
}
// Rust
else if (LANGUAGE_MAP.rust.includes(ext)) {
extensions.push(rust())
}
// Go
else if (LANGUAGE_MAP.go.includes(ext)) {
extensions.push(go())
}
// Python
else if (LANGUAGE_MAP.python.includes(ext)) {
extensions.push(python())
}
// PHP
else if (LANGUAGE_MAP.php.includes(ext)) {
extensions.push(php())
}
// SQL
else if (LANGUAGE_MAP.sql.includes(ext)) {
extensions.push(sql())
}
// Markdown
else if (LANGUAGE_MAP.markdown.includes(ext)) {
extensions.push(markdown())
}
// Java
else if (LANGUAGE_MAP.java.includes(ext)) {
extensions.push(java())
}
// Ruby
else if (LANGUAGE_MAP.ruby.includes(ext)) {
extensions.push(StreamLanguage.define(ruby))
}
// Shell
else if (LANGUAGE_MAP.shell.includes(ext)) {
extensions.push(StreamLanguage.define(shell))
}
// Kotlin
else if (LANGUAGE_MAP.kotlin.includes(ext)) {
extensions.push(StreamLanguage.define(kotlin))
}
// C#
else if (LANGUAGE_MAP.csharp.includes(ext)) {
extensions.push(StreamLanguage.define(csharp))
}
// Swift
else if (LANGUAGE_MAP.swift.includes(ext)) {
extensions.push(StreamLanguage.define(swift))
}
// R
else if (LANGUAGE_MAP.r.includes(ext)) {
extensions.push(StreamLanguage.define(r))
}
// Perl
else if (LANGUAGE_MAP.perl.includes(ext)) {
extensions.push(StreamLanguage.define(perl))
}
// LaTeX
else if (LANGUAGE_MAP.latex.includes(ext)) {
extensions.push(StreamLanguage.define(stex))
}
// Makefile (使用纯文本legacy-modes 没有专门的 makefile 支持)
else if (LANGUAGE_MAP.makefile.includes(ext)) {
// 纯文本模式,不添加语言扩展
}
// INI/Properties/Dockerfile
else if (LANGUAGE_MAP.ini.includes(ext)) {
extensions.push(StreamLanguage.define(properties))
}
else if (LANGUAGE_MAP.dockerfile.includes(ext)) {
extensions.push(StreamLanguage.define(dockerFile))
}
// XML (包括 SVG)
else if (LANGUAGE_MAP.xml.includes(ext)) {
extensions.push(StreamLanguage.define(xml))
}
// Matlab/Octave
else if (LANGUAGE_MAP.matlab.includes(ext)) {
extensions.push(StreamLanguage.define(octave))
}
// 其他类型(包括 gitignore, dockerfile, txt 等)使用纯文本模式
// 不添加任何语言扩展,保持纯文本
return extensions return extensions
} }
onMounted(() => { /**
* 创建编辑器实例
*/
const createEditor = (docContent = '') => {
if (!editorContainer.value) return if (!editorContainer.value) return
const state = EditorState.create({ const state = EditorState.create({
doc: props.modelValue, doc: docContent,
extensions: createExtensions() extensions: createExtensions()
}) })
@@ -181,49 +277,21 @@ onMounted(() => {
state, state,
parent: editorContainer.value parent: editorContainer.value
}) })
}) }
// 监听外部内容变化 /**
watch(() => props.modelValue, (newValue) => { * 重建编辑器(保留内容)
if (view && newValue !== view.state.doc.toString()) { */
view.dispatch({ const recreateEditor = () => {
changes: { if (!view) return
from: 0, const currentDoc = view.state.doc.toString()
to: view.state.doc.length, view.destroy()
insert: newValue createEditor(currentDoc)
} }
})
}
})
// 监听主题变化,重新创建编辑器 // ==================== Lifecycle ====================
watch(isDark, () => { onMounted(() => {
if (view) { createEditor(props.modelValue || '')
view.destroy()
const state = EditorState.create({
doc: view.state.doc.toString(),
extensions: createExtensions()
})
view = new EditorView({
state,
parent: editorContainer.value
})
}
})
// 监听文件扩展名变化,重新创建编辑器
watch(() => props.fileExtension, () => {
if (view) {
view.destroy()
const state = EditorState.create({
doc: view.state.doc.toString(),
extensions: createExtensions()
})
view = new EditorView({
state,
parent: editorContainer.value
})
}
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
@@ -231,80 +299,36 @@ onBeforeUnmount(() => {
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 || ''
}
})
}
})
// 监听主题或文件扩展名变化,重建编辑器
watch([isDark, () => props.fileExtension], recreateEditor)
</script> </script>
<style>
/* 全局语法高亮样式(适用于亮色主题) */
.cm-editor:not(.cm-theme-dark) .tok-keyword {
color: #d73a49;
font-weight: bold;
}
.cm-editor:not(.cm-theme-dark) .tok-string {
color: #032f62;
}
.cm-editor:not(.cm-theme-dark) .tok-number {
color: #005cc5;
}
.cm-editor:not(.cm-theme-dark) .tok-comment {
color: #6a737d;
font-style: italic;
}
.cm-editor:not(.cm-theme-dark) .tok-function {
color: #6f42c1;
}
.cm-editor:not(.cm-theme-dark) .tok-operator {
color: #d73a49;
}
.cm-editor:not(.cm-theme-dark) .tok-class-name {
color: #22863a;
}
.cm-editor:not(.cm-theme-dark) .tok-property {
color: #e36209;
}
.cm-editor:not(.cm-theme-dark) .tok-tag {
color: #22863a;
}
.cm-editor:not(.cm-theme-dark) .tok-attribute {
color: #6f42c1;
}
.cm-editor:not(.cm-theme-dark) .tok-variableName {
color: #e36209;
}
.cm-editor:not(.cm-theme-dark) .tok-variableName2 {
color: #005cc5;
}
.cm-editor:not(.cm-theme-dark) .tok-def {
color: #6f42c1;
}
</style>
<style scoped> <style scoped>
.codemirror-editor { .codemirror-editor {
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
} }
:deep(.cm-editor) { .codemirror-editor :deep(.cm-editor) {
height: 100%; height: 100%;
} }
:deep(.cm-scroller) { .codemirror-editor :deep(.cm-scroller) {
overflow: auto; overflow: auto;
} }
:deep(.cm-content) {
padding: 8px;
}
</style> </style>

View File

@@ -197,6 +197,11 @@
</template> </template>
<script setup> <script setup>
// 定义组件名称,用于 KeepAlive 缓存
defineOptions({
name: 'DeviceTest'
})
import {computed, onMounted, ref} from 'vue' import {computed, onMounted, ref} from 'vue'
import {Message, Modal} from '@arco-design/web-vue' import {Message, Modal} from '@arco-design/web-vue'
import { import {

File diff suppressed because it is too large Load Diff

View File

@@ -91,8 +91,7 @@ export function useFavoriteFiles(storageKey, options = {}) {
if (index > -1) { if (index > -1) {
// 已收藏,执行取消收藏 // 已收藏,执行取消收藏
favoriteFiles.value.splice(index, 1) favoriteFiles.value.splice(index, 1)
sortFavorites() // 排序 save(favoriteFiles.value) // 直接保存,不重新排序
save(favoriteFiles.value)
onRemove(item) onRemove(item)
Message.info(`已取消收藏: ${item.name}`) Message.info(`已取消收藏: ${item.name}`)
@@ -108,11 +107,10 @@ export function useFavoriteFiles(storageKey, options = {}) {
path: item.path, path: item.path,
name: item.name, name: item.name,
is_dir: item.is_dir || false, is_dir: item.is_dir || false,
created_at: Date.now(), // 添加时间戳 created_at: Date.now(), // 添加时间戳(用于 getSortedFavorites
}) })
sortFavorites() // 排序 save(favoriteFiles.value) // 直接保存,不重新排序(新项目添加到末尾)
save(favoriteFiles.value)
onAdd(item) onAdd(item)
Message.success(`已收藏: ${item.name}`) Message.success(`已收藏: ${item.name}`)
@@ -141,8 +139,7 @@ export function useFavoriteFiles(storageKey, options = {}) {
const item = favoriteFiles.value[index] const item = favoriteFiles.value[index]
favoriteFiles.value.splice(index, 1) favoriteFiles.value.splice(index, 1)
sortFavorites() // 排序 save(favoriteFiles.value) // 直接保存,不重新排序
save(favoriteFiles.value)
onRemove(item) onRemove(item)
Message.info(`已取消收藏: ${item.name}`) Message.info(`已取消收藏: ${item.name}`)
@@ -178,7 +175,6 @@ export function useFavoriteFiles(storageKey, options = {}) {
const executeClear = () => { const executeClear = () => {
const count = favoriteFiles.value.length const count = favoriteFiles.value.length
favoriteFiles.value = [] favoriteFiles.value = []
sortFavorites() // 保持一致性
save([]) save([])
Message.success(`已清空 ${count} 个收藏项`) Message.success(`已清空 ${count} 个收藏项`)
@@ -229,10 +225,39 @@ export function useFavoriteFiles(storageKey, options = {}) {
) )
} }
// 组件挂载时加载数据并排序 /**
* 重新排序收藏列表(拖拽排序)
* @param {number} fromIndex - 源索引
* @param {number} toIndex - 目标索引
* @returns {boolean} 是否成功重排序
*/
const reorderFavorites = (fromIndex, toIndex) => {
if (!Array.isArray(favoriteFiles.value)) {
return false
}
if (fromIndex < 0 || fromIndex >= favoriteFiles.value.length ||
toIndex < 0 || toIndex >= favoriteFiles.value.length) {
return false
}
if (fromIndex === toIndex) {
return false
}
// 移动数组元素
const [movedItem] = favoriteFiles.value.splice(fromIndex, 1)
favoriteFiles.value.splice(toIndex, 0, movedItem)
// 保存新顺序
save(favoriteFiles.value)
return true
}
// 组件挂载时加载数据(不自动排序,保持用户拖拽的顺序)
onMounted(() => { onMounted(() => {
load() load()
sortFavorites() // 确保加载后的数据是排序的
}) })
return { return {
@@ -248,6 +273,7 @@ export function useFavoriteFiles(storageKey, options = {}) {
getSortedFavorites, getSortedFavorites,
searchFavorites, searchFavorites,
sortFavorites, sortFavorites,
reorderFavorites,
load, load,
save, save,
} }
@@ -264,6 +290,7 @@ export function useFavoriteFiles(storageKey, options = {}) {
* @property {Function} getSortedFavorites - 获取排序后的列表 * @property {Function} getSortedFavorites - 获取排序后的列表
* @property {Function} searchFavorites - 搜索收藏 * @property {Function} searchFavorites - 搜索收藏
* @property {Function} sortFavorites - 手动排序收藏列表 * @property {Function} sortFavorites - 手动排序收藏列表
* @property {Function} reorderFavorites - 拖拽重新排序
* @property {Function} load - 手动加载数据 * @property {Function} load - 手动加载数据
* @property {Function} save - 手动保存数据 * @property {Function} save - 手动保存数据
*/ */

View File

@@ -107,7 +107,8 @@ export function useFileOperations(options = {}) {
return true return true
} catch (error) { } catch (error) {
onError('listDirectory', error) onError('listDirectory', error)
Message.error(`列出目录失败: ${error.message || error}`) const errorMsg = error.message || error || '未知错误'
Message.error(`列出目录失败 [${targetPath}]: ${errorMsg}`)
return false return false
} finally { } finally {
fileLoading.value = false fileLoading.value = false

View File

@@ -70,9 +70,12 @@ export const FILE_EXTENSIONS = {
// 代码文件 // 代码文件
CODE: [ CODE: [
'js', 'ts', 'jsx', 'tsx', 'vue', 'py', 'java', 'c', 'cpp', 'h', 'go', 'rs', 'php', 'rb', 'cs', 'swift', 'kt', 'js', 'ts', 'jsx', 'tsx', 'vue', 'py', 'java', 'c', 'cpp', 'h', 'go', 'rs', 'php', 'rb', 'cs', 'swift', 'kt',
'scala', 'html', 'htm', 'css', 'scss', 'sass', 'less', 'json', 'xml', 'yaml', 'yml', 'sql', 'sh', 'bat', 'ps1' 'scala', 'css', 'scss', 'sass', 'less', 'json', 'xml', 'yaml', 'yml', 'sql', 'sh', 'bat', 'ps1'
], ],
// 标记语言文件(用于特殊预览)
MARKUP: ['html', 'htm', 'md', 'markdown'],
// 数据库文件 // 数据库文件
DATABASE: ['db', 'sqlite', 'mdb', 'accdb'], DATABASE: ['db', 'sqlite', 'mdb', 'accdb'],
@@ -271,7 +274,7 @@ export const PATH_ICONS = {
* 文件大小单位 * 文件大小单位
* @description 用于文件大小格式化的单位数组 * @description 用于文件大小格式化的单位数组
*/ */
export const BYTE_UNITS = ['B', 'KMGTPE'] export const BYTE_UNITS = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB']
/** /**
* 默认配置值 * 默认配置值
@@ -304,3 +307,12 @@ export const FILE_SIZE_FORMAT = {
UNIT: 1024, // 使用1024进制KiB, MiB等 UNIT: 1024, // 使用1024进制KiB, MiB等
DECIMAL_PLACES: 2, // 保留小数位数 DECIMAL_PLACES: 2, // 保留小数位数
} }
/**
* 文件大小阈值配置
* @description 用于文件处理逻辑的大小限制
*/
export const FILE_SIZE_THRESHOLDS = {
LARGE_FILE: 100 * 1024, // 100KB - 大文件检测阈值
MAX_TEXT_DISPLAY: 5 * 1024 * 1024, // 5MB - 文本文件最大显示大小
}

View File

@@ -0,0 +1,41 @@
/**
* 文件类型工具函数
*/
import { FILE_EXTENSIONS } from './constants'
// 获取文件扩展名
export const getExt = (path) => {
if (!path) return ''
const dot = path.lastIndexOf('.')
const slash = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'))
if (dot === -1 || dot < slash) return ''
return path.slice(dot + 1).toLowerCase()
}
// 文件类型检查
export const isImage = (path) => FILE_EXTENSIONS.IMAGE.includes(getExt(path))
export const isVideo = (path) => FILE_EXTENSIONS.VIDEO_BROWSER.includes(getExt(path))
export const isAudio = (path) => FILE_EXTENSIONS.AUDIO.includes(getExt(path))
export const isPdf = (path) => getExt(path) === 'pdf'
export const isHtml = (path) => { const e = getExt(path); return e === 'html' || e === 'htm' }
export const isMarkdown = (path) => { const e = getExt(path); return e === 'md' || e === 'markdown' }
export const isCode = (path) => FILE_EXTENSIONS.CODE.includes(getExt(path))
export const isArchive = (path) => FILE_EXTENSIONS.ARCHIVE.includes(getExt(path))
export const isDatabase = (path) => FILE_EXTENSIONS.DATABASE.includes(getExt(path))
export const isExecutable = (path) => FILE_EXTENSIONS.EXECUTABLE.includes(getExt(path))
// 复合检查
export const isVideoAny = (path) => {
const e = getExt(path)
return FILE_EXTENSIONS.VIDEO_BROWSER.includes(e) || FILE_EXTENSIONS.VIDEO_EXTERNAL.includes(e)
}
export const isEditableDoc = (path) => {
const e = getExt(path)
return FILE_EXTENSIONS.DOCUMENT.includes(e) && e !== 'pdf'
}
export const isBinary = (path) => isVideoAny(path) || isAudio(path) || isArchive(path) || isExecutable(path)
export const canPreview = (path) => isImage(path) || isVideo(path) || isAudio(path) || isPdf(path)
export const canEdit = (path) => !isBinary(path) && !isImage(path)

View File

@@ -129,6 +129,11 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
// 定义组件名称,用于 KeepAlive 缓存
defineOptions({
name: 'DbCli'
})
import { ref, watch, provide, computed, nextTick, onMounted, onUnmounted, h, onBeforeUpdate } from 'vue' import { ref, watch, provide, computed, nextTick, onMounted, onUnmounted, h, onBeforeUpdate } from 'vue'
import { Message, Modal } from '@arco-design/web-vue' import { Message, Modal } from '@arco-design/web-vue'
import { IconUp, IconDown, IconCopy } from '@arco-design/web-vue/es/icon' import { IconUp, IconDown, IconCopy } from '@arco-design/web-vue/es/icon'