修复:大文件点击卡死 + 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:
@@ -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 {
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
// ==================== 常量定义 ====================
|
||||
|
||||
// AppVersion 应用版本号(发布时直接修改此处)
|
||||
const AppVersion = "0.3.2"
|
||||
const AppVersion = "0.3.3"
|
||||
|
||||
// 版本号缓存
|
||||
var (
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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]))
|
||||
|
||||
@@ -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' },
|
||||
|
||||
Reference in New Issue
Block a user