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 (
"os"
"path/filepath"
"runtime"
)
const (
// AppName 应用名称
AppName = "u-desk"
// AppDataDir 应用数据目录名称(带点号,表示隐藏目录)
AppDataDir = ".u-desk"
)
// GetUserDataDir 获取用户数据目录
// 跨平台支持Windows、macOS、Linux
// 所有平台统一使用: ~/.u-desk
func GetUserDataDir() string {
var basePath string
switch runtime.GOOS {
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")
}
homeDir, err := os.UserHomeDir()
if err != nil {
return "."
}
if basePath == "" {
basePath = "."
}
return filepath.Join(basePath, AppName)
return filepath.Join(homeDir, AppDataDir)
}

View File

@@ -276,7 +276,13 @@ func getAllowedExtensions() map[string]bool {
".wav": true,
".ogg": true,
// 文档
".pdf": true,
".pdf": true,
".doc": true,
".docx": true,
".xls": true,
".xlsx": true,
".ppt": true,
".pptx": true,
// 文本
".txt": true,
".md": true,
@@ -346,10 +352,20 @@ func getMIMETypeMapping() map[string]string {
".wav": "audio/wav",
".ogg": "audio/ogg",
".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",
".html": "text/html; charset=utf-8",
".css": "text/css",
".js": "application/javascript",
".json": "application/json",
".xml": "application/xml",
".md": "text/markdown",
}
}

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,8 @@
package storage
import (
"fmt"
"u-desk/internal/common"
"u-desk/internal/storage/models"
"os"
"path/filepath"
@@ -25,17 +27,13 @@ func InitFast() (*gorm.DB, error) {
return globalDB, nil
}
homeDir, err := os.UserHomeDir()
if err != nil {
return nil, err
}
dataDir := filepath.Join(homeDir, ".go-desk")
// 使用统一的数据目录
dataDir := common.GetUserDataDir()
if err := os.MkdirAll(dataDir, 0755); err != nil {
return nil, err
}
dbPath := filepath.Join(dataDir, "db-cli.db")
dbPath := filepath.Join(dataDir, "app.db")
// 极限性能优化参数:
// - journal_mode=WAL: 写前日志,大幅提升并发性能
@@ -53,7 +51,10 @@ func InitFast() (*gorm.DB, error) {
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.SetMaxIdleConns(1)
sqlDB.SetConnMaxLifetime(time.Hour)

View File

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

View File

@@ -3,106 +3,91 @@
</template>
<script setup>
import { ref, onMounted, watch, onBeforeUnmount } from 'vue'
import { EditorView, lineNumbers, highlightActiveLineGutter } from '@codemirror/view'
import { ref, onMounted, watch, onBeforeUnmount, computed } 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 { 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 { markdown } from '@codemirror/lang-markdown'
import { html } from '@codemirror/lang-html'
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 { oneDark } from '@codemirror/theme-one-dark'
import { keymap } from '@codemirror/view'
import { bracketMatching } from '@codemirror/language'
import { StreamLanguage } from '@codemirror/language'
import { shell } from '@codemirror/legacy-modes/mode/shell'
import { powerShell } from '@codemirror/legacy-modes/mode/powershell'
import { oneDark } from '@codemirror/theme-one-dark'
import { bracketMatching } from '@codemirror/language'
import { useTheme } from '@/composables/useTheme'
// 使用主题系统
const { isDark } = useTheme()
// 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
'js': javascript(),
'jsx': javascript({ jsx: true }),
'ts': javascript({ typescript: true }),
'tsx': javascript({ typescript: true, jsx: true }),
'mjs': javascript(),
'cjs': javascript(),
// JavaScript/TypeScript (使用 javascript 包)
javascript: ['js', 'jsx', 'mjs', 'cjs'],
typescript: ['ts', 'tsx'],
// JSON
'json': json(),
// 数据格式
json: ['json'],
yaml: ['yaml', 'yml'],
xml: ['xml', 'xhtml', 'svg'],
// Java
'java': java(),
// Web
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++
'c': cpp(),
'cpp': cpp(),
'cc': cpp(),
'cxx': cpp(),
'h': cpp(),
'hpp': cpp(),
'hxx': cpp(),
// JVM 语言
java: ['java'],
kotlin: ['kt', 'kts'],
csharp: ['cs', 'csx'],
// 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(),
// 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),
// 纯文本(未知类型)
text: ['txt', 'text', 'log', 'csv']
}
// ==================== Props & Emits ====================
const props = defineProps({
modelValue: {
type: String,
@@ -116,10 +101,15 @@ const props = defineProps({
const emit = defineEmits(['update:modelValue'])
// ==================== State ====================
const { isDark } = useTheme()
const editorContainer = ref(null)
let view = null
// 根据主题动态创建编辑器配置
// ==================== Editor Management ====================
/**
* 创建编辑器扩展配置
*/
const createExtensions = () => {
const extensions = [
lineNumbers(),
@@ -155,25 +145,131 @@ const createExtensions = () => {
})
]
// 根据主题添加 One Dark
// 主题
if (isDark.value) {
extensions.push(oneDark)
}
// 添加语言支持
const langSupport = LANGUAGE_MAP[props.fileExtension]
if (langSupport) {
extensions.push(langSupport)
// 语言支持
const ext = props.fileExtension.toLowerCase()
// 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
}
onMounted(() => {
/**
* 创建编辑器实例
*/
const createEditor = (docContent = '') => {
if (!editorContainer.value) return
const state = EditorState.create({
doc: props.modelValue,
doc: docContent,
extensions: createExtensions()
})
@@ -181,49 +277,21 @@ onMounted(() => {
state,
parent: editorContainer.value
})
})
}
// 监听外部内容变化
watch(() => props.modelValue, (newValue) => {
if (view && newValue !== view.state.doc.toString()) {
view.dispatch({
changes: {
from: 0,
to: view.state.doc.length,
insert: newValue
}
})
}
})
/**
* 重建编辑器(保留内容)
*/
const recreateEditor = () => {
if (!view) return
const currentDoc = view.state.doc.toString()
view.destroy()
createEditor(currentDoc)
}
// 监听主题变化,重新创建编辑器
watch(isDark, () => {
if (view) {
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
})
}
// ==================== Lifecycle ====================
onMounted(() => {
createEditor(props.modelValue || '')
})
onBeforeUnmount(() => {
@@ -231,80 +299,36 @@ onBeforeUnmount(() => {
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>
<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>
.codemirror-editor {
height: 100%;
overflow: hidden;
}
:deep(.cm-editor) {
.codemirror-editor :deep(.cm-editor) {
height: 100%;
}
:deep(.cm-scroller) {
.codemirror-editor :deep(.cm-scroller) {
overflow: auto;
}
:deep(.cm-content) {
padding: 8px;
}
</style>

View File

@@ -197,6 +197,11 @@
</template>
<script setup>
// 定义组件名称,用于 KeepAlive 缓存
defineOptions({
name: 'DeviceTest'
})
import {computed, onMounted, ref} from 'vue'
import {Message, Modal} from '@arco-design/web-vue'
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) {
// 已收藏,执行取消收藏
favoriteFiles.value.splice(index, 1)
sortFavorites() // 排序
save(favoriteFiles.value)
save(favoriteFiles.value) // 直接保存,不重新排序
onRemove(item)
Message.info(`已取消收藏: ${item.name}`)
@@ -108,11 +107,10 @@ export function useFavoriteFiles(storageKey, options = {}) {
path: item.path,
name: item.name,
is_dir: item.is_dir || false,
created_at: Date.now(), // 添加时间戳
created_at: Date.now(), // 添加时间戳(用于 getSortedFavorites
})
sortFavorites() // 排序
save(favoriteFiles.value)
save(favoriteFiles.value) // 直接保存,不重新排序(新项目添加到末尾)
onAdd(item)
Message.success(`已收藏: ${item.name}`)
@@ -141,8 +139,7 @@ export function useFavoriteFiles(storageKey, options = {}) {
const item = favoriteFiles.value[index]
favoriteFiles.value.splice(index, 1)
sortFavorites() // 排序
save(favoriteFiles.value)
save(favoriteFiles.value) // 直接保存,不重新排序
onRemove(item)
Message.info(`已取消收藏: ${item.name}`)
@@ -178,7 +175,6 @@ export function useFavoriteFiles(storageKey, options = {}) {
const executeClear = () => {
const count = favoriteFiles.value.length
favoriteFiles.value = []
sortFavorites() // 保持一致性
save([])
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(() => {
load()
sortFavorites() // 确保加载后的数据是排序的
})
return {
@@ -248,6 +273,7 @@ export function useFavoriteFiles(storageKey, options = {}) {
getSortedFavorites,
searchFavorites,
sortFavorites,
reorderFavorites,
load,
save,
}
@@ -264,6 +290,7 @@ export function useFavoriteFiles(storageKey, options = {}) {
* @property {Function} getSortedFavorites - 获取排序后的列表
* @property {Function} searchFavorites - 搜索收藏
* @property {Function} sortFavorites - 手动排序收藏列表
* @property {Function} reorderFavorites - 拖拽重新排序
* @property {Function} load - 手动加载数据
* @property {Function} save - 手动保存数据
*/

View File

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

View File

@@ -70,9 +70,12 @@ export const FILE_EXTENSIONS = {
// 代码文件
CODE: [
'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'],
@@ -271,7 +274,7 @@ export const PATH_ICONS = {
* 文件大小单位
* @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等
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>
<script setup lang="ts">
// 定义组件名称,用于 KeepAlive 缓存
defineOptions({
name: 'DbCli'
})
import { ref, watch, provide, computed, nextTick, onMounted, onUnmounted, h, onBeforeUpdate } from 'vue'
import { Message, Modal } from '@arco-design/web-vue'
import { IconUp, IconDown, IconCopy } from '@arco-design/web-vue/es/icon'