Files
StarlightRadio/HANDOFF.md
T
Xiao-no-love 16df0a7d08 v2.4: UI 规范化(token体系) + 复制直播链接 + 设置页间距
- 建立完整 token 体系: 色板/字号阶梯(9档)/圆角阶梯/阴影/动效/间距
- 字号 42→9 种、圆角 20→8 种、正文硬编码色清零
- 补丁区(v2/v2.1/v2.2 追加段)并入主体系: 统一缩进 + 全用 token
- header「打开直播流」改为「复制直播流链接」(navigator.clipboard + 降级)
- header 图标按钮统一为胶囊形(20px 圆角)
- 设置页: .page.active 加 gap, section-body 内边距对齐, 缓解拥挤

测试: api 16/16, e2e 17/17, write 5/5 全绿
2026-09-24 22:20:55 +08:00

418 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 交接文档 · 音乐在线电台 v2
> 本文件用于跨会话交接。上下文耗尽后,新会话**先读此文件**再动手。
---
## 0. TL;DR
- **项目**:`F:\music_online\demo` — Node.js + FFmpeg 的 HLS 音乐电台
- **状态**:v2.3 **功能完整 + UI 已去 AI 味重构**,38 项测试全绿(api16/e2e17/write5);详见 §16。
- **服务**:正在运行 `svc005`,`http://127.0.0.1:3000`,密码 `test123`(测试用)
- **Git**:`ea5aa0c`(基线) → `b8e9bf3`(v2主体) → `a4745db`(收尾)
---
## 1. 运行 / 环境
- 环境:Windows 11,Node v24.5.0,npm 11.5.1,FFmpeg 9.0.1(含 ffprobe,均在 PATH),Python 3.11
- 启动:
```cmd
cd /d F:\music_online\demo
set PANEL_PASSWORD=test123 && node index.js
```
- 依赖:`better-sqlite3`、`multer`、`chokidar`、`express`(已装)
- 登录:浏览器开 `http://127.0.0.1:3000`,输入密码
---
## 2. 目录结构(v2)
```
demo/
├── index.js # 入口,仅 require("./src/server").start()
├── package.json # v2.0.0
├── data.db # SQLite(自动生成,WAL)
├── HANDOFF.md # 本文件
├── README.md # v2 说明
├── src/
│ ├── server.js # 装配:中间件、路由、启动、优雅退出
│ ├── config.js # 所有配置(环境变量解析)
│ ├── logger.js # 分级日志 + 落盘 + 内存环形缓冲(tail)
│ ├── db.js # SQLite 初始化 + schema(5表) + WAL
│ ├── store.js # 数据访问层,所有 SQL 收敛于此
│ ├── util.js # fileHash/解析文件名/原子写/目录大小 等
│ ├── media.js # ffprobe + FFmpeg 切片 + 并发队列 + 进程追踪
│ ├── metadata.js # 网易云 API + 封面下载本地化 + 元数据补全
│ ├── livestream.js # LiveStream 类 + 多路流管理 + 缓存状态/清理
│ ├── scanner.js # audio/ 监听 + 同步 DB
│ ├── auth.js # Cookie 会话鉴权 + requireAuth 中间件
│ ├── sse.js # Server-Sent Events 广播
│ └── routes/
│ ├── auth.js # /api/auth/*
│ ├── music.js # /api/music/* (搜索/详情/下载/导入/删除/enrich)
│ ├── resources.js # /api/resources
│ ├── streams.js # /api/streams/* (流管理 + 播放列表)
│ └── system.js # /api/system/* (缓存/日志/配置/设置)
├── public/
│ ├── index.html # 主面板
│ ├── login.html # 登录页(新)
│ ├── css/style.css # 含 v2 追加样式(.stream-select/.import-card/.now-badge)
│ ├── js/app.js # v2 重写:多路流/鉴权/导入/删除
│ ├── vendor/ # 本地化:bootstrap/icons/sortable + woff2
│ ├── covers/ # 本地化封面(每首一个 jpg)
│ └── favicon.ico/.png
├── scripts/fetch_vendor.py # 一次性下载前端 CDN 资源(已跑过)
├── audio/ # 音频(5 首 mp3,已 gitignore)
├── hls/ # 直播输出 hls/{streamId}/index.m3u8
├── hls_cache/ # 切片缓存 hls_cache/{fileHash}/
├── tmp/uploads/ # multer 上传临时目录
└── test_api.py / test_e2e.py / test_write.py / test_enrich.py # 测试脚本
```
---
## 3. 数据模型(SQLite, src/db.js)
```sql
songs(id, file UNIQUE, name, artist, cover, duration, file_size,
heat, cache_status, created_at)
-- file 存「文件名」不是绝对路径(可移植)
-- cover 存 web 路径如 /covers/xxx.jpg
streams(id PK, name, enabled, current_pos, media_seq, created_at)
-- 默认流 id='main',不可删除
stream_playlist(id PK, stream_id, song_id, position)
-- 每路流独立播放列表,外键 CASCADE
settings(key PK, value)
sessions(token PK, created_at, expires_at)
```
**关键**:切片缓存 `hls_cache/{md5(文件名)}/` 与「流」无关,多路共享,省空间。
---
## 4. API 清单
除 `/api/auth/*`、`/hls`、`/hls_cache`、静态资源、`/login` 外,**全部需登录**。
| 方法 | 路径 | 说明 |
|------|------|------|
| POST | `/api/auth/login` | `{password}` → 设 cookie |
| POST | `/api/auth/logout` | 登出 |
| GET | `/api/auth/me` | 登录状态 |
| GET | `/api/resources` | 资源列表(含 cacheSize/cover) |
| GET | `/api/music/search?keyword=` | 网易云搜索 |
| POST | `/api/music/detail` | `{id}` |
| POST | `/api/music/download` | `{id,name,artist,albumImage}` |
| POST | `/api/music/import` | **multipart**:audio + title + artist |
| POST | `/api/music/delete` | `{file}` 级联删 |
| POST | `/api/music/delete-batch` | `{files:[]}` |
| POST | `/api/music/enrich` | `{file, skipMatch?}` 重匹配元数据 |
| GET | `/api/streams` | 流列表 |
| POST | `/api/streams` | `{id,name}` 创建 |
| DELETE | `/api/streams/:id` | 删除(main 不可删) |
| POST | `/api/streams/:id/start\|stop` | 启停 |
| GET | `/api/streams/:id/playlist` | 播放列表(含 entryId/isCurrent) |
| POST | `/api/streams/:id/playlist/add` | `{file}` |
| POST | `/api/streams/:id/playlist/remove` | `{entryId}` |
| POST | `/api/streams/:id/playlist/reorder` | `{order:[entryId...]}` |
| POST | `/api/streams/:id/playlist/clear\|next\|prev\|jump` | |
| GET | `/api/system/cache` | 缓存占用 |
| POST | `/api/system/cache/cleanup` | 手动清理 |
| GET | `/api/system/logs?tail=` | 日志 |
| GET | `/api/events` | SSE(需 cookie 鉴权) |
**统一返回**:`{ok:true, ...}` 或 `{ok:false, error:"..."}`。
---
## 5. 直播流地址
- 默认:`/hls/main/index.m3u8`
- 其它:`/hls/{streamId}/index.m3u8`
- 切片:`/hls_cache/{hash}/indexN.ts`
---
## 6. 关键设计决策(勿轻易推翻)
1. **SQLite 用 better-sqlite3 + WAL**:单进程应用,同步 API 最省事;WAL 解决读写互斥。用户曾担心并发,已解释并落地。
2. **不迁移 v1 的 JSON 数据**:用户明确说旧数据不要。
3. **鉴权选"中档"**:整站 Cookie 会话登录,面板+API 锁,**HLS 流不锁**(播放器难带 cookie)。
4. **封面/资源全本地化**:`public/covers/` 存封面,`public/vendor/` 存前端库;系统字体替代 Google Fonts。
5. **songs.file 存文件名**:不是绝对路径,迁移目录不失效。
6. **导入先落 tmp 再 rename**:避免 chokidar 扫到半截文件。
---
## 7. 测试方法
服务起来后(svc005 / 或新起)跑:
```cmd
cd /d F:\music_online\demo
set PYTHONIOENCODING=utf-8
python test_api.py # 16 项:鉴权/资源/流/缓存/日志
python test_e2e.py # 17 项:静态资源/登录/首页/HTML元素
python test_write.py # 5 项:导入 + 删除(用 tmp 副本,不动原曲)
python test_enrich.py # 给 5 首补封面(已跑过,5/5 成功)
```
上次全绿:**38/38**。但**注意**:这些只测了 HTTP 层,**浏览器端 JS 交互没自动化测过**。
---
## 8. 已完成清单
- [x] SQLite 替换 JSON(5 表 + WAL)
- [x] 级联删除 `/api/music/delete` + 批量
- [x] 多路流 LiveStream 类 + 每路独立播放列表
- [x] Cookie 会话鉴权 + 登录页 + 鉴权绕过漏洞修复
- [x] 本地上传导入(multer)+ 元数据自动匹配
- [x] 封面本地化 + vendor 本地化 + 系统字体
- [x] 日志系统(分级/落盘/面板可查)
- [x] API 统一返回结构
- [x] 优雅退出补 FFmpeg 子进程 kill
- [x] 模块化拆分(1233 行单文件 → src/ 12 模块)
- [x] 前端 app.js 重写(多路流/鉴权/导入/删除)
- [x] **前端多路流管理 UI(频道管理模态框:新建/删除/切换)** ← v2.1
---
## 9. ⚠️ 待排查 / 已知问题(重点!)
### 9.1 用户反馈"功能还有问题" —— **具体未明,需先问清楚**
> 下一会话**第一件事**:问用户到底哪个功能出问题(现象、复现步骤、控制台报错)。
> 可能的方向见下。
### 9.2 前端多路流管理 UI —— ✅ 已解决(v2.1)
- 已补:header 新增 `#btnStreamMgr` 入口 → 弹出「频道管理」模态框 `#streamModal`。
- 功能:列出所有流(含运行状态/当前标记)、一键切换、删除(main 受保护)、新建(ID+名称,前端正则校验)。
- 相关:`index.html` 新增 modal 结构;`app.js` 新增 `openStreamManager/renderStreamMgr/switchStream/createStreamFromForm/deleteStreamById`;`style.css` 末尾追加 `.modal-*/.stream-*` 样式。
- 已验证:后端 create/delete/重复/非法ID/main保护 全通过;app.js `node --check` 通过;JS 引用 ID 与 HTML 无缺失。
### 9.3 前端 JS 交互未实测
- `public/js/app.js` 是重写的,只做了语法检查 + HTTP 层测试。
- **未验证**:SSE 实时刷新、拖拽排序 reorder、mini player、页面切换、导入进度提示等浏览器行为。
- 建议:手动开浏览器点一遍,看 Console 有无报错。
### 9.4 潜在 Bug 点 —— ✅ 已处理(v2.1)
- ~~`enforceCacheSizeLimit` 依赖 streams Map~~ → 保护集为空时最坏误删缓存,无害;保留。
- ~~`scanner.js` 里 `const parsed = meta.sanitizeFileName;` 死代码~~ → **已删**。
- `metadata.js enrichSong` 启动时重匹配慢歌 → 保留(可接受)。
- 多路流并发预生成排队 → 观察中。
- `app.js` 的 `esc()` 用 `String.fromCharCode(38)` 绕过工具层转义,**勿优化**。
### 9.4b v2.1 死代码清理 + Bug 修复(本轮)
- **删死代码**:`media.setCacheCallbacks`(含 `onCacheReady/onCacheError`,无调用方)、`auth.getPassword/isGenerated`、`store.setStreamEnabled/purgeExpiredSessions`、`metadata` 无用的 `sanitizeFileName` 导入导出、`scanner` 死变量 `parsed`。全部 `node --check` 通过。
- **修 Bug**:删歌时内存 Map(`cacheStatus/retryCount/segmentsCache/inFlight`)残留 → 新增 `live.forgetSong(file)`,删除路由两处改用它,避免同名文件重导读到脏状态。
- **修 Bug**:`server.js` 移除非法 CORS 头(`Allow-Origin:*` + `Allow-Credentials:true` 浏览器会拒;本地同源应用无需 CORS)。
- **测试**:45 项全绿(api 16 / e2e 17 / write 5 / 真实 ogg 导入 7)。
### 9.5 前端 HTML 元素 ID 对照(改前端时用)
- 头部:`cacheInfo` `streamSelect` `liveLink` `btnLogout`
- 搜索页:`musicId` `btnSearch` `parseHint` `parsedId` `musicPreview` `musicCover` `musicName` `musicArtist` `musicDetails` `btnDownload` `searchResults` `searchTips`
- 导入卡片:`importCard` `importFile` `importTitle` `importArtist` `btnImport` `importProgress`
- 资源页:`resourceCount` `resourceSearch` `resourceList`
- 列表页:`playlistCount` `btnClear` `playlistList`
- mini player:`miniPlayer` `mpProgress` `mpBody` `mpCover` `mpName` `mpArtist` `mpTime` `mpPlay` `mpExpand` `musicPlayer`
- 导航:`.nav-tab[data-page]`,页面 `pageSearch/pageResources/pagePlaylist/pageAbout`
---
## 10. 工具层踩坑记录(写代码时注意)
1. **XML 实体转义会吃掉 `&` `"`**:写 JS/CSS 含这些字面量时,用 `String.fromCharCode(38)` 或字符码构造,别直接写。
2. **replace_lines 多段锚点行号易偏**:先 `file_read` / python 打印实际行号再改;改错了整批不生效或误删。
3. **smart_patch 行数必须精确**,且锚点行号要对;容易失败,简单改动优先 replace_lines。
4. **后台服务用 action background="true"**(不要 `start /b`,会粘连卡死)。
5. **service_stop 会连子进程一起杀**(node 的 ffmpeg 子进程会被带走)。
6. **Windows 无 `head`/`tail`**,用 `findstr` 或 python。
7. **改完源码要重启 node**(磁盘 .py/.js 改了,运行中的进程不生效)。
8. **Python 跑 Windows 输出**:`chcp 65001 && set PYTHONIOENCODING=utf-8`。
9. **`node -X utf8` 是错的**(那是 Python 的选项),node 直接 `node` 即可。
---
## 11. 当前运行态
- 后台服务:`svc005`(PID 15308 起),日志 `service_log svc005`
- DB:`data.db`(5 首歌曲,封面已本地化)
- 缓存:`hls_cache/` 5 个目录,约 28MB
- Git 工作区:干净(最新 `a4745db`)
---
## 12. 下一步建议(按优先级)
1. **问清用户的"问题"具体是什么** ← 最优先
2. 补前端"新建/删除流"UI(若需求确认)
3. 浏览器手动过一遍所有交互,修 Console 报错
4. 清理死代码(§9.4)
5. 正式化:设置固定 `PANEL_PASSWORD`,写 systemd/服务化启动
6. 补多路流并发压力测试
---
## 13. 用户视角 · 待优化点(下一会话可选)
> 站在真实使用者角度梳理,按"体验影响"排序。均**未实施**,需老板定优先级。
### 高价值(体验硬伤)
1. **缺"电台收听页"** —— 现在 `liveLink` 只打开裸 `.m3u8`,浏览器无法直接播放。应在站内加一个 HLS.js 播放页(选频道→收听直播),否则"电台"没法在浏览器里直接听。
2. **面板无"正在播放"实时卡片** —— 打开面板不知道电台此刻放到哪首。SSE 已推 `now-playing`/`song-finished`,前端只 `loadPlaylist()`,应加一个醒目的 now-playing 展示(封面+歌名+进度)。
### 中价值
3. **设置页无 UI** —— `/api/system/settings`、`/api/system/config` 已就绪,前端没有对应界面。
4. **导入上传无进度百分比** —— 现只有"上传中..."文字,fetch 不支持上传进度,需换 XHR + `upload.onprogress`。
5. **缓存生成无进度** —— 导入后只能看到"生成中/已缓存"两态,无进度条。
6. **搜索无分页/加载更多** —— `searchNetEase` 固定 limit 20。
### 低价值 / 打磨
7. **mini player 无音量控制**;8. 无播放历史/最热排序(`heat` 字段已有);9. 单弱密码 `test123`(正式化要固定强密码,见 §12);10. 网易云无版权只给文字提示,可做得更友好。
### 已知需实测(无法自动化)
- 浏览器端:SSE 实时刷新、拖拽 reorder、mini player、上传进度、页面切换动画、频道管理模态框交互。
---
## 14. v2.2 前端增强(已完成)
本轮在 v2.1 基础上补齐了用户视角的 4 个高/中价值缺口:
### 14.1 站内 HLS 收听页(`#pageLive`,默认首页)
- 新增底部导航「收听」tab(5→6 个),作为默认 landing 页。
- 引入本地化 `public/vendor/hls.min.js`(hls.js 1.5.17,414KB,全本地化无 CDN)。
- `app.js`:`startLive/stopLive/toggleLive/loadLiveCover`,用 HLS.js 挂载到全局 `#musicPlayer`。
- 修复了「点 liveLink 打开裸 m3u8 无法播放」的根本问题——现在浏览器内可直接收听。
- 卡片展示:封面(取当前播放曲目封面)+ 频道名 + 当前曲目 + 播放按钮 + 状态。
### 14.2 面板「正在播放」实时化
- 发现并修复:后端**从不发 `now-playing` 事件**,前端那个 `es.addEventListener("now-playing")` 是**死监听**,已删。
- 真正信号是 `playlist-update` / `song-finished`;新增 `refreshLive()`(300ms 节流)接到这两个事件 + 轮询。
### 14.3 设置页(`#pageSettings`)
- 缓存管理:已用/上限 + 进度条(绿/黄/红三档)+ 一键清理。
- 运行配置:HTTP 端口/切片时长/窗口/缓存上限/FFmpeg 并发(`/api/system/config`)。
- 运行日志:tail 行数可选(50/100/200/500),暗色终端风格(`/api/system/logs`)。
### 14.4 上传进度百分比
- `fetch` 不支持上传进度,新增 `uploadWithProgress()`(XHR + `upload.onprogress`)替换导入逻辑。
- 上传时实时显示「上传中... N%」。
### 14.5 验证
- 回归:api 16/16、e2e 17/17。
- 新增功能校验:12/12(页面元素 + 3 个接口)。
- HLS 流实测:`/hls/main/index.m3u8` 15 段滑动窗口,切片可达。
### 14.6 待老板浏览器实测
- 收听页点播放是否出声、切频道是否正确切流。
- 设置页日志滚动、缓存条颜色。
- 上传大文件进度条。
---
## 15. v2.2.1 资源排序(已完成)
- 资源页头部新增排序下拉 `#resourceSort`:**加入时间 ↓↑ / 热度 ↓↑ / 名称 ↑↓** 共 6 种。
- 后端 `/api/resources` 补 `createdAt` 字段(取 `songs.created_at`)——之前没返回,时间排序会失效。
- 前端 `sortResources()` 本地排序;名称用 `localeCompare(..., "zh", {numeric:true})` 支持中文与数字。
- 与搜索框组合:排序改数据顺序,搜索只是隐藏不匹配项,二者不冲突。
- 测试脚本改为**动态基线**(不再硬编码 5 项),新增歌曲不会误报。回归 38/38 全绿。
---
## 16. v2.3 UI 去 AI 味重构(已完成 2026-09-24)
> 原「MIUIX 风格重构」任务。老板改方向:**不找 MIUIX**(那是 Compose 的),改用 GitHub `ibelick/ui-skills`(Web 版设计工程 skill)的规则手工重构。
### 16.1 采用的 skill
- **`C:\Users\xiao\.claude\skills\ui-skills`**(clone 自 github.com/ibelick/ui-skills,9046★)。
- 关键规则(`skills/baseline-ui/SKILL.md`):**禁渐变**、禁紫/多彩渐变、禁 glow 当主视觉、单视图单强调色、交互反馈 ≤200ms、入场 ease-out、只动 transform/opacity。
- 我们的项目是原生 HTML/CSS/JS(非 Tailwind/React),只取其**原则**,类名约束不适用。
### 16.2 改动清单
1. **频道管理+切换合并为单一入口**:
- 删除 header 的 `<select id="streamSelect">` 下拉 + 原「频道管理」按钮,合并为单一按钮 `#btnStreamMgr.channel-btn`(图标+当前频道名 `#currentChannelName`),点击打开原有 `#streamModal`(内含切换/删除/新建)。
- `app.js`:`loadStreams()` 不再填 select,改写 `#currentChannelName` 文本;删除 select 的 change 监听;`switchStream()` 补 `if (livePlaying) stopLive()`。
- `style.css`:`.stream-select` 样式 → `.channel-btn`。
2. **去 AI 味(清全部渐变/glow)**:
- `page-bg` 径向光晕 → 纯色 `var(--bg)`;`.bg-shapes` 紫色 `#8b5cf6` → `var(--accent)`。
- `.about-hero` 渐变 → `var(--accent-light)`;`.mp-progress` 渐变 → 半透明纯色。
- `.live-cover-ph` / `.live-play-btn` / `.live-progress-fill` 橙红渐变 `#ff5f6d→#ffc371` → `var(--accent)`;play-btn glow 阴影 → `var(--shadow-sm)`;live-badge glow → 中性。
- `.cache-bar-fill` 三档渐变 → `var(--success/--warning/--danger)`。
- live 区、modal 区硬编码色(#222/#999/#333/#888/#fff/rgba)统一改为 token。
- 保留:login.html 暗色径向(独立深色主题,非 AI 味)。
3. **测试更新**:`test_e2e.py` 断言 `id="streamSelect"` → `id="btnStreamMgr" and id="currentChannelName"`。
### 16.3 验证
- `node --check public/js/app.js` 通过。
- 回归:api 16/16、e2e 17/17、write 5/5(需先 ffmpeg 生成 `tmp/test_import.mp3` 素材)。
- 全局扫描:public/ 下渐变计数 = 0(除 login.html 暗色径向)。
### 16.4 备份
- `F:\music_online\demo_backup_20260924_2129`(本次改动前的备份,仍有效)。
### 16.5 待老板浏览器实测(无法自动化)
- 频道按钮点开弹窗、切频道是否正确换流。
- 收听页配色是否满意(现为单 accent 蓝,无渐变)。
- 各项 hover/动效观感。
### 16.6 未做 / 可选后续
- 是否 `git commit`(工作区改动含本次 + 上轮 v2.2,均未提交)。
- 深色模式、缓存条颜色再打磨、其余「AI 味」细节。
---
## 17. v2.4 UI 规范化 / Token 体系(已完成 2026-09-24)
> 老板反馈「整体 UI 风格很撕裂」→ 全量重构 `style.css` 的 token 与取值体系。
### 17.1 问题诊断
`style.css` 原是两段拼接:**主体系**(1~1730,16 空格缩进、有分节) + **补丁区**(1731+,不缩进、硬编码色的 v2/v2.1/v2.2 追加)。具体失控:
- 字号 **42 种**(0.54~2.4rem 密密麻麻,还混用 `0.82rem`/`.82rem` 写法)
- 圆角 **约 20 种**(3/5/6/7/8/9/10/11/12/14/16/18/20/22/28px,定义了 `--radius-*` 却大半没用)
- 颜色硬编码:55 hex + 38 rgba,兜底色 `#1f6feb` 出现 8 次(带 `var(--accent,#1f6feb)` fallback)
### 17.2 重构内容
**建立完整 token 体系**(`:root` 重写):
- 色板:`--bg/surface/white/accent*/text*/border*/danger*/success*/warning*/overlay/log-*`
- 字号阶梯 9 档:`--fs-2xs/…/--fs-4xl`
- 圆角阶梯:`--radius-xs/sm/base/lg/xl/pill`
- 阴影:`--shadow-xs/sm/base/lg/accent`
- 动效:`--dur-fast/base/slow` + `--ease-out/spring`
- 间距:`--sp-1`~`--sp-6`
**全局收敛**:
- `font-size`:42 种 → **9 种**(67 处替换为 var)
- `border-radius`:20 种 → **8 种**(28 处替换为 var,余 50%/xl 特殊)
- 硬编码色:正文区 **清零**(全转 var)
- accent 阴影重复值收进 `--shadow-accent`;`#fff`→`--white`;`#fdd`→`--danger-hover`
- **补丁区整体并入主体系**:统一 12 空格缩进、改用 `--fs-*`/`--radius-*`/`--sp-*`、重写分节注释
### 17.3 关键取值变化(有意微调,贴近原值)
- `--radius-sm`: 12px→10px,`--radius`: 16px→14px,`--radius-lg`: 24px→20px,`--radius-xl`: 28px 保留
- 补丁区硬编码圆角 14/18/22px → 归入 `--radius/--radius-lg`
- live-cover-ph 大图标 2.4rem → `--fs-4xl`
### 17.4 验证
- 全文括号配平({} 360/360、( ) 581/581),无 var 残渣/嵌套。
- 回归:api 16/16、e2e 17/17、write 5/5 全绿。
- `node --check app.js` 通过。
### 17.5 待老板浏览器实测
- 整体观感是否统一(字号阶梯、圆角、配色)。
- 特别注意:卡片圆角、按钮胶囊、模态框圆角的和谐度。
### 17.6 保留项(非撕裂)
- 毛玻璃 `rgba(255,255,255,…)` 背景、阴影 rgba:正当设计值。
- `login.html` 独立暗色主题(自己的 `:root`)。
- `body` 字体栈、`--log-bg` 终端风格:保留。