# 下载平台「预览界面文件名显示成百分号编码」修复记录

- 日期：2026-09-25
- 影响：下载中心（8899）桌面版列表页 → 右侧预览面板的文件名标题
- 状态：已修复并重启（`dl-server.service` MainPID 783500，`/healthz` OK）
- 备份：`~/Downloads/download-server.js.bak-20260925-200239`

## 1. 现象

在下载中心点开可预览的文件时，右侧预览面板顶部的文件名显示为整串百分号编码，例如：

```
%E9%BB%91%E7%A5%9E%E8%AF%9D%E6%82%9F%E7%A9%BA%20%E5%A4%A7%E5%9C%A3%E6%88%98%E6%AD%8C%E3%80%8A%E4%BA%91%E5%AE%AB%E8%BF%85%E9%9F%B3%E3%80%8B%20%E8%B1%AA%E5%8D%8E%E5%85%B8%E8%97%8F%E6%97%A0%E6%8D%9F%E7%89%88%20%5BBV148sge8E8T%5D%20%5B4K60-HEVC%5D.mp4
```

（对应文件 `dl-hub/53-B站视频下载/黑神话悟空 大圣战歌《云宫迅音》 豪华典藏无损版 [BV148sge8E8T] [4K60-HEVC].mp4`）

**整页预览（`?preview=1`）与移动端预览都是正常的**，只有桌面版左右分栏的右侧面板标题异常——这一条是关键线索，说明服务端解码没问题（`decodeURIComponent(u.pathname)` 在 `previewChrome()` 里用 `path.basename(filePath)` 显示，一直是中文），问题出在前端 JS。

## 2. 根因

桌面列表页行模板里，`data-name` 属性**只在目录可删除时**才输出（`listPage()` 中的 `delAttr` 把 `data-href` / `data-isdir` / `data-bytes` / `data-delmode` 和 `data-name` 一起按 `canDelete` 门控）：

```js
// 修复前
const delAttr = canDelete ? ` data-name="${esc(d.name)}" data-isdir="0" ... ` : '';
return `<tr class="file-row"${kindAttr} data-href="${href}"${delAttr}>...`;
```

可删除区只有 `09-迅雷下载`（delete）和 `用户上传`（trash）；`dl-hub` 下的普通子目录（如 `53-B站视频下载`）里 `canDelete === false`，于是行上**没有 `data-name`，只有百分号编码的 `data-href`**。

而前端 `LIST_JS` 打开预览面板时用的是：

```js
var name = row.getAttribute('data-name') || href;   // 没有 data-name 就退化成 data-href
pTitle.textContent = name;                          // 面板标题 = encodeURIComponent(文件名)
```

`data-href = encodeURIComponent(d.name)`，所以面板标题正好显示成用户看到的那串 `%E9%BB%91...`。这也解释了为什么「用户上传/09 区」里点开预览是正常的。

## 3. 修复（`~/Downloads/download-server.js`）

1. `listPage()` 行模板：`data-name` 改为**无条件输出**，删除相关属性仍按 `canDelete` 门控，语义不变：
   - 文件行：`<tr class="file-row" data-kind=… data-href=… data-name=… [data-isdir/data-bytes/data-delmode]>`
   - 目录行：`<tr class="dir-row" data-name=… [data-href/data-isdir/data-delmode]>`
2. 移动端列表行（`mobileListPage()`）同样改为 `data-name` 恒输出。
3. 三处 `LIST_JS` / `UPLOAD_JS` / `MOBILE_DEL_JS` 里的兜底逻辑加解码保险（防旧的缓存页面或将来漏写属性）：

```js
var name = row.getAttribute('data-name');
if (!name) { name = href; try { name = decodeURIComponent(name); } catch (err) {} }
```

（不动服务端的 URL 解码逻辑：URL 本身必须保持百分号编码，这是规范要求，不是 bug。）

## 4. 验证

- `node --check download-server.js` 通过；`bash ~/Downloads/restart-dl-server.sh` → active，HTTP 200，`/healthz` `{"ok":true,...}`。
- 列表 HTML 断言：目标行现在是
  `data-kind="video" data-href="%E9%BB%91…" data-name="黑神话悟空 大圣战歌《云宫迅音》 豪华典藏无损版 [BV148sge8E8T] [4K60-HEVC].mp4"`。
- **真实浏览器（Playwright Chromium）点击验证**：右侧 `#preview-title` 文本 = 中文全名，`/%[0-9A-F]{2}/` 断言为 `false`，视频内联播放正常 → 截图 `截图/2026-09-25-预览面板文件名修复后.png`。
- 不回归验证：
  - `09-迅雷下载` 目录行/文件行仍带 `data-href/data-isdir/data-bytes/data-delmode`，删除弹窗文件名显示正常（实测弹窗标题为中文目录名）。
  - `用户上传` 目录行属性完整；`DELETE …?dry=1` 统计接口返回正常（只读，未做任何删除）。
  - 移动端整页预览 `<h1 class="fname">` 仍为中文全名。

## 5. 影响面与后续

- 只改了 HTML 属性与前端兜底逻辑，**未改路由、权限（可删除区判定）、上传/转码/字幕链路**。
- 浏览器可能缓存了旧列表页，用户侧若仍看到编码标题，强刷一次（Ctrl/Cmd+Shift+R）即可。
- 经验：凡是「前端显示名 = 后端 URL 属性」的写法，属性要无条件输出，且前端要有 `decodeURIComponent` 兜底。
