Private
Public Access
1
0

重构:文件系统模块化架构,增强 Markdown 渲染

- 拆分 FileSystem.vue 为模块化组件架构
- 新增 Markdown Mermaid 图表渲染支持
- 新增 180+ 编程语言代码高亮
- 修复编辑/预览模式切换渲染问题
- 优化亮色/暗色模式主题适配
- 新增 TypeScript 类型定义
This commit is contained in:
2026-02-04 03:31:22 +08:00
parent eb2cbad17b
commit a5d30684ed
119 changed files with 11244 additions and 12042 deletions

View File

@@ -0,0 +1,508 @@
# Composable 集成失败根因分析报告
**日期**: 2025-01-30
**目标**: 分析为什么 useFileEdit 和 useFilePreview 无法集成到 FileSystem.vue
---
## 执行摘要
集成尝试失败的根本原因:**Composables 和本地实现在设计哲学、API 契约、功能范围上存在系统性差异**。
-**useFileEdit**: 不兼容(状态变量不匹配:`isEditMode` vs `isEditableView`
-**useFilePreview**: 不兼容URL 格式、路径处理、ZIP 模式支持差异)
-**useNavigation**: 兼容(已成功集成)
---
## 一、useFileEdit.js vs FileSystem.vue
### 1.1 状态变量差异
| 功能点 | useFileEdit.js | FileSystem.vue | 兼容性 |
|--------|----------------|----------------|--------|
| **编辑模式开关** | `isEditMode` (简单 ref) | `isEditableView` (复杂 computed) | ❌ 不兼容 |
| **路径来源** | `filePath` (单一) | `selectedFilePath` \| `filePath` (双重) | ❌ 不兼容 |
| **文件修改检测** | 简单比较 | 复杂逻辑(含新建文件) | ❌ 不兼容 |
### 1.2 致命差异:`canSaveFile` 的条件
**useFileEdit.js:87-89**
```javascript
const canSaveFile = computed(() => {
return isEditMode.value && contentChanged.value
})
```
**FileSystem.vue:2997**
```javascript
const canSaveFile = computed(() => isEditableView.value && contentChanged.value)
```
**问题**
- `isEditMode`: 简单的布尔值 ref来自 localStorage
- `isEditableView`: 复杂的 computed依赖预览状态
```javascript
// FileSystem.vue:2968-2974
const isEditableView = computed(() => {
return !isImageView.value &&
!isVideoView.value &&
!isAudioView.value &&
!isPdfFile.value &&
!isBinaryFile.value
})
```
**影响**
- 使用 `isEditMode` → 保存按钮可能在图片预览时也显示(错误)
- 使用 `isEditableView` → 保存按钮只在文本编辑时显示(正确)
### 1.3 致命差异:`isFileModified` 的逻辑
**useFileEdit.js:71-74**
```javascript
const isFileModified = computed(() => {
return originalContent.value !== undefined &&
originalContent.value !== fileContent.value
})
```
**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)
})
```
**缺失功能**
- Composable 版本**不支持新建文件场景**
- FileSystem.vue 版本可以检测到"未选择文件路径但有内容"的新建文件状态
### 1.4 依赖图对比
**useFileEdit 依赖树**:
```
canSaveFile
├─ isEditMode (ref)
└─ contentChanged
├─ fileContent
└─ originalContent
```
**FileSystem.vue 依赖树**:
```
canSaveFile
├─ isEditableView (computed)
│ ├─ isImageView
│ ├─ isVideoView
│ ├─ isAudioView
│ ├─ isPdfFile
│ └─ isBinaryFile
└─ contentChanged
├─ fileContent
└─ originalContent
```
**结论**: FileSystem.vue 的依赖更复杂Composable 过于简化
---
## 二、useFilePreview.js vs FileSystem.vue
### 2.1 URL 构建差异(致命)
**useFilePreview.js:163**
```javascript
const encodedPath = encodeURIComponent(pathToPreview)
previewUrl.value = `${fileServerURL.value}/file?path=${encodedPath}`
```
**FileSystem.vue:1503**
```javascript
previewUrl.value = `${fileServerURL.value}/localfs/${normalizeFilePath(pathToPreview, true)}`
```
**问题**
- Composable: `/file?path=xxx` (查询参数格式)
- FileSystem.vue: `/localfs/xxx` (路径格式,需要规范化)
**不兼容原因**
- 后端可能只支持其中一种格式
- `normalizeFilePath()` 可能有特殊处理(如 Windows 路径转换)
### 2.2 路径参数优先级差异
**useFilePreview.js:148**
```javascript
const previewImage = async (targetPath) => {
const pathToPreview = targetPath || filePath.value // 只用 filePath
// ...
}
```
**FileSystem.vue:1487**
```javascript
const previewImageLocal = async (targetPath) => {
const pathToPreview = targetPath || selectedFilePath.value || filePath.value // 三级优先级
// ...
}
```
**三级优先级**
1. `targetPath` (显式传入)
2. `selectedFilePath` (当前选中的文件)
3. `filePath` (当前目录)
**影响**
- Composable 在"选中文件但未传参"时会失败
- FileSystem.vue 可以自动回退到 `selectedFilePath`
### 2.3 computed 属性功能差异
**currentFileName** 对比:
| 功能 | useFilePreview | FileSystem.vue | 差异 |
|------|----------------|----------------|------|
| **ZIP 模式支持** | ❌ 无 | ✅ 有 | 关键差异 |
| **目录检测** | ❌ 无 | ✅ 有 | UX 增强 |
| **路径截断** | ❌ 无 | ✅ 有 | UX 增强 |
| **错误处理** | ❌ 无 | ✅ try-catch | 健壮性 |
**FileSystem.vue:1437-1460** (23行包含 ZIP 逻辑)
```javascript
const currentFileNameDisplay = computed(() => {
if (isBrowsingZip.value && selectedFilePath.value) {
// ZIP 模式:从 zip 内路径中提取文件名
const parts = selectedFilePath.value.split('/')
return parts[parts.length - 1] || parts[parts.length - 2] || ''
}
if (selectedFilePath.value) {
// 正常模式:如果文件在当前目录,只显示文件名;否则显示完整路径
try {
if (isFileInCurrentDirectory.value) {
return getFileName(selectedFilePath.value)
} else {
return selectedFilePath.value // 返回完整路径
}
} catch (error) {
debugWarn('[currentFileName] 计算失败,返回文件名:', error)
return getFileName(selectedFilePath.value)
}
}
return ''
})
```
**useFilePreview.js:122-126** (5行无特殊逻辑)
```javascript
const currentFileName = computed(() => {
if (!filePath.value) return ''
const parts = filePath.value.split(/[/\\]/)
return parts[parts.length - 1]
})
```
### 2.4 函数命名体系差异
| 功能 | useFilePreview | FileSystem.vue |
|------|----------------|----------------|
| 图片预览 | `previewImage` | `previewImageLocal` |
| 视频预览 | `previewVideo` | `previewVideoLocal` |
| 音频预览 | `previewAudio` | `previewAudioLocal` |
| PDF 预览 | `previewPdf` | `previewPdfLocal` |
| HTML 预览 | `previewHtml` | `previewHtmlLocal` |
| Markdown 预览 | `previewMarkdown` | `previewMarkdownLocal` |
**Local 后缀的意义**
- 表明这是本地实现,避免与外部库或全局函数冲突
- 如果替换为 Composable需要全局重命名模板中的所有调用点30+ 处)
---
## 三、useNavigation.js vs FileSystem.vue
### 3.1 集成状态
**已成功集成** (FileSystem.vue:605-625)
```javascript
const {
navHistory,
navIndex,
isNavigating,
canGoBack,
canGoForward,
addToHistory,
pushNav,
goBack,
goForward,
onPathSelect,
onPathEnter,
browseDirectory,
} = useNavigation({
filePath,
onListDirectory: async (path) => {
filePath.value = path
await listDirectory()
}
})
```
### 3.2 为什么成功?
1. **清晰的回调接口**: `onListDirectory` 作为回调,连接到本地实现
2. **状态变量简单**: 只依赖 `filePath`,没有复杂的 computed 依赖
3. **无 API 假设**: 不涉及 URL 格式、网络请求等
4. **功能独立**: 导航逻辑不依赖预览、编辑等其他模块
### 3.3 集成模式
```
┌─────────────────┐
│ useNavigation │
└────────┬────────┘
│ onListDirectory(path)
┌─────────────────┐
│ FileSystem.vue │
│ listDirectory()│
└─────────────────┘
```
这种模式清晰、解耦、易于测试。
---
## 四、根因总结
### 4.1 设计哲学差异
| 维度 | Composables | FileSystem.vue |
|------|-------------|----------------|
| **复杂度** | 追求简洁、纯粹 | 追求功能完整 |
| **假设** | 单一路径、标准API | 多路径源、自定义API |
| **范围** | 单一职责 | 全功能 |
| **演进** | 从头设计 | 增量演进ZIP、新建文件等 |
### 4.2 API 契议不匹配
**Composable 隐式假设**
```javascript
// 假设 1: URL 格式
`${fileServerURL}/file?path=${encodedPath}`
// 假设 2: 路径来源
const path = filePath.value // 单一来源
// 假设 3: 状态变量
const canSave = isEditMode && changed // 简单布尔值
```
**FileSystem.vue 实际**
```javascript
// 实际 1: URL 格式
`${fileServerURL}/localfs/${normalizeFilePath(path, true)}`
// 实际 2: 路径来源
const path = targetPath || selectedFilePath || filePath // 三级优先级
// 实际 3: 状态变量
const canSave = isEditableView && changed // 复杂 computed
```
### 4.3 功能演进差距
**FileSystem.vue 独有功能**
- ✅ ZIP 文件浏览模式
- ✅ 新建文件检测
- ✅ 目录感知显示
- ✅ 路径规范化
- ✅ 文件是否在当前目录检测
**useFileEdit/useFilePreview 创建时未考虑这些功能**
---
## 五、集成失败的三个层次
### 层次 1: 语法层面(易于发现)
```
❌ ReferenceError: loadDraft is not defined
❌ Identifier 'previewImage' has already been declared
```
### 层次 2: 语义层面(运行时错误)
```
❌ 保存按钮在图片预览时也显示 (isEditMode vs isEditableView)
❌ URL 404 错误 (/file?path= vs /localfs/)
❌ 新建文件无法保存
```
### 层次 3: 设计层面(深层不兼容)
```
❌ 单一路径模型 vs 多路径源
❌ 简单布尔值 vs 复杂 computed
❌ 标准API vs 自定义API
❌ 静态功能 vs 增量演进
```
---
## 六、解决方案
### 方案 A: 保持现状 + 提取工具函数(推荐)
**理由**
- 功能完整性优先
- 避免破坏性重构
- 渐进式优化
**行动**
1. 保留 `useNavigation` 集成
2. 删除 `useFileEdit``useFilePreview`(或作为参考文档)
3. 提取真正的通用工具函数:
```javascript
// utils/pathHelpers.js
export const splitPath = (path) => path.split(/[/\\]/)
export const getFileName = (path) => { /* ... */ }
export const getParentPath = (path) => { /* ... */ }
// utils/fileHelpers.js
export const isImageFile = (ext) => FILE_EXTENSIONS.IMAGE.includes(ext)
export const isVideoFile = (ext) => FILE_EXTENSIONS.VIDEO_BROWSER.includes(ext)
```
4. 减少调试日志65 → 10
### 方案 B: 重构 FileSystem.vue激进
**风险**: 高
**时间**: 2-3周
**收益**: 长期可维护性
**步骤**
1. 统一状态管理(单一 `filePath` vs `selectedFilePath`
2. 标准化 API统一 URL 格式)
3. 组件化拆分(子组件)
4. 然后重新集成 Composables
### 方案 C: 创建轻量级 Composables折中
```javascript
// useFileEditMinimal.js
export function useFileEditMinimal({ fileContent, originalContent }) {
const contentChanged = computed(() =>
fileContent.value !== '' &&
fileContent.value !== originalContent.value
)
return { contentChanged }
}
// FileSystem.vue
const { contentChanged } = useFileEditMinimal({ fileContent, originalContent })
const canSaveFile = computed(() => isEditableView.value && contentChanged.value)
```
---
## 七、检查清单
### 立即行动(本周)
- [x] 分析集成失败根因
- [ ] 修复 `loadDraft is not defined` 运行时错误
- [ ] 决定方案 A/B/C
- [ ] 执行决定
### 短期优化2周
- [ ] 提取路径工具函数
- [ ] 提取文件类型判断函数
- [ ] 统一 localStorage 键名
- [ ] 减少调试日志
### 长期重构1个月
- [ ] 组件化拆分(子组件)
- [ ] 状态管理优化
- [ ] TypeScript 迁移
- [ ] 单元测试覆盖
---
## 八、关键发现
### 发现 1: Composables 是"理想版本"
Composables 基于**理想假设**设计:
- 单一路径来源
- 标准 API
- 简单状态
- 纯净功能
但 FileSystem.vue 是**现实版本**
- 多路径源(历史包袱)
- 自定义 API性能优化
- 复杂状态(功能完整)
- 增量演进(业务需求)
### 发现 2: 命名体系反映演进历史
所有预览函数都有 `Local` 后缀:
```javascript
previewImageLocal // 表明"本地实现"
previewVideoLocal // 避免"全局冲突"
```
这说明开发者在添加这些函数时,**已经意识到可能存在外部冲突**,因此添加后缀。
如果强行使用无后缀的 Composable 版本,会破坏这种防御性设计。
### 发现 3: useNavigation 成功的启示
useNavigation 成功的关键:
1. **清晰的边界**: 只负责导航历史
2. **回调接口**: 不直接操作文件系统
3. **状态简单**: 只依赖 `filePath`
4. **无副作用**: 不涉及 UI 状态
**教训**: 如果要提取 Composables应该遵循同样的原则。
---
## 九、最终建议
### 推荐:方案 A - 提取工具函数
**原因**
1. **风险最低**: 不破坏现有功能
2. **收益明确**: 减少代码重复(路径处理、文件类型判断)
3. **时间可控**: 1周内完成
4. **渐进式**: 为未来重构铺路
**具体行动**
```javascript
// 第1步提取工具函数
// utils/pathHelpers.js
// utils/fileTypeHelpers.js
// 第2步替换重复代码
// path.split(/[/\\/]/) → splitPath(path)
// 第3步删除未使用的 Composables
// rm useFileEdit.js useFilePreview.js
// 第4步减少调试日志
// 保留 10 个关键日志,删除 55 个
```
**预期结果**
- 代码减少 ~200 行
- DRY 评分改善 5%
- 维护成本降低
- 为长期重构打好基础