1051 lines
24 KiB
Markdown
1051 lines
24 KiB
Markdown
# 模块化架构缺陷分析报告
|
||
|
||
**项目**: go-desk / u-desk
|
||
**分析日期**: 2026-01-31
|
||
**分析对象**: FileSystem模块化架构
|
||
**当前代码行数**: 4711行(8个文件+5个composables)
|
||
|
||
---
|
||
|
||
## 一、架构概览
|
||
|
||
### 1.1 当前结构
|
||
|
||
```
|
||
FileSystem/ (4711行)
|
||
├── index.vue (1313行) ⚠️ 过长
|
||
├── components/ (2153行)
|
||
│ ├── FileEditorPanel.vue (717行) ⚠️ 过长
|
||
│ ├── Sidebar.vue (244行)
|
||
│ ├── Toolbar.vue (255行)
|
||
│ ├── FileListPanel.vue (215行)
|
||
│ ├── FileItemRow.vue (256行)
|
||
│ └── ContextMenu.vue (166行)
|
||
└── composables/ (1545行)
|
||
├── useFileEdit.ts (560行) ⚠️ 偏长
|
||
├── useFilePreview.ts (283行)
|
||
├── useFileOperations.ts (271行)
|
||
├── usePathNavigation.ts (200行)
|
||
└── useFavorites.ts (231行)
|
||
```
|
||
|
||
### 1.2 架构评价
|
||
|
||
| 维度 | 评分 | 说明 |
|
||
|------|------|------|
|
||
| **模块化程度** | ⭐⭐⭐⭐☆ | 良好,但主组件仍过长 |
|
||
| **代码复用** | ⭐⭐⭐⭐☆ | Composables复用性好 |
|
||
| **可维护性** | ⭐⭐⭐☆☆ | 中等,主组件和部分文件偏长 |
|
||
| **可测试性** | ⭐⭐⭐⭐☆ | 良好,独立模块易于测试 |
|
||
| **性能** | ⭐⭐⭐☆☆ | 中等,存在优化空间 |
|
||
| **类型安全** | ⭐⭐⭐⭐⭐ | 优秀,完整TypeScript支持 |
|
||
|
||
**综合评分**: ⭐⭐⭐⭐☆ (4/5星)
|
||
|
||
---
|
||
|
||
## 二、识别的架构缺陷
|
||
|
||
### 缺陷1:主组件过于臃肿 ⚠️⚠️⚠️
|
||
|
||
**严重程度**: 高
|
||
|
||
**问题描述**:
|
||
- `index.vue` 有 **1313行**,包含50+个函数
|
||
- 违反了单一职责原则
|
||
- 难以维护和理解
|
||
|
||
**具体表现**:
|
||
|
||
```typescript
|
||
// index.vue 中的状态定义(20+个ref)
|
||
const fileList = ref<FileItem[]>([])
|
||
const fileLoading = ref(false)
|
||
const selectedFileItem = ref<FileItem | null>(null)
|
||
const editingFilePath = ref('')
|
||
const editingFileName = ref('')
|
||
const showSidebar = ref(true)
|
||
const panelWidth = ref({ left: 50, right: 50 })
|
||
// ... 还有13个
|
||
|
||
// 事件处理函数(50+个handle函数)
|
||
const handleFilePathUpdate = (path: string) => { /* ... */ }
|
||
const handleSidebarToggle = (show: boolean) => { /* ... */ }
|
||
const handleRefresh = async () => { /* ... */ }
|
||
const handleGoToPath = async (path: string) => { /* ... */ }
|
||
// ... 还有46个
|
||
```
|
||
|
||
**影响**:
|
||
- 新开发者难以理解代码
|
||
- 修改时容易引入bug
|
||
- 代码审查困难
|
||
- 难以定位问题
|
||
|
||
**建议方案**:
|
||
|
||
**方案1:抽取自定义Hook(推荐)**
|
||
|
||
```typescript
|
||
// composables/useFileSystem.ts - 文件系统主逻辑
|
||
export function useFileSystem() {
|
||
// 状态管理
|
||
const state = reactive({
|
||
fileList: [],
|
||
fileLoading: false,
|
||
selectedFileItem: null,
|
||
// ...
|
||
})
|
||
|
||
// 文件操作
|
||
const loadDirectory = async (path: string) => { /* ... */ }
|
||
const selectFile = async (path: string) => { /* ... */ }
|
||
|
||
// ZIP操作
|
||
const enterZipMode = async (zipPath: string) => { /* ... */ }
|
||
const exitZipMode = () => { /* ... */ }
|
||
|
||
return {
|
||
state,
|
||
loadDirectory,
|
||
selectFile,
|
||
enterZipMode,
|
||
exitZipMode
|
||
}
|
||
}
|
||
|
||
// composables/useUIState.ts - UI状态管理
|
||
export function useUIState() {
|
||
const showSidebar = ref(true)
|
||
const panelWidth = ref({ left: 50, right: 50 })
|
||
|
||
const toggleSidebar = () => { /* ... */ }
|
||
|
||
return {
|
||
showSidebar,
|
||
panelWidth,
|
||
toggleSidebar
|
||
}
|
||
}
|
||
|
||
// composables/useDialogs.ts - 对话框管理
|
||
export function useDialogs() {
|
||
const inputDialogVisible = ref(false)
|
||
const inputDialogTitle = ref('')
|
||
const inputDialogValue = ref('')
|
||
|
||
const showInputDialog = (options) => { /* ... */ }
|
||
|
||
return {
|
||
inputDialogVisible,
|
||
showInputDialog
|
||
}
|
||
}
|
||
|
||
// index.vue 简化为
|
||
const { state, loadDirectory, selectFile, enterZipMode } = useFileSystem()
|
||
const { showSidebar, panelWidth } = useUIState()
|
||
const { showInputDialog } = useDialogs()
|
||
```
|
||
|
||
**方案2:使用Pinia状态管理**
|
||
|
||
```typescript
|
||
// stores/fileSystem.ts
|
||
import { defineStore } from 'pinia'
|
||
|
||
export const useFileSystemStore = defineStore('fileSystem', {
|
||
state: () => ({
|
||
fileList: [],
|
||
selectedFileItem: null,
|
||
currentPath: '',
|
||
isBrowsingZip: false,
|
||
// ...
|
||
}),
|
||
|
||
actions: {
|
||
async loadDirectory(path) { /* ... */ },
|
||
async selectFile(path) { /* ... */ },
|
||
enterZipMode(zipPath) { /* ... */ },
|
||
exitZipMode() { /* ... */ }
|
||
}
|
||
})
|
||
|
||
// index.vue 中使用
|
||
const fileSystemStore = useFileSystemStore()
|
||
const { fileList, selectedFileItem } = storeToRefs(fileSystemStore)
|
||
```
|
||
|
||
---
|
||
|
||
### 缺陷2:FileEditorPanel组件过长 ⚠️⚠️
|
||
|
||
**严重程度**: 中
|
||
|
||
**问题描述**:
|
||
- `FileEditorPanel.vue` 有 **717行**
|
||
- 包含多种编辑器逻辑(代码、图片、视频、音频、PDF)
|
||
- 违反了单一职责原则
|
||
|
||
**建议方案**:
|
||
|
||
拆分为多个子组件:
|
||
|
||
```
|
||
components/Editor/
|
||
├── EditorPanel.vue (主组件,~150行)
|
||
├── CodeEditor.vue (代码编辑器)
|
||
├── ImagePreview.vue (图片预览)
|
||
├── VideoPlayer.vue (视频播放器)
|
||
├── AudioPlayer.vue (音频播放器)
|
||
├── PdfViewer.vue (PDF查看器)
|
||
└── BinaryFileInfo.vue (二进制文件信息)
|
||
```
|
||
|
||
```vue
|
||
<!-- EditorPanel.vue - 简化后 -->
|
||
<template>
|
||
<div class="editor-panel">
|
||
<CodeEditor
|
||
v-if="isCodeFile"
|
||
:content="fileContent"
|
||
@save="handleSave"
|
||
/>
|
||
<ImagePreview
|
||
v-else-if="isImageFile"
|
||
:url="previewUrl"
|
||
@load="handleImageLoad"
|
||
/>
|
||
<VideoPlayer
|
||
v-else-if="isVideoFile"
|
||
:url="previewUrl"
|
||
/>
|
||
<!-- ... 其他编辑器 -->
|
||
</div>
|
||
</template>
|
||
```
|
||
|
||
---
|
||
|
||
### 缺陷3:状态管理分散 ⚠️⚠️
|
||
|
||
**严重程度**: 中
|
||
|
||
**问题描述**:
|
||
- 状态分散在多个ref中(20+个)
|
||
- 没有统一的状态管理
|
||
- 跨组件状态同步困难
|
||
|
||
**当前状态**:
|
||
|
||
```typescript
|
||
// 分散在各个地方
|
||
const fileList = ref<FileItem[]>([]) // 在index.vue
|
||
const favorites = ref<FavoriteFile[]>([]) // 在useFavorites
|
||
const filePath = ref('') // 在usePathNavigation
|
||
const fileContent = ref('') // 在useFileEdit
|
||
const previewUrl = ref('') // 在useFilePreview
|
||
// ... 还有15个
|
||
```
|
||
|
||
**问题场景**:
|
||
|
||
```typescript
|
||
// 场景1:多个地方需要访问同一状态
|
||
// 需要通过props传递或事件发射,导致props drilling
|
||
|
||
// 场景2:状态同步困难
|
||
// 修改fileList后,需要手动通知其他组件
|
||
|
||
// 场景3:无法追踪状态变化
|
||
// 不知道何时、何地修改了状态
|
||
```
|
||
|
||
**建议方案**:
|
||
|
||
**方案1:使用Pinia(推荐)**
|
||
|
||
```typescript
|
||
// stores/fileSystem.ts
|
||
import { defineStore } from 'pinia'
|
||
|
||
export const useFileSystemStore = defineStore('fileSystem', {
|
||
state: () => ({
|
||
// 文件列表
|
||
fileList: [] as FileItem[],
|
||
fileLoading: false,
|
||
|
||
// 当前选中
|
||
selectedFileItem: null as FileItem | null,
|
||
currentPath: '',
|
||
|
||
// 编辑状态
|
||
fileContent: '',
|
||
originalContent: '',
|
||
isEditMode: false,
|
||
|
||
// ZIP状态
|
||
isBrowsingZip: false,
|
||
currentZipPath: '',
|
||
currentZipDirectory: '',
|
||
|
||
// UI状态
|
||
showSidebar: true,
|
||
panelWidth: { left: 50, right: 50 }
|
||
}),
|
||
|
||
getters: {
|
||
// 计算属性
|
||
hasSelectedFile: (state) => state.selectedFileItem !== null,
|
||
isCodeFile: (state) => isCode(state.selectedFileItem?.name),
|
||
// ...
|
||
},
|
||
|
||
actions: {
|
||
// 异步操作
|
||
async loadDirectory(path: string) {
|
||
this.fileLoading = true
|
||
try {
|
||
this.fileList = await listDir(path)
|
||
this.currentPath = path
|
||
} finally {
|
||
this.fileLoading = false
|
||
}
|
||
},
|
||
|
||
async selectFile(path: string) {
|
||
const file = this.fileList.find(f => f.path === path)
|
||
this.selectedFileItem = file || { path, name: getFileName(path) }
|
||
await this.loadFileContent(path)
|
||
},
|
||
|
||
// ... 其他actions
|
||
}
|
||
})
|
||
```
|
||
|
||
**使用示例**:
|
||
|
||
```typescript
|
||
// 在任何组件中使用
|
||
import { useFileSystemStore } from '@/stores/fileSystem'
|
||
|
||
const fileSystemStore = useFileSystemStore()
|
||
const { fileList, selectedFileItem, currentPath } = storeToRefs(fileSystemStore)
|
||
|
||
// 调用actions
|
||
await fileSystemStore.loadDirectory('C:\\Users')
|
||
|
||
// 监听变化
|
||
watch(() => fileSystemStore.currentPath, (newPath) => {
|
||
console.log('Path changed:', newPath)
|
||
})
|
||
```
|
||
|
||
**方案2:使用provide/inject(临时方案)**
|
||
|
||
```typescript
|
||
// index.vue
|
||
import { provide, ref } from 'vue'
|
||
|
||
const fileList = ref([])
|
||
provide('fileList', fileList)
|
||
provide('loadDirectory', loadDirectory)
|
||
|
||
// FileListPanel.vue
|
||
const fileList = inject('fileList')
|
||
const loadDirectory = inject('loadDirectory')
|
||
```
|
||
|
||
---
|
||
|
||
### 缺陷4:ZIP逻辑分散 ⚠️
|
||
|
||
**严重程度**: 中
|
||
|
||
**问题描述**:
|
||
- ZIP相关代码分散在多个函数中
|
||
- 没有统一的ZIP管理模块
|
||
|
||
**分散位置**:
|
||
|
||
```typescript
|
||
// index.vue 中
|
||
const enterZipMode = async (zipFilePath: string) => { /* 60行 */ }
|
||
const exitZipMode = () => { /* 20行 */ }
|
||
const loadZipDirectory = async () => { /* 50行 */ }
|
||
const handleZipFileClick = async (zipFilePath: string) => { /* 30行 */ }
|
||
const readZipFile = async (zipFilePath: string) => { /* 30行 */ }
|
||
const getZipFileName = (zipPath: string): string => { /* 5行 */ }
|
||
const getZipBreadcrumbs = (): ZipBreadcrumbItem[] => { /* 20行 */ }
|
||
const navigateToZipDirectory = async (path: string) => { /* 5行 */ }
|
||
|
||
// 总计:~220行ZIP相关代码
|
||
```
|
||
|
||
**建议方案**:
|
||
|
||
```typescript
|
||
// composables/useZipBrowser.ts
|
||
export function useZipBrowser() {
|
||
// ZIP状态
|
||
const isBrowsingZip = ref(false)
|
||
const currentZipPath = ref('')
|
||
const currentZipDirectory = ref('')
|
||
const pathBeforeZip = ref('')
|
||
|
||
// ZIP操作
|
||
const enterZipMode = async (zipFilePath: string) => {
|
||
pathBeforeZip.value = filePath.value
|
||
currentZipPath.value = zipFilePath
|
||
currentZipDirectory.value = ''
|
||
isBrowsingZip.value = true
|
||
await loadZipDirectory()
|
||
}
|
||
|
||
const exitZipMode = () => {
|
||
if (pathBeforeZip.value) {
|
||
filePath.value = pathBeforeZip.value
|
||
pathBeforeZip.value = ''
|
||
}
|
||
currentZipPath.value = ''
|
||
currentZipDirectory.value = ''
|
||
isBrowsingZip.value = false
|
||
selectedFileItem.value = null
|
||
clearContent()
|
||
loadDirectory(filePath.value)
|
||
}
|
||
|
||
const loadZipDirectory = async () => {
|
||
const allFiles = await listZipContents(currentZipPath.value)
|
||
// 过滤和映射文件...
|
||
}
|
||
|
||
const readZipFile = async (zipFilePath: string) => {
|
||
// 读取ZIP文件逻辑...
|
||
}
|
||
|
||
const getZipBreadcrumbs = (): ZipBreadcrumbItem[] => {
|
||
// 生成面包屑...
|
||
}
|
||
|
||
const navigateToZipDirectory = async (path: string) => {
|
||
currentZipDirectory.value = path
|
||
await loadZipDirectory()
|
||
}
|
||
|
||
return {
|
||
// 状态
|
||
isBrowsingZip,
|
||
currentZipPath,
|
||
currentZipDirectory,
|
||
|
||
// 操作
|
||
enterZipMode,
|
||
exitZipMode,
|
||
loadZipDirectory,
|
||
readZipFile,
|
||
getZipBreadcrumbs,
|
||
navigateToZipDirectory
|
||
}
|
||
}
|
||
|
||
// index.vue 中使用
|
||
const {
|
||
isBrowsingZip,
|
||
currentZipPath,
|
||
enterZipMode,
|
||
exitZipMode
|
||
} = useZipBrowser()
|
||
```
|
||
|
||
---
|
||
|
||
### 缺陷5:错误处理不统一 ⚠️
|
||
|
||
**严重程度**: 中
|
||
|
||
**问题描述**:
|
||
- 错误处理方式不一致
|
||
- 有些用try-catch,有些用Message.error
|
||
- 没有统一的错误处理策略
|
||
|
||
**当前错误处理**:
|
||
|
||
```typescript
|
||
// 方式1:try-catch + Message.error
|
||
try {
|
||
await loadDirectory(path)
|
||
} catch (error) {
|
||
Message.error(`加载目录失败: ${error}`)
|
||
}
|
||
|
||
// 方式2:直接Message.error(无try-catch)
|
||
const loadFile = async (path: string) => {
|
||
const content = await readFile(path) // 可能抛出异常
|
||
fileContent.value = content
|
||
}
|
||
|
||
// 方式3:Composable中的错误处理
|
||
const readFile = async (path: string): Promise<string> => {
|
||
try {
|
||
const content = await readFileApi(path)
|
||
onSuccess?.('readFile', { path, size: content.length })
|
||
return content
|
||
} catch (error) {
|
||
onError?.('readFile', err) // 回调函数
|
||
throw err
|
||
}
|
||
}
|
||
|
||
// 方式4:无错误处理
|
||
const handleRefresh = async () => {
|
||
await loadDirectory(filePath.value) // 无错误处理
|
||
}
|
||
```
|
||
|
||
**建议方案**:
|
||
|
||
**方案1:统一错误处理中间件**
|
||
|
||
```typescript
|
||
// utils/errorHandler.ts
|
||
export class AppError extends Error {
|
||
constructor(
|
||
message: string,
|
||
public code: string,
|
||
public severity: 'info' | 'warning' | 'error' | 'fatal'
|
||
) {
|
||
super(message)
|
||
this.name = 'AppError'
|
||
}
|
||
}
|
||
|
||
export function handleError(error: Error | AppError, context?: string) {
|
||
// 记录日志
|
||
console.error(`[${context || 'App'}]`, error)
|
||
|
||
// 显示用户提示
|
||
if (error instanceof AppError) {
|
||
switch (error.severity) {
|
||
case 'info':
|
||
Message.info(error.message)
|
||
break
|
||
case 'warning':
|
||
Message.warning(error.message)
|
||
break
|
||
case 'error':
|
||
case 'fatal':
|
||
Message.error(error.message)
|
||
break
|
||
}
|
||
} else {
|
||
Message.error(`操作失败: ${error.message}`)
|
||
}
|
||
|
||
// 上报错误(可选)
|
||
// reportError(error, context)
|
||
}
|
||
|
||
// 包装异步函数
|
||
export function withErrorHandling<T extends (...args: any[]) => any>(
|
||
fn: T,
|
||
context?: string
|
||
): T {
|
||
return (async (...args: any[]) => {
|
||
try {
|
||
return await fn(...args)
|
||
} catch (error) {
|
||
handleError(error as Error, context)
|
||
throw error
|
||
}
|
||
}) as T
|
||
}
|
||
```
|
||
|
||
**使用示例**:
|
||
|
||
```typescript
|
||
// 在组件中使用
|
||
import { handleError, withErrorHandling } from '@/utils/errorHandler'
|
||
|
||
// 方式1:手动错误处理
|
||
const loadDirectory = async (path: string) => {
|
||
try {
|
||
fileList.value = await listDir(path)
|
||
} catch (error) {
|
||
handleError(error as Error, 'loadDirectory')
|
||
}
|
||
}
|
||
|
||
// 方式2:使用包装函数
|
||
const loadDirectory = withErrorHandling(async (path: string) => {
|
||
fileList.value = await listDir(path)
|
||
}, 'loadDirectory')
|
||
```
|
||
|
||
**方案2:Composable中统一错误处理**
|
||
|
||
```typescript
|
||
// composables/useFileOperations.ts
|
||
export function useFileOperations(options: UseFileOperationsOptions = {}) {
|
||
const { onSuccess, onError } = options
|
||
|
||
const listDirectory = async (path: string): Promise<FileItem[]> => {
|
||
try {
|
||
const result = await listDir(path)
|
||
onSuccess?.('listDirectory', result)
|
||
return result
|
||
} catch (error) {
|
||
const err = error as Error
|
||
// 统一错误处理
|
||
Message.error(`加载目录失败: ${err.message}`)
|
||
onError?.('listDirectory', err)
|
||
throw err
|
||
}
|
||
}
|
||
|
||
// ... 其他操作
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 缺陷6:类型定义不完整 ⚠️
|
||
|
||
**严重程度**: 低
|
||
|
||
**问题描述**:
|
||
- 部分函数缺少返回类型
|
||
- 部分参数类型使用`any`
|
||
- 缺少严格的类型检查
|
||
|
||
**示例**:
|
||
|
||
```typescript
|
||
// 问题1:缺少返回类型
|
||
const handleFileClick = (file: FileItem) => { // 应该: : void
|
||
emit('click', props.file)
|
||
}
|
||
|
||
// 问题2:使用any
|
||
const handleContextMenuAction = async (action: string, payload?: any) => {
|
||
// payload应该有明确的类型
|
||
}
|
||
|
||
// 问题3:可选类型过多
|
||
interface FileItem {
|
||
name: string
|
||
path: string
|
||
size: number
|
||
is_dir: boolean
|
||
modified_time?: string // 可选属性过多
|
||
}
|
||
```
|
||
|
||
**建议方案**:
|
||
|
||
```typescript
|
||
// 1. 为每个操作定义明确的类型
|
||
interface ContextMenuActionPayload {
|
||
file?: FileItem
|
||
path?: string
|
||
newName?: string
|
||
}
|
||
|
||
const handleContextMenuAction = async (
|
||
action: string,
|
||
payload?: ContextMenuActionPayload
|
||
) => {
|
||
// ...
|
||
}
|
||
|
||
// 2. 使用严格的类型
|
||
interface FileItem {
|
||
name: string
|
||
path: string
|
||
size: number
|
||
is_dir: boolean
|
||
modifiedTime: string // 移除可选,统一命名
|
||
}
|
||
|
||
// 3. 使用枚举代替字符串字面量
|
||
enum ContextMenuAction {
|
||
Open = 'open',
|
||
Rename = 'rename',
|
||
Delete = 'delete',
|
||
CreateFile = 'createFile',
|
||
CreateDir = 'createDir',
|
||
}
|
||
|
||
const handleContextMenuAction = async (
|
||
action: ContextMenuAction,
|
||
payload?: ContextMenuActionPayload
|
||
) => {
|
||
// ...
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 三、性能问题
|
||
|
||
### 问题1:大列表渲染性能 ⚠️
|
||
|
||
**严重程度**: 中
|
||
|
||
**问题描述**:
|
||
- 文件列表可能有成百上千个文件
|
||
- 使用`v-for`直接渲染,没有虚拟滚动
|
||
- 大目录下渲染性能差
|
||
|
||
**建议方案**:
|
||
|
||
```vue
|
||
<!-- 使用虚拟滚动 -->
|
||
<template>
|
||
<a-virtual-list
|
||
:data="fileList"
|
||
:item-height="40"
|
||
:visible-height="600"
|
||
>
|
||
<template #item="{ item }">
|
||
<FileItemRow :file="item" />
|
||
</template>
|
||
</a-virtual-list>
|
||
</template>
|
||
```
|
||
|
||
### 问题2:频繁的文件读取 ⚠️
|
||
|
||
**严重程度**: 低
|
||
|
||
**问题描述**:
|
||
- 没有缓存机制
|
||
- 重复读取相同文件
|
||
|
||
**建议方案**:
|
||
|
||
```typescript
|
||
// 添加简单的文件缓存
|
||
const fileCache = new Map<string, string>()
|
||
|
||
const readFile = async (path: string): Promise<string> => {
|
||
// 检查缓存
|
||
if (fileCache.has(path)) {
|
||
return fileCache.get(path)!
|
||
}
|
||
|
||
// 读取文件
|
||
const content = await readFileApi(path)
|
||
|
||
// 缓存内容(限制缓存大小)
|
||
if (fileCache.size > 100) {
|
||
const firstKey = fileCache.keys().next().value
|
||
fileCache.delete(firstKey)
|
||
}
|
||
fileCache.set(path, content)
|
||
|
||
return content
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 四、代码重复
|
||
|
||
### 重复1:路径标准化逻辑
|
||
|
||
**位置**:
|
||
- `useFavorites.ts:28-38`
|
||
- `index.vue:1093-1098`(selectFile函数中)
|
||
|
||
**重复代码**:
|
||
|
||
```typescript
|
||
// useFavorites.ts
|
||
const normalizePath = (path: string): string => {
|
||
return path.replace(/\\/g, '/').toLowerCase()
|
||
}
|
||
|
||
// index.vue
|
||
const normalizedPath = path.replace(/\\/g, '/').toLowerCase()
|
||
```
|
||
|
||
**建议方案**:
|
||
|
||
```typescript
|
||
// utils/pathUtils.ts
|
||
export function normalizePath(path: string): string {
|
||
return path.replace(/\\/g, '/').toLowerCase()
|
||
}
|
||
|
||
// 在所有地方使用
|
||
import { normalizePath } from '@/utils/pathUtils'
|
||
```
|
||
|
||
### 重复2:文件类型判断
|
||
|
||
**位置**:
|
||
- `useFilePreview.ts`
|
||
- `useFileEdit.ts`
|
||
- `fileUtils.ts`
|
||
|
||
**建议方案**:
|
||
- 统一使用`fileUtils.ts`中的函数
|
||
- 其他地方import使用
|
||
|
||
---
|
||
|
||
## 五、测试覆盖
|
||
|
||
### 问题:缺少单元测试 ⚠️⚠️
|
||
|
||
**严重程度**: 高
|
||
|
||
**当前状态**:
|
||
- 无单元测试
|
||
- 依赖手动测试
|
||
- 重构时容易引入bug
|
||
|
||
**建议方案**:
|
||
|
||
**1. 为Composables添加单元测试**
|
||
|
||
```typescript
|
||
// tests/composables/useFavorites.test.ts
|
||
import { describe, it, expect, beforeEach } from 'vitest'
|
||
import { useFavorites } from '@/composables/useFavorites'
|
||
|
||
describe('useFavorites', () => {
|
||
beforeEach(() => {
|
||
localStorage.clear()
|
||
})
|
||
|
||
it('should add favorite', () => {
|
||
const { favorites, toggleFavorite } = useFavorites()
|
||
const file = { path: '/test.txt', name: 'test.txt' }
|
||
|
||
toggleFavorite(file)
|
||
|
||
expect(favorites.value).toHaveLength(1)
|
||
expect(favorites.value[0].path).toBe('/test.txt')
|
||
})
|
||
|
||
it('should remove favorite', () => {
|
||
const { favorites, toggleFavorite, removeFavorite } = useFavorites()
|
||
const file = { path: '/test.txt', name: 'test.txt' }
|
||
|
||
toggleFavorite(file)
|
||
removeFavorite('/test.txt')
|
||
|
||
expect(favorites.value).toHaveLength(0)
|
||
})
|
||
|
||
// ... 更多测试
|
||
})
|
||
```
|
||
|
||
**2. 为工具函数添加测试**
|
||
|
||
```typescript
|
||
// tests/utils/pathUtils.test.ts
|
||
import { describe, it, expect } from 'vitest'
|
||
import { normalizePath, getParentPath } from '@/utils/pathUtils'
|
||
|
||
describe('pathUtils', () => {
|
||
describe('normalizePath', () => {
|
||
it('should normalize Windows paths', () => {
|
||
expect(normalizePath('C:\\Users\\file.txt')).toBe('c:/users/file.txt')
|
||
})
|
||
|
||
it('should normalize Unix paths', () => {
|
||
expect(normalizePath('/home/user/file.txt')).toBe('/home/user/file.txt')
|
||
})
|
||
|
||
it('should handle mixed separators', () => {
|
||
expect(normalizePath('C:/Users\\file.txt')).toBe('c:/users/file.txt')
|
||
})
|
||
})
|
||
|
||
// ... 更多测试
|
||
})
|
||
```
|
||
|
||
**3. 为组件添加测试**
|
||
|
||
```typescript
|
||
// tests/components/FileItemRow.test.ts
|
||
import { describe, it, expect } from 'vitest'
|
||
import { mount } from '@vue/test-utils'
|
||
import FileItemRow from '@/components/FileSystem/components/FileItemRow.vue'
|
||
|
||
describe('FileItemRow', () => {
|
||
it('should render file name', () => {
|
||
const wrapper = mount(FileItemRow, {
|
||
props: {
|
||
file: { name: 'test.txt', path: '/test.txt' }
|
||
}
|
||
})
|
||
|
||
expect(wrapper.text()).toContain('test.txt')
|
||
})
|
||
|
||
it('should emit click event', async () => {
|
||
const wrapper = mount(FileItemRow, {
|
||
props: {
|
||
file: { name: 'test.txt', path: '/test.txt' }
|
||
}
|
||
})
|
||
|
||
await wrapper.trigger('click')
|
||
|
||
expect(wrapper.emitted('click')).toBeTruthy()
|
||
})
|
||
|
||
// ... 更多测试
|
||
})
|
||
```
|
||
|
||
---
|
||
|
||
## 六、优先级改进计划
|
||
|
||
### 🔴 高优先级(立即执行)
|
||
|
||
| 序号 | 问题 | 建议 | 预计工作量 |
|
||
|------|------|------|-----------|
|
||
| 1 | 主组件过长 | 抽取useZipBrowser、useUIState、useDialogs | 2天 |
|
||
| 2 | 缺少单元测试 | 为核心Composables添加测试 | 3天 |
|
||
| 3 | 错误处理不统一 | 统一错误处理策略 | 1天 |
|
||
|
||
### 🟡 中优先级(本周完成)
|
||
|
||
| 序号 | 问题 | 建议 | 预计工作量 |
|
||
|------|------|------|-----------|
|
||
| 4 | FileEditorPanel过长 | 拆分为多个编辑器组件 | 2天 |
|
||
| 5 | 状态管理分散 | 使用Pinia统一管理 | 2天 |
|
||
| 6 | ZIP逻辑分散 | 抽取useZipBrowser | 1天 |
|
||
|
||
### 🟢 低优先级(后续迭代)
|
||
|
||
| 序号 | 问题 | 建议 | 预计工作量 |
|
||
|------|------|------|-----------|
|
||
| 7 | 大列表渲染 | 添加虚拟滚动 | 2天 |
|
||
| 8 | 代码重复 | 提取公共函数 | 1天 |
|
||
| 9 | 类型定义不完整 | 完善类型系统 | 2天 |
|
||
| 10 | 文件缓存 | 添加LRU缓存 | 1天 |
|
||
|
||
**总计**: 约17天(按每天8小时计算)
|
||
|
||
---
|
||
|
||
## 七、重构建议路线图
|
||
|
||
### 阶段1:基础重构(1周)
|
||
|
||
**目标**: 改善代码组织,提升可维护性
|
||
|
||
1. **抽取自定义Hook**(2天)
|
||
- 创建`useZipBrowser.ts`
|
||
- 创建`useUIState.ts`
|
||
- 创建`useDialogs.ts`
|
||
- 简化`index.vue`
|
||
|
||
2. **统一错误处理**(1天)
|
||
- 创建`errorHandler.ts`
|
||
- 更新所有错误处理
|
||
- 添加错误日志
|
||
|
||
3. **添加单元测试**(2天)
|
||
- 为`useFavorites`添加测试
|
||
- 为`usePathNavigation`添加测试
|
||
- 为工具函数添加测试
|
||
|
||
### 阶段2:架构优化(1周)
|
||
|
||
**目标**: 统一状态管理,提升性能
|
||
|
||
1. **引入Pinia**(2天)
|
||
- 创建`fileSystem` store
|
||
- 迁移状态到store
|
||
- 更新组件使用store
|
||
|
||
2. **拆分FileEditorPanel**(2天)
|
||
- 创建`Editor/`目录
|
||
- 拆分编辑器组件
|
||
- 更新引用
|
||
|
||
3. **性能优化**(1天)
|
||
- 添加虚拟滚动
|
||
- 添加文件缓存
|
||
- 优化渲染
|
||
|
||
### 阶段3:完善和测试(3天)
|
||
|
||
**目标**: 提升代码质量,确保稳定性
|
||
|
||
1. **完善类型定义**(1天)
|
||
- 移除`any`类型
|
||
- 添加枚举
|
||
- 完善接口
|
||
|
||
2. **消除代码重复**(1天)
|
||
- 提取公共函数
|
||
- 统一路径处理
|
||
- 统一文件类型判断
|
||
|
||
3. **集成测试**(1天)
|
||
- 端到端测试
|
||
- 性能测试
|
||
- 边界情况测试
|
||
|
||
---
|
||
|
||
## 八、总结
|
||
|
||
### 8.1 架构优势 ✅
|
||
|
||
1. **模块化**: 从单文件935行拆分为8个组件+5个Composables
|
||
2. **可复用**: Composables可在其他组件中复用
|
||
3. **类型安全**: 完整的TypeScript类型系统
|
||
4. **可测试**: 独立模块易于单元测试
|
||
|
||
### 8.2 架构劣势 ⚠️
|
||
|
||
1. **主组件过长**: index.vue有1313行
|
||
2. **状态分散**: 没有统一的状态管理
|
||
3. **缺少测试**: 无单元测试覆盖
|
||
4. **错误处理**: 不统一
|
||
|
||
### 8.3 改进建议
|
||
|
||
**短期(1周内)**:
|
||
1. ✅ 抽取useZipBrowser、useUIState、useDialogs
|
||
2. ✅ 统一错误处理
|
||
3. ✅ 添加核心功能单元测试
|
||
|
||
**中期(1个月内)**:
|
||
1. ✅ 引入Pinia状态管理
|
||
2. ✅ 拆分FileEditorPanel组件
|
||
3. ✅ 添加虚拟滚动优化性能
|
||
|
||
**长期(持续优化)**:
|
||
1. ✅ 完善单元测试覆盖率(目标80%+)
|
||
2. ✅ 集成E2E测试
|
||
3. ✅ 性能监控和优化
|
||
|
||
### 8.4 最终评价
|
||
|
||
**当前架构**: ⭐⭐⭐⭐☆ (4/5星)
|
||
|
||
**重构后预期**: ⭐⭐⭐⭐⭐ (5/5星)
|
||
|
||
**总体评价**: 这是一次成功的架构升级,从单文件重构为模块化架构是一个巨大的进步。虽然还存在一些可改进的地方,但整体架构设计合理,代码质量良好,为后续功能扩展和维护打下了坚实的基础。
|
||
|
||
---
|
||
|
||
**报告生成时间**: 2026-01-31
|
||
**报告版本**: v1.0
|
||
**下次审查建议**: 重构完成后1个月
|