From fb12ec48e848489a25125eb893f78715b28838f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Tue, 7 Apr 2026 11:39:50 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E5=A4=A7=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E7=82=B9=E5=87=BB=E5=8D=A1=E6=AD=BB=20+=20Dockerfile?= =?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 - useFileEdit: 新增 KNOWN_BINARY_EXTS 集合,exe/dll/zip 等 28 种二进制扩展名直接判定,不再读取文件内容 - index.vue: loadFileContent 增加大文件预检,基于 fileSize 超过阈值直接拦截 - service.go: ReadFile 增加 10MB 读取上限,超限返回错误 - Dockerfile 支持:CODE 分类、🐳图标、CodeMirror shell 模式高亮、languageMap 映射 --- internal/filesystem/service.go | 12 +++++- internal/service/version.go | 2 +- .../FileSystem/composables/useFileEdit.ts | 26 ++++++++---- web/src/components/FileSystem/index.vue | 42 ++++++++++++++----- web/src/utils/codeMirrorLoader.js | 3 ++ web/src/utils/constants.js | 5 ++- web/src/utils/languageMap.ts | 3 +- 7 files changed, 71 insertions(+), 22 deletions(-) diff --git a/internal/filesystem/service.go b/internal/filesystem/service.go index 5df086d..c3dc866 100644 --- a/internal/filesystem/service.go +++ b/internal/filesystem/service.go @@ -119,13 +119,23 @@ func (s *FileSystemService) Read(path string) (string, error) { return s.ReadFile(path) } -// ReadFile 读取文件内容 +// ReadFile 读取文件内容(限制最大 10MB) func (s *FileSystemService) ReadFile(path string) (string, error) { // 路径验证 if err := s.validatePath(path); err != nil { return "", err } + // 检查文件大小,避免读取超大文件导致内存问题 + info, err := os.Stat(path) + if err != nil { + return "", fmt.Errorf("获取文件信息失败: %v", err) + } + const maxReadSize = 10 * 1024 * 1024 // 10MB + if info.Size() > maxReadSize { + return "", fmt.Errorf("文件过大 (%.1f MB),超过读取上限 (%d MB)", float64(info.Size())/1024/1024, maxReadSize/1024/1024) + } + // 读取文件 data, err := os.ReadFile(path) if err != nil { diff --git a/internal/service/version.go b/internal/service/version.go index c51aa74..939f033 100644 --- a/internal/service/version.go +++ b/internal/service/version.go @@ -14,7 +14,7 @@ import ( // ==================== 常量定义 ==================== // AppVersion 应用版本号(发布时直接修改此处) -const AppVersion = "0.3.2" +const AppVersion = "0.3.3" // 版本号缓存 var ( diff --git a/web/src/components/FileSystem/composables/useFileEdit.ts b/web/src/components/FileSystem/composables/useFileEdit.ts index 7b5b865..ea7eb5e 100644 --- a/web/src/components/FileSystem/composables/useFileEdit.ts +++ b/web/src/components/FileSystem/composables/useFileEdit.ts @@ -69,16 +69,26 @@ export function useFileEdit(options: UseFileEditOptions = {}) { return '' } + // 已知二进制扩展名(无需读取内容即可判定) + const KNOWN_BINARY_EXTS = new Set([ + 'exe', 'dll', 'so', 'bin', 'dat', 'db', 'sqlite', 'pdb', 'idb', + 'lib', 'obj', 'o', 'a', 'class', 'pyc', 'pyo', 'wasm', + 'zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz', 'iso', 'img', 'dmg', + 'msi', 'jar', 'war', 'ear', 'apk' + ]) + /** * 判断是否为二进制文件(基于扩展名) - * 注意:媒体文件(图片、视频、音频、PDF)不是二进制文件,它们可以预览 - * 对于无扩展名的文件,返回 null 表示未知,需要内容检测 + * 返回值: true=已知二进制, false=已知非二进制(文本/媒体/Office), null=未知需检测 */ const isBinaryFileByExt = (filepath: any): boolean | null => { const path = getFilePath(filepath) const ext = getExt(path) if (!ext) return null // 无扩展名返回 null,表示需要进一步检测 + // 已知二进制扩展名 → 直接判定 + if (KNOWN_BINARY_EXTS.has(ext)) return true + // 媒体文件(可预览,不算二进制) const isMediaFile = isImageFile(path) || isVideoFile(path) || @@ -184,8 +194,8 @@ export function useFileEdit(options: UseFileEditOptions = {}) { const filename = getFilePath(path) const ext = getExt(filename) - // Office 文件和 CSV 文件直接读取内容进行预览,跳过二进制检测 - if (isExcelFile(filename) || isWordFile(filename) || isCsvFile(filename)) { + // Office 文件直接读取内容进行预览,跳过二进制检测 + if (isExcelFile(filename) || isWordFile(filename)) { const content = await readFile(path) fileContent.value = content originalContent.value = content @@ -372,9 +382,9 @@ ${ext ? `文件类型: ${fileTypeDesc}\n` : ''} const saveDraft = () => { if (!currentFilePath.value) return - // Office 文件和 CSV 不支持草稿功能 + // Office 文件不支持草稿功能 const path = getFilePath(currentFilePath.value) - if (isExcelFile(path) || isWordFile(path) || isCsvFile(path)) { + if (isExcelFile(path) || isWordFile(path)) { return } @@ -396,8 +406,8 @@ ${ext ? `文件类型: ${fileTypeDesc}\n` : ''} * 加载草稿 */ const loadDraft = (path: string) => { - // Office 文件和 CSV 不支持草稿功能,并清除已有的草稿 - if (isExcelFile(path) || isWordFile(path) || isCsvFile(path)) { + // Office 文件不支持草稿功能,并清除已有的草稿 + if (isExcelFile(path) || isWordFile(path)) { const key = `${STORAGE_KEYS.FILESYSTEM.FILE_DRAFT}-${path}` try { localStorage.removeItem(key) diff --git a/web/src/components/FileSystem/index.vue b/web/src/components/FileSystem/index.vue index 67464f0..1920933 100644 --- a/web/src/components/FileSystem/index.vue +++ b/web/src/components/FileSystem/index.vue @@ -123,7 +123,7 @@ import { useCommonPaths } from './composables/useCommonPaths' import { getFileName, sortFileList } from '@/utils/fileUtils' import { isImageFile, isVideoFile, isAudioFile, isPdfFile, isHtmlFile, isMarkdownFile, isExcelFile, isWordFile, isCsvFile } from '@/utils/fileTypeHelpers' import { listDir } from '@/api/system' -import { STORAGE_KEYS, DEFAULTS, UI_TEXT, VALIDATION_RULES, FILE_EXTENSIONS } from '@/utils/constants' +import { STORAGE_KEYS, DEFAULTS, UI_TEXT, VALIDATION_RULES, FILE_EXTENSIONS, FILE_SIZE_THRESHOLDS } from '@/utils/constants' // 导入类型 import type { FileItem, FavoriteFile, ContextMenuConfig, ShortcutPath } from '@/types/file-system' @@ -135,10 +135,10 @@ defineOptions({ // ========== 工具函数(最先定义,避免初始化顺序问题) ========== -// 判断是否可以在编辑/预览模式之间切换(HTML/Markdown) +// 判断是否可以在编辑/预览模式之间切换(HTML/Markdown/CSV) const isEditableWithPreview = (filename: string): boolean => { const ext = filename.split('.').pop()?.toLowerCase() || '' - return ['html', 'htm', 'md', 'markdown'].includes(ext) + return ['html', 'htm', 'md', 'markdown', 'csv', 'tsv'].includes(ext) } // ========== 状态管理 ========== @@ -1001,6 +1001,28 @@ const loadFileContent = async (path: string) => { return } + // 大文件预检:基于目录列表中的 size 字段,避免读取大文件导致卡死 + if (fileSize > FILE_SIZE_THRESHOLDS.BIG_FILE) { + const sizeMB = (fileSize / 1024 / 1024).toFixed(2) + clearContent() + fileContent.value = `================================================================ +⚠️ 文件过大 (${sizeMB} MB) +================================================================ + +文件名: ${fileName} +完整路径: ${path} +文件大小: ${sizeMB} MB + +================================================================ +当前文件超过 ${(FILE_SIZE_THRESHOLDS.BIG_FILE / 1024)}KB,不适合在编辑器中打开。 + +💡 提示: + • 右键菜单 → "使用系统程序打开" 在默认应用中打开 + • 右键菜单 → "在资源管理器中显示" 查看文件位置 +================================================================` + return + } + // 对于小文件(≤500KB)且扩展名不可识别的情况,进行内容检测 if (fileSize > 0 && fileSize <= 500 * 1024) { const ext = fileName.split('.').pop()?.toLowerCase() || '' @@ -1272,6 +1294,13 @@ const handleKeyDown = async (event: KeyboardEvent) => { return } + // Ctrl+Shift+N 新建文件夹(必须在 Ctrl+N 之前判断) + if ((event.ctrlKey || event.metaKey) && event.shiftKey && event.key === 'n') { + event.preventDefault() + handleCreateDir() + return + } + // Ctrl+N 新建文件 if ((event.ctrlKey || event.metaKey) && event.key === 'n') { event.preventDefault() @@ -1279,13 +1308,6 @@ const handleKeyDown = async (event: KeyboardEvent) => { return } - // Ctrl+Shift+N 新建文件夹 - if ((event.ctrlKey || event.metaKey) && event.key === 'n' && event.shiftKey) { - event.preventDefault() - handleCreateDir() - return - } - // Alt+← 后退到上一个目录 if (event.altKey && event.key === 'ArrowLeft') { event.preventDefault() diff --git a/web/src/utils/codeMirrorLoader.js b/web/src/utils/codeMirrorLoader.js index cfae1a3..a3952f9 100644 --- a/web/src/utils/codeMirrorLoader.js +++ b/web/src/utils/codeMirrorLoader.js @@ -79,6 +79,9 @@ export function loadLanguageExtension(language) { case 'dart': extension = StreamLanguage.define(dart) break + case 'dockerfile': + extension = StreamLanguage.define(shell) + break default: return null } diff --git a/web/src/utils/constants.js b/web/src/utils/constants.js index e60516b..9facf94 100644 --- a/web/src/utils/constants.js +++ b/web/src/utils/constants.js @@ -75,7 +75,7 @@ export const FILE_EXTENSIONS = { 'vue', 'py', 'java', 'c', 'cpp', 'h', 'go', 'rs', 'php', 'rb', 'cs', 'swift', 'kt', 'scala', 'dart', 'css', 'scss', 'sass', 'less', 'sql', 'sh', 'bat', 'ps1', 'flow', 'pch', 'cc', 'cxx', 'hpp', 'hxx', 'tcc', 'defs', 'makefile', 'mk', 'cmake', - 'm', 'r', 'matlab' + 'dockerfile', 'm', 'r', 'matlab' ], // 配置文件(可编辑的文本格式) @@ -155,6 +155,7 @@ export const FILE_ICONS = { PHP: '🐘', RUBY: '💎', DART: '🎯', + DOCKERFILE: '🐳', // 数据库 DATABASE: '🗄️', @@ -269,6 +270,8 @@ const initIconMap = () => { 'sql': FILE_ICONS.SQL, // Dart 'dart': FILE_ICONS.DART, + // Dockerfile + 'dockerfile': FILE_ICONS.DOCKERFILE, } Object.keys(langIcons).forEach(ext => FILE_ICON_MAP.set(ext, langIcons[ext])) diff --git a/web/src/utils/languageMap.ts b/web/src/utils/languageMap.ts index b58b035..14e9693 100644 --- a/web/src/utils/languageMap.ts +++ b/web/src/utils/languageMap.ts @@ -82,7 +82,8 @@ const extensionToLanguage: Record = { adoc: { hljs: 'plaintext', cm: 'text' }, // === 构建工具 / 配置 === - dockerfile: { hljs: 'dockerfile', cm: 'text' }, + // CodeMirror 6 无内置 Dockerfile 支持,用 shell 模式近似(Dockerfile 本质是类 shell 指令) + dockerfile: { hljs: 'dockerfile', cm: 'shell' }, makefile: { hljs: 'makefile', cm: 'text' }, mk: { hljs: 'makefile', cm: 'text' }, cmake: { hljs: 'cmake', cm: 'text' },