# InterFile UI 与中文文件名修复提示词

把本文档直接交给服务器 agent 执行。  
本次只修复两个问题：页面表格布局被撑开、中文文件名上传/下载乱码。

## 背景

当前 InterFile 页面出现以下问题：

- 我的资料夹、共享给我、我发出的共享、回收站、管理后台等列表页，表格行没有从上往下紧凑排列。
- 少量数据时，表格行被撑到屏幕中间或底部。
- 回收站页面里“资料夹”和“文件”两个区域被上下拉开，第二个表头跑到底部。
- 上传中文文件名后，文件名可能变成乱码。

本地定位结论：

- UI 问题不是数据问题，是 CSS/grid 布局问题。
- `.panel.full` 是 grid 容器，很多页面直接把 `table` 放在 `.panel.full` 下方，`table` 作为 grid item 被 stretch，导致少量行被强行撑满整屏。
- 中文文件名乱码不是服务器没有中文字体，而是 multipart 上传时 `file.originalname` 在部分环境下可能被 `multer/busboy` 按 latin1 解码。

## 总要求

请修复 InterFile 当前两个问题：页面表格布局被撑开、中文文件名上传后乱码。只改应用代码，不修改服务器系统配置、Nginx 配置、Docker 配置和域名配置。

目标：

1. 所有列表页面从上往下自然排列。
2. 页面白底容器固定高度，内部内容区域滚动。
3. 不允许 table 行被拉伸到整屏高度。
4. 上传中文文件名必须正确保存和展示。
5. 下载中文文件名必须正确。

涉及文件：

- `src/styles/app.css`
- `src/App.tsx`
- `src/services/apiClient.ts`
- `server/src/main.ts`

## 一、修复 UI 容器滚动问题

当前问题：

`.panel.full` 是 grid 容器，很多页面直接把 `table` 放在 `.panel.full` 下方，table 作为 grid item 被 stretch，导致少量行被撑满整屏。回收站里多个 table 直接作为 grid child，还会导致第一个表占满 `1fr`，第二个表被挤到底部。

修复要求：

1. 新增统一滚动容器 class，例如：
   - `.panel-scroll`
   - `.stacked-tables`
2. `.panel.full` 应该是固定高度外壳：
   - `display: grid`
   - `grid-template-rows: auto minmax(0, 1fr)`
   - `overflow: hidden`
   - `height: calc(100vh - 58px)`
3. `.panel-scroll` 才负责滚动：
   - `min-height: 0`
   - `overflow: auto`
4. 不要让 table 直接承担 panel 的 `1fr` 高度。
5. table 必须自然高度排列，`tbody/tr` 不允许被拉伸。
6. 保留现有简洁紧凑风格，别加大卡片、别加多余文字。

需要改造的组件：

- `CanvasList`
- `SharedWithMe`
- `OutgoingShares`
- `TrashView`
- `AdminUsers`
- `AdminDepartments`
- `AdminRoles`
- `AdminSpaces`
- `AdminCanvases`
- `AdminFiles`
- `AdminShares`
- `AdminAudit`
- `AdminStorage`
- 其他所有 `<section className="panel full">` 里直接放 table 的页面

改造形式示例：

错误结构：

```tsx
<section className="panel full">
  <SectionTitle title="共享给我" />
  <table>...</table>
</section>
```

正确结构：

```tsx
<section className="panel full">
  <div className="table-toolbar">
    <SectionTitle title="共享给我" />
  </div>
  <div className="panel-scroll">
    <table>...</table>
  </div>
</section>
```

回收站这种多个表的页面：

```tsx
<section className="panel full">
  <div className="table-toolbar">
    <SectionTitle title="回收站" />
  </div>
  <div className="panel-scroll stacked-tables">
    <div className="subsection-title">资料夹</div>
    <table>...</table>
    <div className="subsection-title">文件</div>
    <table>...</table>
  </div>
</section>
```

CSS 要点：

