Private
Public Access
1
0

修复:大文件点击卡死 + Dockerfile高亮支持

- useFileEdit: 新增 KNOWN_BINARY_EXTS 集合,exe/dll/zip 等 28 种二进制扩展名直接判定,不再读取文件内容
- index.vue: loadFileContent 增加大文件预检,基于 fileSize 超过阈值直接拦截
- service.go: ReadFile 增加 10MB 读取上限,超限返回错误
- Dockerfile 支持:CODE 分类、🐳图标、CodeMirror shell 模式高亮、languageMap 映射
This commit is contained in:
2026-04-07 11:39:50 +08:00
parent e5dbe89a6f
commit fb12ec48e8
7 changed files with 71 additions and 22 deletions

View File

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

View File

@@ -14,7 +14,7 @@ import (
// ==================== 常量定义 ====================
// AppVersion 应用版本号(发布时直接修改此处)
const AppVersion = "0.3.2"
const AppVersion = "0.3.3"
// 版本号缓存
var (

View File

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

View File

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

View File

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

View File

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

View File

@@ -82,7 +82,8 @@ const extensionToLanguage: Record<string, { hljs?: string; cm?: string }> = {
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' },