From eb2cbad17b3dce1ac70668e5189e48b4433d74e0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com>
Date: Fri, 30 Jan 2026 02:24:09 +0800
Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=EF=BC=9A=E4=BB=A3=E7=A0=81?=
=?UTF-8?q?=E8=B4=A8=E9=87=8F=E6=8F=90=E5=8D=87=EF=BC=8C=E4=BF=AE=E5=A4=8D?=
=?UTF-8?q?=E9=87=8D=E5=A4=8D=E9=80=BB=E8=BE=91=E5=92=8C=E8=AF=AD=E6=B3=95?=
=?UTF-8?q?=E9=AB=98=E4=BA=AE=E6=94=AF=E6=8C=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 简化计算属性,删除重复代码
- 优化文件扩展名获取逻辑
- 新增文件工具函数库 fileHelpers.js
- 增强 CodeEditor 语法高亮(支持 30+ 语言)
- 修复 Office 文档文件服务器访问权限
- 添加特殊文件名支持(Dockerfile、Makefile 等)
---
internal/common/path.go | 35 +-
internal/filesystem/config.go | 18 +-
internal/service/update.go | 11 +-
internal/service/update_config.go | 13 +-
internal/service/update_download.go | 66 +-
internal/storage/sqlite.go | 17 +-
web/src/App.vue | 9 +-
web/src/components/CodeEditor.vue | 406 +++++----
web/src/components/DeviceTest.vue | 5 +
web/src/components/FileSystem.vue | 1031 ++++++++++++----------
web/src/composables/useFavoriteFiles.js | 47 +-
web/src/composables/useFileOperations.js | 3 +-
web/src/utils/constants.js | 16 +-
web/src/utils/fileHelpers.js | 41 +
web/src/views/db-cli/index.vue | 5 +
15 files changed, 962 insertions(+), 761 deletions(-)
create mode 100644 web/src/utils/fileHelpers.js
diff --git a/internal/common/path.go b/internal/common/path.go
index ab50513..850f818 100644
--- a/internal/common/path.go
+++ b/internal/common/path.go
@@ -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)
}
diff --git a/internal/filesystem/config.go b/internal/filesystem/config.go
index 56db2a1..3f67549 100644
--- a/internal/filesystem/config.go
+++ b/internal/filesystem/config.go
@@ -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",
}
}
diff --git a/internal/service/update.go b/internal/service/update.go
index d63b755..5d3e7cd 100644
--- a/internal/service/update.go
+++ b/internal/service/update.go
@@ -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 {
diff --git a/internal/service/update_config.go b/internal/service/update_config.go
index 191f5d1..c5405ea 100644
--- a/internal/service/update_config.go
+++ b/internal/service/update_config.go
@@ -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 加载更新配置
diff --git a/internal/service/update_download.go b/internal/service/update_download.go
index 77788b3..c633df5 100644
--- a/internal/service/update_download.go
+++ b/internal/service/update_download.go
@@ -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
}
diff --git a/internal/storage/sqlite.go b/internal/storage/sqlite.go
index c6f0020..4508c53 100644
--- a/internal/storage/sqlite.go
+++ b/internal/storage/sqlite.go
@@ -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)
diff --git a/web/src/App.vue b/web/src/App.vue
index 12b2429..0fe3f2a 100644
--- a/web/src/App.vue
+++ b/web/src/App.vue
@@ -49,11 +49,10 @@
-
-
-
-
-
+
+
+
+
diff --git a/web/src/components/CodeEditor.vue b/web/src/components/CodeEditor.vue
index 3e64e80..e65e684 100644
--- a/web/src/components/CodeEditor.vue
+++ b/web/src/components/CodeEditor.vue
@@ -3,106 +3,91 @@
-
-
diff --git a/web/src/components/DeviceTest.vue b/web/src/components/DeviceTest.vue
index f183e83..593b615 100644
--- a/web/src/components/DeviceTest.vue
+++ b/web/src/components/DeviceTest.vue
@@ -197,6 +197,11 @@
@@ -3475,6 +3539,25 @@ onUnmounted(() => {
opacity: 1;
}
+/* 拖拽样式 */
+.sidebar-item-dragging {
+ opacity: 0.5;
+ background: var(--color-fill-3);
+ cursor: grabbing !important;
+ transform: scale(0.98);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+}
+
+.sidebar-item-drag-over {
+ border: 2px dashed var(--color-primary-light-3);
+ background: var(--color-fill-1);
+}
+
+/* 防止拖拽时显示删除按钮 */
+.sidebar-item-dragging .sidebar-item-remove {
+ opacity: 0 !important;
+}
+
.sidebar-empty {
display: flex;
flex-direction: column;
@@ -3997,6 +4080,18 @@ onUnmounted(() => {
justify-content: center;
}
+.media-meta .file-name {
+ font-weight: 500;
+ color: var(--color-text-2);
+}
+
+.media-meta .image-dimensions {
+ padding: 2px 8px;
+ background: var(--color-fill-2);
+ border-radius: 4px;
+ font-family: monospace;
+}
+
/* ========== 文本编辑器 ========== */
.text-editor-wrapper {
display: flex;
diff --git a/web/src/composables/useFavoriteFiles.js b/web/src/composables/useFavoriteFiles.js
index b154a48..1d218bf 100644
--- a/web/src/composables/useFavoriteFiles.js
+++ b/web/src/composables/useFavoriteFiles.js
@@ -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 - 手动保存数据
*/
diff --git a/web/src/composables/useFileOperations.js b/web/src/composables/useFileOperations.js
index bf0c19f..a1946d9 100644
--- a/web/src/composables/useFileOperations.js
+++ b/web/src/composables/useFileOperations.js
@@ -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
diff --git a/web/src/utils/constants.js b/web/src/utils/constants.js
index 31a2e3f..35ed718 100644
--- a/web/src/utils/constants.js
+++ b/web/src/utils/constants.js
@@ -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 - 文本文件最大显示大小
+}
diff --git a/web/src/utils/fileHelpers.js b/web/src/utils/fileHelpers.js
new file mode 100644
index 0000000..c1d4a60
--- /dev/null
+++ b/web/src/utils/fileHelpers.js
@@ -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)
diff --git a/web/src/views/db-cli/index.vue b/web/src/views/db-cli/index.vue
index 7d2d668..6dc4ed1 100644
--- a/web/src/views/db-cli/index.vue
+++ b/web/src/views/db-cli/index.vue
@@ -129,6 +129,11 @@