- 删除或覆盖 `.card-scroll, .panel.full { overflow: auto; }` 这种写法。
- 改成 `.card-scroll, .panel-scroll { overflow: auto; min-height: 0; }`。
- `.panel.full` 自己不要滚动，只负责外壳。
- `.panel.full > table` 不应再作为正常结构使用。
- 移动端也要保持内部滚动，不要出现行高拉伸。

## 二、修复中文文件名上传乱码

当前前端：

`src/services/apiClient.ts` 里 `uploadFilesApi` 使用：

```ts
formData.append("files", file, relativeName);
```

后端：

`server/src/main.ts` 里直接使用：

```ts
item.originalname
```

问题：

`multer/busboy` 在部分环境下会把 multipart filename 的 UTF-8 按 latin1 解码，中文文件名会乱码。

修复方案：

前端上传时额外传一份 UTF-8 JSON 文件名列表，不再只依赖 multipart filename。

前端修改：

```ts
const names = files.map((file) => (file as File & { webkitRelativePath?: string }).webkitRelativePath || file.name);
formData.append("relativeNames", JSON.stringify(names));
files.forEach((file, index) => {
  formData.append("files", file, names[index]);
});
```

后端修改：

1. 解析 `req.body.relativeNames`：
   - `JSON.parse`
   - 必须是 `string[]`
   - 长度可以小于 `files.length`，但不能报错
2. 上传循环里优先用 `relativeNames[index]`：

```ts
const submittedName = relativeNames[index] || item.originalname;
const originalName = sanitizeRelativePath(normalizeUploadFileName(submittedName));
```

3. 再用 `originalName` 做：
   - `extensionOf`
   - `detectPreviewKind`
   - `storageKey`
   - `originalName`
   - `displayName`
   - node title

同时加一个兜底函数，防止仍然从 `item.originalname` 来的名字是乱码：

```ts
function normalizeUploadFileName(name: string) {
  const decoded = Buffer.from(name, "latin1").toString("utf8");
  const looksDecodedOk = decoded && !decoded.includes("\uFFFD");
  const looksLikeMojibake = /[ÃÂÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßà-ÿ]/.test(name);
  if (looksLikeMojibake && looksDecodedOk) return decoded;
  return name;
}
```

注意：

- 不要破坏文件夹上传的 `webkitRelativePath`。
- `sanitizeRelativePath` 仍然必须保留，防止路径穿越。
- 不要允许 `../` 或绝对路径。
- 中文文件名、中文文件夹名都要保留。

## 三、修复下载中文文件名

`server/src/main.ts` 里已有 `contentDisposition` 函数，但 MinIO 单文件下载没有用标准写法。

把单文件下载里的：

```ts
"response-content-disposition": `attachment; filename="${encodeURIComponent(file.originalName)}"`
```

改成：

```ts
"response-content-disposition": contentDisposition(file.originalName)
```

同时把 `contentDisposition` 改成带 ASCII fallback 的标准格式：

```ts
function contentDisposition(fileName: string) {
  const fallback = fileName
    .replace(/[^\x20-\x7E]/g, "_")
    .replace(/["\\]/g, "_");
  return `attachment; filename="${fallback}"; filename*=UTF-8''${encodeURIComponent(fileName)}`;
}
```

## 四、验证

完成后运行：

```bash
npm run build
npm --prefix server run build
```

本地/服务器验证：

1. 我的资料夹、共享给我、我发出的共享、已归档、回收站、管理后台所有 tab，表格都必须从顶部自然排列。
2. 少量数据时不能出现在屏幕中间。
3. 回收站里“资料夹”和“文件”两块必须从上往下排列，不能一个在顶部一个在底部。
4. 表格多数据时只滚动内部 `panel-scroll`，不让整个页面乱跳。
5. 上传文件：
   - `测试文件.md`
   - `经营汇报资料.xlsx`
   - `中文文件夹/接口配置.json`
6. 上传后列表显示中文正常。
7. 预览、下载、下载全部 zip 中文名正常。
8. 不要修改 Nginx、Docker、系统 locale 或服务器配置。

## 五、完成后回报

请回报：

- 改了哪些文件
- UI 拉伸问题的根因
- 中文文件名乱码的根因
- 构建结果
- 中文文件名上传/下载验证结果

