# 文件内容区显示问题修复（Bug Fixes）

---

## 修复记录：Excel/Word 预览失败

### 修复日期
2026-02-16

### 问题描述
用户点击 Excel 文件时，预览区域显示错误：
```
❌ Excel 预览失败
Failed to fetch
💡 提示：尝试使用外部应用打开文件
```

### 根本原因
在 Wails 应用中，前端通过 `fetch` 请求本地 HTTP 服务器 (`http://localhost:18765`) 获取 Office 文件内容。这种方式在 Wails 的 webview 环境中可能会失败：
1. Wails webview 对外部 HTTP 请求有限制
2. 本地文件服务器的启动时机可能存在竞态条件
3. 网络请求在桌面应用环境中不够可靠

### 解决方案
改用 Wails IPC 机制直接读取二进制文件，不再依赖本地 HTTP 服务器。

### 修改的文件

#### 1. 后端：添加二进制文件读取 API
**位置：** `internal/filesystem/service.go`

添加 `ReadFileAsBase64` 方法：
```go
// ReadFileAsBase64 读取二进制文件并返回 base64 编码的字符串
func (s *FileSystemService) ReadFileAsBase64(path string) (string, error) {
    // 路径验证
    if err := s.validatePath(path); err != nil {
        return "", err
    }

    // 检查文件扩展名是否在允许列表中
    ext := strings.ToLower(filepath.Ext(path))
    if !s.fileTypeManager.IsAllowed(ext) {
        return "", fmt.Errorf("不允许的文件类型: %s", ext)
    }

    // 限制文件大小（最大 100MB）
    const maxFileSize = 100 * 1024 * 1024
    // ... 读取文件并编码为 base64 ...

    // 返回 data URI 格式
    return fmt.Sprintf("data:%s;base64,%s", mimeType, encoded), nil
}
```

#### 2. 后端：暴露 API 接口
**位置：** `app.go`

```go
// ReadFileAsBase64 读取二进制文件并返回 base64 编码的字符串
func (a *App) ReadFileAsBase64(path string) (string, error) {
    return a.filesystem.ReadFileAsBase64(path)
}
```

#### 3. 前端：添加 API 调用
**位置：** `frontend/src/api/system.ts`

```typescript
export async function readFileAsBase64(path: string): Promise<string> {
  if (!window.go?.main?.App?.ReadFileAsBase64) {
    throw new Error('ReadFileAsBase64 API 不可用')
  }
  return await window.go.main.App.ReadFileAsBase64(path)
}
```

#### 4. 前端：修改预览函数
**位置：** `frontend/src/components/FileSystem/components/FileEditorPanel.vue`

修改前（通过 HTTP 服务器）：
```javascript
const response = await fetch(`${serverURL}/localfs/${encodedPath}`)
const blob = await response.blob()
```

修改后（通过 Wails IPC）：
```javascript
// 直接通过 Wails API 读取文件（base64 编码）
const dataUri = await readFileAsBase64(filePath)

// 将 data URI 转换为 Blob
const response = await fetch(dataUri)
const blob = await response.blob()
```

### 优点
- **更可靠**：直接通过 Wails IPC 通信，不依赖网络请求
- **更安全**：在后端进行文件类型验证和大小限制
- **更简洁**：移除了复杂的重试逻辑和错误处理

### 测试验证
- [x] Excel 文件预览正常显示
- [x] Word 文件预览正常显示
- [x] 文件大小限制生效
- [x] 不允许的文件类型被正确拒绝

---

## 修复记录：文件内容区状态管理

### 修复日期
2026-01-28

## 问题描述

用户报告了三个问题：
1. **闪烁问题**：打开新的目录或文件后，整个文件管理区域闪烁刷新
2. **文件名不显示**：文件内容区上面之前有文件名现在没有了
3. **切换目录后文件名消失**：点击切换到别的目录后，文件名消失了
4. **二进制文件乱码**：点击没有后缀的文件时，加载了一堆乱码字符（实际是二进制文件）

## 根本原因

### 问题1：函数名错误
在计算属性 `isFileInCurrentDirectory` 中使用了不存在的 `normalizePath` 函数，应该使用 `normalizeFilePath`。这导致了运行时错误，使得计算失败并返回空值。

**错误代码：**
```javascript
return normalizePath(fileDir) === normalizePath(filePath.value)
```

### 问题2：频繁的计算和重新渲染
- `isFileInCurrentDirectory` 计算属性频繁调用 `normalizeFilePath`，性能较差
- `listDirectory` 函数在目录切换时立即清空 `selectedFileItem`，导致视觉闪烁

## 修复方案

### 修复1：使用正确的函数名

**位置：** `frontend/src/components/FileSystem.vue:1437-1449`

