- 拆分 FileSystem.vue 为模块化组件架构 - 新增 Markdown Mermaid 图表渲染支持 - 新增 180+ 编程语言代码高亮 - 修复编辑/预览模式切换渲染问题 - 优化亮色/暗色模式主题适配 - 新增 TypeScript 类型定义
629 lines
15 KiB
Markdown
629 lines
15 KiB
Markdown
# 重构缺漏检查报告
|
||
**日期**: 2025-01-30
|
||
**审查范围**: FileSystem.vue + 3个Composables
|
||
|
||
---
|
||
|
||
## 一、严重问题 🔴
|
||
|
||
### 1. **重构目标未达成 - FileSystem.vue 仍然过大**
|
||
|
||
| 文件 | 当前行数 | 目标行数 | 差距 | 状态 |
|
||
|------|----------|----------|------|------|
|
||
| FileSystem.vue | 4047 | < 500 | +3547 | 🔴 |
|
||
| useNavigation.js | 273 | - | - | ✅ |
|
||
| useFileEdit.js | 369 | - | - | ✅ |
|
||
| useFilePreview.js | 611 | - | - | ✅ |
|
||
| **总计** | 5300 | < 1500 | +3800 | 🔴 |
|
||
|
||
**问题**:
|
||
- Composables已创建(1253行),但**未真正集成**
|
||
- FileSystem.vue仍然包含所有原始逻辑(4047行)
|
||
- **代码总量增加**:从4241行 → 5300行(+25%)
|
||
|
||
**根本原因**:
|
||
- 之前因20+个重复函数声明错误,撤销了composable集成
|
||
- 保留了所有本地实现,导致双重代码存在
|
||
|
||
---
|
||
|
||
### 2. **重复的计算属性(DRY违反)**
|
||
|
||
#### 问题1: `isFileModified` 重复定义
|
||
|
||
**FileSystem.vue:2977-2988**
|
||
```javascript
|
||
const isFileModified = computed(() => {
|
||
const hasContent = fileContent.value !== '' && fileContent.value.trim() !== ''
|
||
const hasModified = selectedFilePath.value && fileContent.value !== originalContent.value
|
||
const isNewFile = !selectedFilePath.value && hasContent
|
||
return isEditableView.value && (hasModified || isNewFile)
|
||
})
|
||
```
|
||
|
||
**useFileEdit.js:71-74** (未使用)
|
||
```javascript
|
||
const isFileModified = computed(() => {
|
||
return originalContent.value !== undefined &&
|
||
originalContent.value !== fileContent.value
|
||
})
|
||
```
|
||
|
||
**差异**:FileSystem.vue版本包含"新建文件"逻辑,useFileEdit版本更简单
|
||
|
||
---
|
||
|
||
#### 问题2: 文件名计算属性重复
|
||
|
||
**FileSystem.vue:1437-1460**
|
||
```javascript
|
||
const currentFileNameDisplay = computed(() => {
|
||
if (!selectedFilePath.value && !filePath.value) return '无文件'
|
||
|
||
const path = selectedFilePath.value || filePath.value
|
||
const parts = path.split(/[/\\]/)
|
||
const fileName = parts[parts.length - 1]
|
||
|
||
if (fileName.length > 30) {
|
||
return fileName.substring(0, 15) + '...' + fileName.substring(fileName.length - 10)
|
||
}
|
||
return fileName
|
||
})
|
||
```
|
||
|
||
**useFilePreview.js:122-126** (未使用)
|
||
```javascript
|
||
const currentFileName = computed(() => {
|
||
if (!filePath.value) return ''
|
||
const parts = filePath.value.split(/[/\\]/)
|
||
return parts[parts.length - 1]
|
||
})
|
||
```
|
||
|
||
**重复**:都做路径分割取文件名,但Display版本有截断逻辑
|
||
|
||
---
|
||
|
||
#### 问题3: 文件路径计算属性重复
|
||
|
||
**FileSystem.vue:1462-1485**
|
||
```javascript
|
||
const currentFileFullPathDisplay = computed(() => {
|
||
if (isBrowsingZip.value) {
|
||
return `ZIP: ${currentZipPath.value} → ${currentZipDirectory.value || '/'}`
|
||
}
|
||
|
||
if (!selectedFilePath.value) {
|
||
return filePath.value || '未选择文件'
|
||
}
|
||
|
||
const path = selectedFilePath.value
|
||
if (path.length > 50) {
|
||
return '...' + path.substring(path.length - 50)
|
||
}
|
||
return path
|
||
})
|
||
```
|
||
|
||
**useFilePreview.js:131** (未使用)
|
||
```javascript
|
||
const currentFileFullPath = computed(() => filePath.value || '')
|
||
```
|
||
|
||
**重复**:获取文件路径,但Display版本有ZIP模式和截断逻辑
|
||
|
||
---
|
||
|
||
#### 问题4: 内容修改检测重复
|
||
|
||
**FileSystem.vue:2991-2994**
|
||
```javascript
|
||
const contentChanged = computed(() => {
|
||
return fileContent.value !== '' &&
|
||
fileContent.value !== originalContent.value
|
||
})
|
||
```
|
||
|
||
**useFileEdit.js:79-82** (未使用)
|
||
```javascript
|
||
const contentChanged = computed(() => {
|
||
return fileContent.value !== '' &&
|
||
fileContent.value !== originalContent.value
|
||
})
|
||
```
|
||
|
||
**完全相同**:100%重复代码
|
||
|
||
---
|
||
|
||
#### 问题5: 保存/重置按钮状态重复
|
||
|
||
**FileSystem.vue:2997-3004**
|
||
```javascript
|
||
const canSaveFile = computed(() => isEditableView.value && contentChanged.value)
|
||
const canResetContent = computed(() =>
|
||
isEditableView.value &&
|
||
contentChanged.value &&
|
||
originalContent.value !== undefined
|
||
)
|
||
```
|
||
|
||
**useFileEdit.js:87-98** (未使用)
|
||
```javascript
|
||
const canSaveFile = computed(() => {
|
||
return isEditMode.value && contentChanged.value
|
||
})
|
||
|
||
const canResetContent = computed(() => {
|
||
return isEditMode.value &&
|
||
contentChanged.value &&
|
||
originalContent.value !== undefined
|
||
})
|
||
```
|
||
|
||
**差异**:FileSystem.vue用`isEditableView`,useFileEdit用`isEditMode`
|
||
|
||
---
|
||
|
||
### 3. **调试日志仍然过多 - 65个**
|
||
|
||
```bash
|
||
$ grep -c "debug(Log|Warn|Error|Info)" web/src/components/FileSystem.vue
|
||
65
|
||
```
|
||
|
||
**分布**:
|
||
- `debugLog`: ~45处
|
||
- `debugWarn`: ~12处
|
||
- `debugError`: ~8处
|
||
|
||
**问题**:
|
||
- 已从raw console替换为debugLog,但**数量仍然过多**
|
||
- 过度防御性编程,每个分支都记录日志
|
||
- 影响代码可读性和运行时性能
|
||
|
||
---
|
||
|
||
## 二、中等问题 🟡
|
||
|
||
### 4. **currentFileExtension 逻辑嵌套过多**
|
||
|
||
**FileSystem.vue:2941-2960** (19行)
|
||
```javascript
|
||
const currentFileExtension = computed(() => {
|
||
const path = selectedFilePath.value || filePath.value
|
||
if (!path) return ''
|
||
|
||
const fileName = path.split(/[/\\]/).pop()?.toLowerCase() || ''
|
||
const specialFiles = {
|
||
'dockerfile': 'dockerfile',
|
||
'containerfile': 'dockerfile',
|
||
'makefile': 'makefile',
|
||
'cmakelists.txt': 'cmake',
|
||
'.gitignore': 'gitignore',
|
||
'.env': 'properties',
|
||
}
|
||
|
||
if (specialFiles[fileName]) return specialFiles[fileName]
|
||
return getExt(path)
|
||
})
|
||
```
|
||
|
||
**可以改进为**(使用fileHelpers.js中的函数):
|
||
```javascript
|
||
const currentFileExtension = computed(() => {
|
||
const path = selectedFilePath.value || filePath.value
|
||
return getExtensionForHighlight(path) // 复用现有工具函数
|
||
})
|
||
```
|
||
|
||
---
|
||
|
||
### 5. **函数命名不一致**
|
||
|
||
| FileSystem.vue | useFilePreview.js | 用途 |
|
||
|----------------|-------------------|------|
|
||
| `currentFileNameDisplay` | `currentFileName` | 获取文件名 |
|
||
| `currentFileFullPathDisplay` | `currentFileFullPath` | 获取完整路径 |
|
||
| `currentImageDimensionsLocal` | `currentImageDimensions` | 图片尺寸 |
|
||
|
||
**问题**:
|
||
- 有的带`Display`后缀,有的不带
|
||
- 有的带`Local`后缀,含义不明
|
||
- 命名不一致导致维护困难
|
||
|
||
---
|
||
|
||
### 6. **Go代码配置函数重复**
|
||
|
||
**internal/filesystem/config.go:256-295**
|
||
```go
|
||
func getAllowedExtensions() map[string]bool {
|
||
return map[string]bool{
|
||
".jpg": true, ".jpeg": true, ".png": true,
|
||
// ... 30+ 个硬编码扩展名
|
||
}
|
||
}
|
||
```
|
||
|
||
**web/src/utils/constants.js:27-73** (重复定义)
|
||
```javascript
|
||
export const FILE_EXTENSIONS = {
|
||
IMAGE: ['jpg', 'jpeg', 'png', /* ... */],
|
||
VIDEO_BROWSER: ['mp4', 'webm', /* ... */],
|
||
// ... 类似的30+个扩展名
|
||
}
|
||
```
|
||
|
||
**问题**:前后端用不同格式重复定义相同的数据
|
||
|
||
**建议**:后端从配置文件加载,或生成JSON供前端使用
|
||
|
||
---
|
||
|
||
## 三、代码规范问题 ⚠️
|
||
|
||
### 7. **路径分隔符正则重复**
|
||
|
||
**出现次数**: 15+
|
||
|
||
```javascript
|
||
// FileSystem.vue 多处
|
||
path.split(/[/\\]/) // 行 719, 798, 819, 833, 845, 2946, ...
|
||
|
||
// useFilePreview.js:124
|
||
path.split(/[/\\/]/)
|
||
|
||
// useNavigation.js:304
|
||
const parts = path.split(/[/\\]/)
|
||
```
|
||
|
||
**建议**:提取为共享常量
|
||
```javascript
|
||
// utils/pathConstants.js
|
||
export const PATH_SEPARATOR_REGEX = /[/\\]/
|
||
export const splitPath = (path) => path.split(PATH_SEPARATOR_REGEX)
|
||
```
|
||
|
||
---
|
||
|
||
### 8. **文件类型判断分散**
|
||
|
||
**FileSystem.vue:857-869**
|
||
```javascript
|
||
const previewableTypes = [
|
||
...FILE_EXTENSIONS.IMAGE,
|
||
...FILE_EXTENSIONS.VIDEO_BROWSER,
|
||
...FILE_EXTENSIONS.AUDIO,
|
||
'pdf', 'html', 'htm', 'md', 'markdown'
|
||
]
|
||
|
||
const knownBinaryTypes = [
|
||
'exe', 'dll', 'so', 'bin',
|
||
'zip', 'rar', '7z', 'tar', 'gz', 'iso', 'img', 'dmg',
|
||
'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'
|
||
]
|
||
```
|
||
|
||
**问题**:
|
||
- 内联定义在函数内部
|
||
- 应该定义在constants.js中复用
|
||
|
||
---
|
||
|
||
### 9. **localStorage键名分散**
|
||
|
||
**多处重复定义**:
|
||
- FileSystem.vue: 使用`STORAGE_KEYS.FILESYSTEM.*`
|
||
- useFileEdit.js: 直接定义`DRAFT_STORAGE_KEY`
|
||
- useNavigation.js: 直接定义`STORAGE_KEY_PATH_HISTORY`
|
||
|
||
**应该统一使用**:`STORAGE_KEYS`常量对象
|
||
|
||
---
|
||
|
||
## 四、DRY原则违反统计
|
||
|
||
### 重复代码统计
|
||
|
||
| 类型 | 重复次数 | 总行数 | 浪费 |
|
||
|------|----------|--------|------|
|
||
| 计算属性 | 5组 | ~80行 | 40行 |
|
||
| 路径分割正则 | 15+次 | ~15行 | 14行 |
|
||
| 文件类型判断 | 8+次 | ~50行 | 40行 |
|
||
| localStorage键 | 6+处 | ~12行 | 8行 |
|
||
| **总计** | **34+处** | **~157行** | **102行** |
|
||
|
||
---
|
||
|
||
## 五、优化建议
|
||
|
||
### 优先级1: 立即修复 🔴
|
||
|
||
#### 1.1 移除未使用的Composables
|
||
```bash
|
||
# 由于composables未被实际使用,应该删除或文档化
|
||
rm web/src/composables/useNavigation.js
|
||
rm web/src/composables/useFileEdit.js
|
||
rm web/src/composables/useFilePreview.js
|
||
```
|
||
|
||
**理由**:如果不用,就不应该存在,避免混淆
|
||
|
||
---
|
||
|
||
#### 1.2 删除重复计算属性
|
||
|
||
**FileSystem.vue - 保留更完整的版本,删除useFileEdit/useFilePreview中的**:
|
||
|
||
```javascript
|
||
// 保留 FileSystem.vue:1437 - currentFileNameDisplay (有截断逻辑)
|
||
// 保留 FileSystem.vue:1462 - currentFileFullPathDisplay (有ZIP模式)
|
||
// 保留 FileSystem.vue:2977 - isFileModified (有新建文件逻辑)
|
||
// 删除 useFileEdit.js:71, useFilePreview.js:122-126 等重复定义
|
||
```
|
||
|
||
**或者相反**:如果决定使用composables,则删除FileSystem.vue中的重复定义
|
||
|
||
---
|
||
|
||
#### 1.3 大幅减少调试日志
|
||
|
||
**策略A: 环境变量控制**(已部分实现)
|
||
```javascript
|
||
// utils/debugLog.js
|
||
const ENABLE_DEBUG = import.meta.env.DEV
|
||
|
||
export const debugLog = ENABLE_DEBUG ? console.log : () => {}
|
||
export const debugWarn = ENABLE_DEBUG ? console.warn : () => {}
|
||
export const debugError = console.error // 始终保留错误日志
|
||
```
|
||
|
||
**策略B: 删除非关键日志**(推荐)
|
||
```javascript
|
||
// 删除这些类型的日志:
|
||
debugLog('[readFile] 开始读取文件') // 显而易见的操作
|
||
debugLog('[handleKeyDown] F2 pressed') // 用户操作
|
||
debugLog('[startResizeHorizontal] 开始拖拽') // UI交互
|
||
|
||
// 保留这些:
|
||
debugError('[readFile] 读取失败:', error) // 错误
|
||
debugWarn('[loadCommonPaths] Wails API未就绪') // 降级场景
|
||
```
|
||
|
||
**目标**: 从65个 → < 10个(只保留错误和关键警告)
|
||
|
||
---
|
||
|
||
### 优先级2: 短期优化 🟡
|
||
|
||
#### 2.1 提取共享工具函数
|
||
|
||
**创建 web/src/utils/pathHelpers.js**:
|
||
```javascript
|
||
export const PATH_SEPARATOR_REGEX = /[/\\]/
|
||
|
||
export const splitPath = (path) => path.split(PATH_SEPARATOR_REGEX)
|
||
|
||
export const getFileName = (path) => {
|
||
if (!path) return ''
|
||
const parts = splitPath(path)
|
||
return parts[parts.length - 1] || path
|
||
}
|
||
|
||
export const getParentPath = (path) => {
|
||
if (!path) return ''
|
||
const lastSep = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'))
|
||
return lastSep > 0 ? path.substring(0, lastSep) : path
|
||
}
|
||
```
|
||
|
||
**替换所有** `path.split(/[/\\]/)` 为 `splitPath(path)`
|
||
|
||
---
|
||
|
||
#### 2.2 统一文件类型常量
|
||
|
||
**创建 web/src/utils/fileTypeCategories.js**:
|
||
```javascript
|
||
import { FILE_EXTENSIONS } from './constants'
|
||
|
||
export const PREVIEWABLE_TYPES = [
|
||
...FILE_EXTENSIONS.IMAGE,
|
||
...FILE_EXTENSIONS.VIDEO_BROWSER,
|
||
...FILE_EXTENSIONS.AUDIO,
|
||
'pdf', 'html', 'htm', 'md', 'markdown'
|
||
]
|
||
|
||
export const KNOWN_BINARY_TYPES = [
|
||
'exe', 'dll', 'so', 'bin',
|
||
'zip', 'rar', '7z', 'tar', 'gz', 'iso', 'img', 'dmg',
|
||
'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'
|
||
]
|
||
|
||
export const TEXT_EDITABLE_TYPES = [
|
||
...FILE_EXTENSIONS.TEXT,
|
||
...FILE_EXTENSIONS.CODE
|
||
]
|
||
```
|
||
|
||
**替换所有内联定义**
|
||
|
||
---
|
||
|
||
#### 2.3 统一localStorage键名
|
||
|
||
**只在 constants.js 中定义一次**:
|
||
```javascript
|
||
export const STORAGE_KEYS = {
|
||
FILESYSTEM: {
|
||
PATH_HISTORY: 'app-filesystem-path-history',
|
||
EDIT_MODE: 'app-filesystem-edit-mode',
|
||
PANEL_WIDTH: 'app-filesystem-panel-width',
|
||
DRAFT_CONTENT: 'filesystem-draft-content',
|
||
DRAFT_TIME: 'filesystem-draft-time',
|
||
FAVORITE_FILES: 'filesystem-favorite-files',
|
||
}
|
||
}
|
||
|
||
// 删除所有其他文件中的重复定义
|
||
```
|
||
|
||
---
|
||
|
||
### 优先级3: 长期重构 🔵
|
||
|
||
#### 3.1 真正拆分FileSystem.vue
|
||
|
||
**目标**: 从4047行 → < 500行
|
||
|
||
**策略**:
|
||
1. **提取子组件** (~1500行)
|
||
- `FileListPanel.vue` (文件列表, ~300行)
|
||
- `CodeEditorPanel.vue` (编辑器面板, ~400行)
|
||
- `PreviewPanel.vue` (预览面板, ~300行)
|
||
- `FavoriteSidebar.vue` (收藏夹侧边栏, ~200行)
|
||
- `Toolbar.vue` (顶部工具栏, ~150行)
|
||
- `ContextMenu.vue` (右键菜单, ~150行)
|
||
|
||
2. **提取composables** (~1000行)
|
||
- `useFileSystem.js` (核心文件系统操作, ~300行)
|
||
- `useFileEditor.js` (编辑器逻辑, ~200行)
|
||
- `useFilePreview.js` (预览逻辑, ~250行)
|
||
- `useFavoriteFiles.js` (收藏夹管理, ~150行)
|
||
- `useKeyboardShortcuts.js` (快捷键, ~100行)
|
||
|
||
3. **主组件保留** (~500行)
|
||
- 布局和状态协调
|
||
- 子组件通信
|
||
- 生命周期管理
|
||
|
||
**时间估算**: 2-3周
|
||
|
||
---
|
||
|
||
#### 3.2 TypeScript迁移
|
||
|
||
**目标**: 添加类型安全,减少运行时错误
|
||
|
||
```typescript
|
||
// types/file.ts
|
||
export interface FileItem {
|
||
path: string
|
||
name: string
|
||
is_dir: boolean
|
||
size: number
|
||
modified: string
|
||
}
|
||
|
||
export interface PreviewState {
|
||
isImageView: boolean
|
||
isVideoView: boolean
|
||
isAudioView: boolean
|
||
isPdfFile: boolean
|
||
isHtmlFile: boolean
|
||
isMarkdownFile: boolean
|
||
isBinaryFile: boolean
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
#### 3.3 统一前后端文件类型定义
|
||
|
||
**方案A: 后端生成JSON**
|
||
```go
|
||
// internal/filesystem/export_types.go
|
||
func ExportFileTypes() string {
|
||
types := map[string][]string{
|
||
"image": getAllowedExtensions(),
|
||
"binary": getForbiddenExtensions(),
|
||
}
|
||
json, _ := json.Marshal(types)
|
||
return string(json)
|
||
}
|
||
```
|
||
|
||
**方案B: 独立配置文件**
|
||
```yaml
|
||
# config/file_types.yaml
|
||
image:
|
||
- jpg
|
||
- jpeg
|
||
- png
|
||
binary:
|
||
- exe
|
||
- dll
|
||
```
|
||
|
||
前后端都从同一配置读取
|
||
|
||
---
|
||
|
||
## 六、检查清单
|
||
|
||
### 立即执行(本周)
|
||
|
||
- [ ] **决定**: 删除还是使用composables
|
||
- [ ] **删除重复**: 移除5组重复计算属性(102行)
|
||
- [ ] **减少日志**: 从65个debugLog → < 10个
|
||
- [ ] **提取工具**: 创建pathHelpers.js
|
||
- [ ] **统一常量**: 合并文件类型定义
|
||
- [ ] **统一键名**: 只使用STORAGE_KEYS
|
||
|
||
### 短期计划(2周)
|
||
|
||
- [ ] **提取子组件**: FileListPanel, Toolbar, ContextMenu
|
||
- [ ] **提效composables**: 真正集成useFileSystem, useFilePreview
|
||
- [ ] **优化函数**: 简化currentFileExtension逻辑
|
||
- [ ] **命名统一**: 统一Display/Local后缀规则
|
||
|
||
### 长期优化(1个月)
|
||
|
||
- [ ] **组件化**: 完成所有子组件提取
|
||
- [ ] **TypeScript**: 添加类型定义
|
||
- [ ] **前后端统一**: 文件类型配置共享
|
||
- [ ] **单元测试**: 覆盖核心逻辑
|
||
|
||
---
|
||
|
||
## 七、代码质量指标(更新后)
|
||
|
||
| 指标 | 当前值 | 目标值 | 评级 |
|
||
|------|--------|--------|------|
|
||
| 单文件最大行数 | 4047 | < 500 | 🔴 |
|
||
| 函数平均行数 | ~50 | < 30 | 🟡 |
|
||
| 代码重复率 | ~8% | < 3% | 🔴 |
|
||
| 调试语句数量 | 65 | < 10 | 🔴 |
|
||
| 圈复杂度 | 15+ | < 10 | 🟡 |
|
||
| 未使用代码 | 1253行 | 0 | 🔴 |
|
||
|
||
---
|
||
|
||
## 八、总结
|
||
|
||
### 关键发现
|
||
|
||
1. **重构未完成**: Composables已创建但未使用,反而增加了总代码量
|
||
2. **重复代码严重**: 5组计算属性重复,102行浪费
|
||
3. **过度防御性编程**: 65个调试日志,远超必要数量
|
||
4. **命名不一致**: Display/Local后缀混乱
|
||
|
||
### 下一步行动
|
||
|
||
**推荐方案A: 激进重构**
|
||
- 删除3个未使用的composables
|
||
- 立即开始拆分子组件
|
||
- 1个月内完成组件化
|
||
|
||
**推荐方案B: 渐进优化(更稳妥)**
|
||
- 先清理重复代码和日志
|
||
- 提取共享工具函数
|
||
- 逐步拆分子组件
|
||
|
||
### 风险提示
|
||
|
||
⚠️ **当前状态**: 代码库处于"半重构"状态,既有旧实现又有新参考,容易造成混淆
|
||
|
||
**建议**: 尽快决定方向(彻底重构 vs 回滚到重构前),避免技术债务累积
|