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