**修复内容：**
```javascript
// 判断当前打开的文件是否在当前目录中（优化性能，减少计算）
const isFileInCurrentDirectory = computed(() => {
  if (!selectedFilePath.value || !filePath.value) return false

  // 提取文件的父目录
  const lastBackslash = selectedFilePath.value.lastIndexOf('\\')
  const lastSlash = selectedFilePath.value.lastIndexOf('/')
  const lastSeparator = Math.max(lastBackslash, lastSlash)

  if (lastSeparator === -1) return false

  const fileDir = selectedFilePath.value.substring(0, lastSeparator)

  // 直接比较路径，避免频繁调用 normalizeFilePath
  // 只在必要时才进行路径标准化
  const fileDirNormalized = fileDir.replace(/\\/g, '/').replace(/\/$/, '')
  const currentPathNormalized = filePath.value.replace(/\\/g, '/').replace(/\/$/, '')

  return fileDirNormalized.toLowerCase() === currentPathNormalized.toLowerCase()
})
```

**改进点：**
- 不再调用 `normalizeFilePath` 函数，改用简单的字符串替换
- 性能优化：直接进行字符串比较而不是函数调用
- 统一路径格式：将反斜杠转换为正斜杠，并移除尾部斜杠
- 忽略大小写：使用 `toLowerCase()` 进行大小写不敏感比较

### 修复2：添加错误处理

**位置：** `frontend/src/components/FileSystem.vue:1452-1473`

**修复内容：**
```javascript
const currentFileName = computed(() => {
  if (isBrowsingZip.value && selectedFilePath.value) {
    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 ''
})
```

**改进点：**
- 添加 try-catch 错误处理
- 即使计算失败也能返回基本的文件名
- 防止因计算错误导致文件名完全不显示

### 修复3：减少视觉闪烁

**位置：** `frontend/src/components/FileSystem.vue:847-883`

**修复内容：**
```javascript
const listDirectory = async () => {
  if (!filePath.value) return

  if (isBrowsingZip.value && filePath.value !== originalPathBeforeZip.value) {
    debugLog('检测到路径切换，退出 ZIP 模式')
    exitZipMode()
  }

  addToHistory(filePath.value)
  pushToNavigationHistory(filePath.value)
  fileLoading.value = true
  try {
    fileList.value = await listDir(filePath.value)

    // 目录加载完成后，检查原选中的文件是否还在新目录中
    // 如果不在，清空 selectedFileItem，避免视觉闪烁
    if (selectedFileItem.value) {
      const stillExists = fileList.value.some(f => f.path === selectedFileItem.value.path)
      if (!stillExists) {
        selectedFileItem.value = null
      }
    }

    if (selectedFilePath.value) {
      debugLog('[listDirectory] 目录已切换，保留原文件引用:', selectedFilePath.value)
    }
  } catch (error) {
    Message.error('列出目录失败: ' + error.message)
    selectedFileItem.value = null
  } finally {
    fileLoading.value = false
  }
}
```

**改进点：**
- 延迟清空 `selectedFileItem`，等到新目录加载完成后再检查
- 如果原文件仍在新目录中，保持选中状态
- 如果原文件不在新目录中，再清空选中状态
- 减少了不必要的视觉闪烁

### 修复4：优化样式

**位置：** `frontend/src/components/FileSystem.vue:3389-3406`

**修复内容：**
```css
.panel-filename {
  font-weight: normal;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 500px;  /* 从 300px 增加到 500px */
  display: inline-block;
  vertical-align: middle;
}

.panel-filename.file-outside-dir {
  color: rgb(var(--warning-6));
  font-weight: 500;
}

.file-location-hint {
  font-size: 11px;
  color: var(--color-text-3);
  font-weight: normal;
  white-space: nowrap;
  display: inline;
}
```

**改进点：**
- 增加最大宽度到 500px，可以显示更长的路径
- 添加 `display: inline-block` 确保布局正确
- 提示文字使用 `display: inline` 避免换行

### 修复5：切换目录时保留文件名

**位置：** `frontend/src/components/FileSystem.vue:958-967`

**问题描述：**
当用户点击切换到其他目录时，文件名消失了。这是因为在 `selectFile` 函数中，当点击目录时会清空 `selectedFilePath`。

**修复内容：**
```javascript
if (item.is_dir) {
  // 目录：更新路径并列出内容
  // 注意：不要清空 selectedFilePath，保留原文件内容以便跨目录编辑
  filePath.value = path
  addToHistory(path)
  listDirectory()
}
```

**同时修复收藏目录跳转：**
**位置：** `frontend/src/components/FileSystem.vue:2866-2875`

```javascript
if (fav && fav.is_dir) {
  // 目录：列出内容
  // 注意：不要清空 selectedFilePath，保留原文件内容以便跨目录编辑
  filePath.value = path
  addToHistory(path)
  listDirectory()
}
```

**改进点：**
- 移除了 `selectedFilePath.value = ''` 语句
- 保留了跨目录编辑的文件内容
- 用户可以在浏览其他目录时继续编辑原文件

### 修复6：二进制文件检测

**位置：** `frontend/src/components/FileSystem.vue:2008-2045`

**问题描述：**
点击没有后缀的文件时，加载了一堆乱码字符。这些文件实际上是二进制文件，但代码没有检测，直接当作文本显示。

**修复内容：**
添加二进制内容检测逻辑：
```javascript
// 检查前 1000 个字符中二进制字符的比例
const checkLength = Math.min(content.length, 1000)
let binaryCharCount = 0
for (let i = 0; i < checkLength; i++) {
  const charCode = content.charCodeAt(i)
  // 检查是否为空字节或其他控制字符（除了常见的换行符、制表符等）
  if (charCode === 0 || (charCode < 32 && charCode !== 9 && charCode !== 10 && charCode !== 13)) {
    binaryCharCount++
  }
}

// 如果二进制字符超过 5%，认为是二进制文件
const binaryRatio = binaryCharCount / checkLength
if (binaryRatio > 0.05) {
  // 显示友好的二进制文件提示信息
  isBinaryFile.value = true
  isEditMode.value = false
  // ... 显示提示信息 ...
  return
}
```

**改进点：**
- 自动检测二进制内容（不依赖文件扩展名）
- 检查前 1000 个字符中的二进制字符比例
- 阈值设置为 5%，平衡误报和漏报
- 显示友好的提示信息，而不是乱码
- 支持有后缀和无后缀的二进制文件检测

**检测算法：**
- 空字节（charCode === 0）肯定是二进制
- 控制字符（charCode < 32）除了 Tab(9)、LF(10)、CR(13) 外都是二进制
- 如果二进制字符比例超过 5%，判定为二进制文件

## 技术要点

1. **函数命名一致性**：确保所有函数调用都使用正确的名称
2. **性能优化**：避免在计算属性中进行昂贵的操作
3. **延迟更新**：等到数据加载完成后再更新UI状态
4. **错误处理**：在关键路径添加 try-catch，防止连锁失败
5. **样式优化**：合理的布局和宽度设置，确保内容正确显示
6. **状态保留**：跨目录操作时保留编辑状态，提升用户体验
7. **内容检测**：智能检测二进制文件，避免显示乱码

## 相关文件

- `frontend/src/components/FileSystem.vue` - 主要修改文件
- `frontend/src/utils/fileUtils.js` - 工具函数（normalizeFilePath 等）
- `docs/file-content-state-fix.md` - 之前的改进文档

### 1. 文件名显示测试
- [x] 打开文件后，文件名正确显示在内容区标题
- [x] 切换目录后，文件名仍然显示（如果之前打开了文件）
- [x] 文件在当前目录时，只显示文件名
- [x] 文件不在当前目录时，显示完整路径和提示

### 2. 性能测试
- [x] 切换目录时无明显闪烁
- [x] 打开文件时响应迅速
- [x] 计算属性不会频繁触发重新渲染

### 3. 错误处理测试
- [x] 即使路径计算失败，文件名仍能显示
- [x] 控制台不会有运行时错误

### 4. 跨目录编辑测试
- [x] 打开文件A
- [x] 切换到目录B
- [x] 文件A的内容和文件名仍然保留
- [x] 可以继续编辑文件A
- [x] 保存时正确保存到文件A的原位置

### 5. 二进制文件检测测试
- [x] 打开无后缀的二进制文件，显示友好提示
- [x] 不会显示乱码字符
- [x] 检测算法不会误判文本文件
- [x] 有后缀的二进制文件也能正确识别

## 用户体验改进

### 修复前
- ❌ 文件名完全不显示
- ❌ 切换目录时整个区域闪烁
- ❌ 控制台有函数未定义错误
- ❌ 切换目录后文件名消失
- ❌ 二进制文件显示乱码

### 修复后
- ✅ 文件名正常显示
- ✅ 切换目录时流畅无闪烁
- ✅ 性能优化，响应更快
- ✅ 错误处理更健壮
- ✅ 切换目录后保留文件名和内容
- ✅ 二进制文件显示友好提示

## 技术要点

1. **函数命名一致性**：确保所有函数调用都使用正确的名称
2. **性能优化**：避免在计算属性中进行昂贵的操作
3. **延迟更新**：等到数据加载完成后再更新UI状态
4. **错误处理**：在关键路径添加 try-catch，防止连锁失败
5. **样式优化**：合理的布局和宽度设置，确保内容正确显示

## 相关文件

- `frontend/src/components/FileSystem.vue` - 主要修改文件
- `frontend/src/utils/fileUtils.js` - 工具函数（normalizeFilePath 等）
- `docs/file-content-state-fix.md` - 之前的改进文档
