删除 MUSIC_MODULE_REVIEW.md
This commit is contained in:
@@ -1,465 +0,0 @@
|
||||
# 音乐模块全链路审查报告
|
||||
|
||||
> 审查范围:数据请求(搜索/歌单解析/下载/解析真实链接)、播放控制(试听、播放器内核、队列、歌词、缓存)、状态管理(musicStore / feiniuStore)、UI 交互与视觉。
|
||||
> 覆盖文件(前端 11 个 + Rust 4 个):
|
||||
> `src/stores/musicStore.ts`、`src/stores/feiniuStore.ts`、`src/modules/music/MusicModule.vue`、`components/PlayerBar.vue`、`components/PlayerFull.vue`、`components/MyMusicLibrary.vue`、`components/TrackItem.vue`、`components/settings/ConnectionsSettings.vue`、`components/settings/CacheSettings.vue`、`SourcePicker.vue`、`sources.ts`、`index.ts`、`src/App.vue`、`src/components/layout/ModuleContainer.vue`、`src-tauri/src/music/{mod,commands}.rs`、`src-tauri/src/music/feiniu/{mod,proxy}.rs`
|
||||
|
||||
---
|
||||
|
||||
## 0. 结论摘要
|
||||
|
||||
整体架构是清晰的:**搜索/下载走 Python 桥接(musicdl)**,**播放走飞牛 NAS 本地流代理(Rust axum,支持 Range 透传)**,两个 store 职责分离,流代理与缓存设计是对的。问题集中在三类:
|
||||
|
||||
| 类别 | 数量 | 代表问题 |
|
||||
|---|---|---|
|
||||
| P0 功能性失效 | 3 | 飞牛曲库搜索框完全无效;重启后播放键无响应;两个音频引擎可同时发声 |
|
||||
| P1 逻辑/状态缺陷 | 12 | shuffle 自动切歌不随机;缓存开关状态不同步;队列被单曲播放重置;任务持久化写入 `rawSearch` 撑爆 localStorage |
|
||||
| P2 性能瓶颈 | 9 | 拖动进度条即发 Range 请求;歌词/队列无虚拟滚动;全屏模糊背景;每行 2 个 Tooltip |
|
||||
| P3 界面与交互缺陷 | 25 | 播放条未贴底且离开模块即消失;左右内边距不对称;三套切换控件;徽标 5 色抢戏;列表变卡片堆 |
|
||||
|
||||
**最需要优先决策的一件事**:把 `PlayerBar` / `PlayerFull` 从 `MusicModule.vue` 提升到 `App.vue` 全局层。当前它们挂载在音乐模块内部,而模块容器是 `:key="activeModule"` 重挂载(`App.vue:36-41`)——**切到任何其他模块,播放条和"正在播放"大屏整体卸载,但音频仍在后台播放,用户完全失去控制入口**。这是当前体验上最严重的问题。
|
||||
|
||||
---
|
||||
|
||||
## 1. P0 功能性失效(必须修)
|
||||
|
||||
### 1.1 飞牛曲库搜索框完全无效
|
||||
**现象**:输入关键词、回车,曲库列表纹丝不动。
|
||||
|
||||
**根因**:`MyMusicLibrary.vue:21` 定义了本地 `keywordInput`,`onSearchInput`(:24-29)只做了防抖后调用 `store.loadTracks(1)`,而 `loadTracks` 读的是 **store 里的 `keyword`**(`feiniuStore.ts:381`)。检查全仓:`store.keyword` **从未被任何 UI 赋值**。
|
||||
|
||||
```ts
|
||||
// feiniuStore.ts:375-382
|
||||
const data = await invoke('feiniu_list_tracks', {
|
||||
page: p, size: 50,
|
||||
keyword: keyword.value.trim() || null // ← keyword 永远是 ''
|
||||
})
|
||||
```
|
||||
|
||||
**修复**:把输入直接接到 store 字段,去掉中间层。
|
||||
|
||||
```vue
|
||||
<!-- MyMusicLibrary.vue -->
|
||||
<Input v-model="store.keyword" placeholder="搜索飞牛曲库(歌名 / 歌手)"
|
||||
@input="onSearchInput" @keydown.enter="onSearchInput" />
|
||||
```
|
||||
```ts
|
||||
// onSearchInput 里补一句:清空关键词时立即回全量
|
||||
function onSearchInput() {
|
||||
clearTimeout(searchTimer)
|
||||
searchTimer = setTimeout(() => store.loadTracks(1).catch(e => toast.error(String(e))), 400)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 1.2 应用重启后,播放键点了没反应
|
||||
**现象**:重启应用,底部播放条显示着上次的歌,点播放键没任何反应,也没有提示。
|
||||
|
||||
**根因**:`restoreQueue()`(`feiniuStore.ts:644-655`)只恢复了 `queue` / `queueIndex`,**没有给 audio 设置 `src`**。此时点击播放走 `toggle()`(:564-568):
|
||||
|
||||
```ts
|
||||
function toggle() {
|
||||
const a = ensureAudio()
|
||||
if (a.paused) a.play().catch(() => {}) // ← 无 src,play() 直接 reject,被静默吞掉
|
||||
else a.pause()
|
||||
}
|
||||
```
|
||||
|
||||
`ensureAudio()` 新建的 `Audio()` 没有 `src`,`play()` 抛 `NotSupportedError`,被空 catch 吞掉。点「上一首/下一首」反而能用(走 `playCurrent()` 会设 src),所以表现为"只有播放键坏了"。
|
||||
|
||||
**修复**:`toggle()` 检测无 src 时先走一次 `playCurrent()`;同时给失败加可见提示。
|
||||
|
||||
```ts
|
||||
function toggle() {
|
||||
const a = ensureAudio()
|
||||
if (!a.src) { // 恢复队列后首次播放
|
||||
if (current.value) playCurrent()
|
||||
return
|
||||
}
|
||||
if (a.paused) a.play().catch(err => { playing.value = false; logger.error(`播放失败: ${err}`) })
|
||||
else a.pause()
|
||||
}
|
||||
```
|
||||
另外建议 `startPlayback` 的 catch 里落一条 `toast.error('播放失败:' + err.message)`,当前 `:558-561` 是先 catch 再静默,用户拿不到任何信息。
|
||||
|
||||
---
|
||||
|
||||
### 1.3 两个独立音频引擎,可同时发声
|
||||
**现象**:在「发现音乐」点某首歌试听,再回到播放器播另一首 → 两首歌同时响;或者试听中点播放器播放,两路声音叠加。
|
||||
|
||||
**根因**:存在两个互不知晓的 `<audio>`:
|
||||
- `MusicModule.vue:1413` —— 试听用的模板 `<audio ref="audioEl">`(`togglePlay` :372-406 驱动)
|
||||
- `feiniuStore.ts:109-126` —— 播放器内核 `new Audio()`
|
||||
|
||||
二者没有互斥,`playingKey` 与 `store.playing` 彼此独立。附带问题:试听音频在**切换 tab / 卸载模块时不会停止**(模板 audio 被移除,引用置空,媒体仍可能继续播到自然结束)。
|
||||
|
||||
**修复(推荐)**:废弃模块内试听 audio,试听直接走同一内核——`playQueue([{...}], 0)`,source 用 `'preview'`,播完即止。若短期不想动内核,至少在 `togglePlay` 之前 `feiniu.clearPlayback()`,并在 `onBeforeUnmount` 里 `audioEl.value?.pause()`。
|
||||
|
||||
---
|
||||
|
||||
## 2. P1 逻辑与状态缺陷
|
||||
|
||||
| # | 问题 | 位置 | 说明与改法 |
|
||||
|---|---|---|---|
|
||||
| 2.1 | **shuffle 自动切歌不随机** | `feiniuStore.ts:570-586` | `next(manual)` 的随机分支带 `manual` 条件,`onEnded → next(false)` 在 shuffle 下走的是顺序 `++`。用户开了随机,一首播完却是下一首。改:把随机逻辑与 `manual` 解耦,`shuffle` 时无论手动/自动都随机(并记录已播集合避免短期重复)。 |
|
||||
| 2.2 | **prev 在 shuffle 下也是顺序** | `feiniuStore.ts:598-607` | 同上;随机模式应回退到历史栈。 |
|
||||
| 2.3 | **切缓存模式/清缓存会中断播放** | `feiniuStore.ts:266-280` | `setCacheMode('stream')` 和 `clearCache()` 都调 `clearPlayback()`;而 `CacheSettings.setEnabled(false)` 又会触发 `setCacheMode('stream')`。改设置就把歌停了,属于误伤。应只在必要(如清掉正在播的那首缓存)时停止,或改为"下一首生效"。 |
|
||||
| 2.4 | **缓存开关与真实模式不同步** | `CacheSettings.vue:13-23,32-35` | `cacheEnabled` 初始恒为 `false`,`onMounted` 只读了 `cacheMode` 没回填开关;若历史已是 cache 模式,打开设置页看到的是"关",但实际在缓存。另外 `cacheMax = ref(5)` 是死代码(从未使用,后端对应字段是 `feiniu_cache_max_gb`),`cacheStatus` 在 onMounted 也没刷新。 |
|
||||
| 2.5 | **单曲播放会重置整个队列** | `feiniuStore.ts:500-511` | `playItem` 未命中队列时 `queue.value = [{...item}]`——用户从 2000 首的曲库点播放,队列被替换成 1 首,播完就停。播放器语义应是"以所在列表为上下文播放"。改:`playItem(item, contextList?)`,命中则跳转,未命中则以 `contextList` 建队列。 |
|
||||
| 2.6 | **歌单内的曲目无法暂停 / 无法整单播放** | `MyMusicLibrary.vue:228-233` | 歌单里的 `TrackItem` 既没有 `@dblclick`,播放按钮又被 `TrackItem.vue:71` 的 `:disabled="store.playing && store.current?.guid === item.guid"` 禁用——正在播这首时按钮变灰,**没有任何方式暂停**。且点击播放会走 2.5 的"单曲替换队列"。 |
|
||||
| 2.7 | **`init()` 被重复调用,会覆盖内存中的队列** | `feiniuStore.ts:129-157` | `MyMusicLibrary.onMounted`(:64-69)与 `CacheSettings.onMounted`(:16-20)都调 `store.init()`;`init` 内含 `restoreQueue()`,会在用户切到设置页时用 localStorage 的旧值覆盖当前队列/索引。应加 `inited` 标志位做幂等,或把 `restoreQueue` 只放在应用启动路径。 |
|
||||
| 2.8 | **任务持久化把 `rawSearch` 一起写进 localStorage** | `musicStore.ts:79-93,228-234,439` | `songsData: songs` 保留了每首歌的 `rawSearch`(上游 API 原始响应)。一个 500 首歌单任务 → 序列化后极易超过 5MB 配额,`persistTasks` 的 catch 会静默失败,历史记录随机丢失。改:持久化前剥离 `rawSearch` / `defaultDownloadHeaders`,只留重下所需字段。 |
|
||||
| 2.9 | **`pendingUploadOnDone` 泄漏,造成误上传** | `MusicModule.vue:60,74-82,483-500` | 该标志在下发下载请求时置 `true`,只有"检测到有任务 done"才清 `false`。若下载报错/被取消、或用户在选择音质弹窗点了取消,标志会残留,之后**任意一个无关任务完成**都会触发一次上传。`autoUpload` 打开时则是每个任务完成都重复全量上传一次。建议改为按 `taskId` 记录"待上传任务集合",完成时精确消费。 |
|
||||
| 2.10 | **Rust 引擎下载后跳到空的任务页** | `MusicModule.vue:460` | `doDownload` 无条件 `activeTab.value = 'tasks'`,但 `downloadEngine === 'rust'` 时任务被交给「下载器」模块(`musicStore.ts:391-430` 不创建本模块任务),本模块任务列表是空的,用户看到"暂无下载任务"。且此时 `downloadTarget === 'feiniu'` 的自动上传链路也永远不会触发。应在 rust 引擎下改为提示"已交给下载器模块"并跳转到对应模块。 |
|
||||
| 2.11 | **`duration` 未知时拖动进度条无效** | `feiniuStore.ts:609-613`(配合 `PlayerBar.vue:18-20`) | `onProgress` 用 `store.duration` 换算秒数;若流没有返回 `Content-Length`,`loadedmetadata` 后 `duration` 为 `NaN`,`a.currentTime = NaN` 被忽略,进度条要么不动要么跳回 0。应在 `seek` 内做 `if (!isFinite(a.duration) || a.duration <= 0) return` 并给出"当前音频不支持跳转"的提示。 |
|
||||
| 2.12 | **`nowPlayingOpen` 状态跨模块残留** | `feiniuStore.ts:105` + `PlayerFull.vue:43` | 大屏开着时切走模块,Dialog 随模块卸载消失,但 `nowPlayingOpen` 仍是 `true`;回到音乐模块会突兀地自动弹出。建议改为由组件本地 `ref` 承载,或在 `onMounted` 时重置。 |
|
||||
|
||||
其它可在后续清理的小项:`doneCount` 在重复 `done` 事件下会虚增(`musicStore.ts:320`);`redownloadTask` 用的是**当前默认音质**而非原任务音质(`musicStore.ts:523`);音质弹窗取消后 `selectedKeys` 已被清空(`MusicModule.vue:505-509`);`installRuntime` 的进度监听清理逻辑正确,但 `installError` 与最后一条进度事件之间缺少"失败即清进度"的处理(注释已提到,代码未做)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 性能瓶颈与优化点
|
||||
|
||||
### 3.1 拖动进度条 = 连续发起 Range 请求(最该改的一项)
|
||||
`PlayerBar.vue:104-110` / `PlayerFull.vue:75-80` 把 Slider 的 `update:model-value` 直接接到 `store.seek()`,而 `seek` 立刻写 `a.currentTime`。飞牛流代理是**真流式透传 Range**(`proxy.rs:131-133`),所以拖动一次会触发几十次"中断旧请求 → 发新 Range 请求":
|
||||
|
||||
- 占用带宽、NAS 侧反复建连
|
||||
- 拖动过程中出现爆音/卡顿
|
||||
- 拖到底后又从新位置重新缓冲
|
||||
|
||||
**推荐**:拖动期间只更新本地"预览位置",`pointerup` / `change` 时提交 `seek`。
|
||||
|
||||
```ts
|
||||
// 父组件维护 dragging + previewValue
|
||||
const dragging = ref(false)
|
||||
const preview = ref(0)
|
||||
function onProgressInput(v?: number[]) { dragging.value = true; preview.value = v?.[0] ?? 0 }
|
||||
function onProgressCommit(v?: number[]) {
|
||||
dragging.value = false
|
||||
store.seek(((v?.[0] ?? 0) / 100) * store.duration)
|
||||
}
|
||||
```
|
||||
```vue
|
||||
<Slider :model-value="[dragging ? preview : store.progress]" @update:model-value="onProgressInput" @value-commit="onProgressCommit" />
|
||||
```
|
||||
另外 `:step="0.5"`(200 档)对 4 分钟歌 ≈ 1.2s 粒度尚可,但对播客/长音频(1 小时以上)单档 ≈ 18s,建议改为按 `duration` 动态计算 step(如 `100 / duration`)。
|
||||
|
||||
### 3.2 进度时间轴以 4Hz 驱动整棵组件树
|
||||
`timeupdate` 约每 250ms 一次写 `position.value`,凡是读它的组件都会重渲染。当前影响面:
|
||||
- `PlayerBar` + `PlayerFull` **同时挂载**(大屏打开时)→ 双份重渲染
|
||||
- `PlayerFull` 的 `currentLine()`(`feiniuStore.ts:710-717`)是**线性扫描**,每次重算 O(歌词行数)。改:缓存上次命中的索引,只在 `position > 下一行时间` 时 `idx++`(歌词已排序,单调推进),或二分查找。
|
||||
- `PlayerFull.vue:33-39` 的 `watchEffect` 每次行切换都 `querySelectorAll('[data-line]')` 全量查询后再 `scrollIntoView({behavior:'smooth'})`。改:用 `ref` 数组或在 v-for 里收集 DOM,仅对目标行滚动;并给滚动加 `prefers-reduced-motion` 降级。
|
||||
|
||||
```ts
|
||||
let lastLineIdx = -1
|
||||
function currentLine(): number {
|
||||
const lines = lyricLines.value
|
||||
if (lastLineIdx >= 0 && lastLineIdx < lines.length
|
||||
&& (lastLineIdx + 1 >= lines.length || position.value < lines[lastLineIdx + 1].t)) {
|
||||
while (lastLineIdx > 0 && position.value < lines[lastLineIdx].t) lastLineIdx--
|
||||
while (lastLineIdx + 1 < lines.length && lines[lastLineIdx + 1].t <= position.value) lastLineIdx++
|
||||
return lastLineIdx
|
||||
}
|
||||
lastLineIdx = -1
|
||||
for (let k = 0; k < lines.length; k++) if (lines[k].t <= position.value) lastLineIdx = k; else break
|
||||
return lastLineIdx
|
||||
}
|
||||
```
|
||||
同时建议歌词/队列上**虚拟滚动**:LRC 常有 80~200 行,长播放列表可上千行,当前是全量 DOM。
|
||||
|
||||
### 3.3 大屏背景的整图高斯模糊
|
||||
`PlayerFull.vue:50-52`:`<img class="h-full w-full scale-110 object-cover blur-2xl opacity-30">` 铺满整个弹窗(`min-h-[70vh]`,实际常超 900×700)。一张全尺寸图叠加 `blur-2xl` + `scale-110`,在打开期间持续占用合成/光栅资源,低端机上会明显掉帧(拖进度条时最明显)。
|
||||
|
||||
**推荐**:改为一次性低分辨率方案——用同图 `size=96` 缩略图 + CSS 渐变遮罩,或直接用取色(后端返回主色)生成柔和纯色/线性渐变背景。收益远大于视觉损失。
|
||||
|
||||
### 3.4 搜索结果行每行 2 个 Tooltip 组件
|
||||
`MusicModule.vue:928-965`:每首歌行包了 2 个 `Tooltip`(reka-ui 组件 + 上下文)。首批 80 行 = 160 个 Tooltip 实例,滚动加载到 500 行 = 1000 个。挂载/更新成本高,且 Tooltip 会随列表 diff 频繁重建。
|
||||
|
||||
**推荐**:列表行用原生 `title` 属性承载提示(无需组件);只对"需要富文本提示"的 `+N` 音质档位保留一个 Tooltip。
|
||||
|
||||
### 3.5 其它
|
||||
- **下载任务聚合计算**:`taskAggDone/taskAggTotal`(`MusicModule.vue:547-548`)每次渲染都遍历全部 `songs`。一个 500 首歌的任务,每来一次进度事件就重算 500×2。改:在 `handleDownloadEvent` 里维护 `task.doneBytes/totalBytes` 增量字段。
|
||||
- **`displayItems` / `groupKeys` / `groupAllSelected`**:模板中对每个源分组头调用 `groupAllSelected`,内部 `filteredResults.filter(...)` 是 O(n);结果集大时是 O(n × 分组数)。改:用一次 `computed` 预生成 `Map<source, keys[]>`。
|
||||
- **飞牛曲库无分页**:`loadTracks` 固定 `size: 50`,`page/total` 暴露了却没有 UI。曲库 > 50 首时后面的歌**完全访问不到**(`playAll` 也只能播前 50 首)。应加无限滚动或分页。
|
||||
- **本地扫描无上限**:`scanLocal` 一次性返回全部条目,超大音乐目录下会长时间占住主线程做映射与渲染。
|
||||
- **缓存模式首播延迟**:`feiniu_cache_fetch`(`commands.rs:421-429` → `cache_fetch`)是"**下载完整文件后返回路径**",即 `cache` 模式下首次播放要等整首下完;而 `loadingPlay` 这个状态在 UI 上**从未被使用**(`PlayerBar`/`PlayerFull` 都没读),用户看到的是"点了没反应"。要么改用边下边播的双源策略,要么至少把 `loadingPlay` 接上 spinner。
|
||||
|
||||
---
|
||||
|
||||
## 4. 交互体验问题(按操作路径)
|
||||
|
||||
**播放控制**
|
||||
1. 播放/暂停按钮 `disabled` 时无视觉降级(`PlayerBar.vue:76-84`),看起来可点却没反应。
|
||||
2. 音量图标(`PlayerBar.vue:117`)不可点击,没有"一键静音";音量条只有 80px,长按/滚轮调节都没有。
|
||||
3. 切歌没有 crossfade,也没有间奏/淡出,切歌瞬间是硬切。
|
||||
4. 播放模式按钮图标随状态变(Repeat/Repeat1/Shuffle),初看像三个不同功能,且没有文字提示(只有 Tooltip)。
|
||||
5. 队列入口用 `ListMusic` 图标 + 数字角标,但点击是**平铺开关**(`store.queueVisible = !store.queueVisible`),而队列实际渲染在 `PlayerFull` 的右栏里——在大屏没打开时点这个图标看不到任何反馈(因为它只切了右栏的 tab,而弹窗没开)。
|
||||
6. 上一首/下一首没有"按住快进/快退"(老播放器的常见预期)。
|
||||
|
||||
**进度条**
|
||||
7. 没有 hover 时间气泡、没有缓冲进度层、没有"拖动时显示预览时间"。当前拖动时只能看两侧的小字。
|
||||
8. 轨道 6px + 16px 白色圆点滑块,`Slider.vue:40` 硬编码 `bg-white`,在浅色主题下白滑块白描边几乎隐形。
|
||||
|
||||
**列表切换**
|
||||
9. 发现音乐页切到「我的音乐」再切回,搜索关键词和结果**全部丢失**(模块内 tab 用 `Tabs` 非 keep-alive,`flatResults` 被重算但 store 里 `results` 还在——实际是因为 `MusicModule` 未卸载所以结果还在,但 `visibleCount` 会被 `watch(filteredResults)` 重置,滚动位置丢失)。
|
||||
10. 列表里双击行不会播放(只有点小图标才播),不符合音乐软件的肌肉记忆。
|
||||
11. 键盘:无空格播放/暂停、无 ↑↓ 选行、无 Enter 播放;所有列表行都不可聚焦。
|
||||
12. 多选操作只有"下载所选",缺"加入歌单""加入队列""下一首播放"。
|
||||
|
||||
---
|
||||
|
||||
## 5. 界面缺陷清单(具体位置 + 改法)
|
||||
|
||||
### 5.1 结构与布局
|
||||
|
||||
| # | 缺陷 | 位置 | 改法 |
|
||||
|---|---|---|---|
|
||||
| L1 | **播放条没贴底**:被模块根 `p-6` 包住,`border-t` 只在左右各缩进 24px 的宽度上出现,视觉上像一张"飘着的卡片"而非底部坞站 | `MusicModule.vue:688,1416` | 把 `PlayerBar` 提升到 `App.vue`,作为 `flex-col` 壳层的最后一行(全宽、`border-t`、`h-[72px]`);音乐模块内容区 `pb-[72px]` 让位 |
|
||||
| L2 | **左右内边距不对称**:模块左 24px + `px-1`(4px) = 28px;右 24px + `pr-3`(12px) + `px-1` = 40px,右侧被滚动条再吃一层,卡片右边距明显窄于左边 | `MusicModule.vue:688,706-707,987-988` | 统一成 `px-6`,滚动区改成 `[&>[data-slot=scroll-area-viewport]]:pr-6` 或让 ScrollArea 用 `scrollbar-gutter: stable`,避免内边距被滚动条挤压 |
|
||||
| L3 | **三套切换控件**:模块级 `TabsList`(半透明覆盖)、`MyMusicLibrary` 自绘分段按钮(:89-114)、`PlayerFull` 的裸文字切换(:113-129)——三种视觉语言表达同一件事 | 三处 | 统一为一种 segmented 控件(建议用现有 `Tabs`/`TabsList`,或统一样式的 `ButtonGroup`),并用同一套"选中=主色底/浅底 + 中粗字"的规则 |
|
||||
| L4 | **`Empty` 组件误用**:直接塞裸 children,继承 `gap-6`(24px) 与 `md:p-12`(48px),图标与文案间距过大且垂直被撑高;`border-dashed` 没有宽度声明,虚线框实际不显示 | `MusicModule.vue:801-809, 977-980`、`MyMusicLibrary.vue:76-83,135-140` | 用组件既定的子结构:`EmptyMedia / EmptyTitle / EmptyDescription`;或给容器加 `border` + `border-dashed`、`gap-3`、`p-8` |
|
||||
| L5 | **横向 Tab 与子 Tab 层级混乱**:顶层 4 个 tab 里,「我的音乐」内部又横排 3 个子 tab,「设置」内部又竖排 5 张卡;导航成本高 | `MusicModule.vue:691-696`、`MyMusicLibrary.vue:88-114` | 见 §6 的"左侧来源树 + 右侧主区"结构,把两级 tab 压成一级 |
|
||||
| L6 | **首屏被低价值信息占据**:「下载到 本地/飞牛」占一整个卡片行(`MusicModule.vue:709-726`),说明文字是 12px 灰字却放在最显眼位置;真正的搜索框被挤到第二行 | `MusicModule.vue:708-794` | 把"下载目标"收进搜索工具栏右侧的 `Select`(与源选择器同排),或放进设置页;首行只留 搜索输入 + 源选择 + 搜索按钮 |
|
||||
| L7 | **歌单详情页底部孤零零一个"移除最后一首"按钮** | `MyMusicLibrary.vue:234-243` | 语义错误,删掉。改为:行 hover 显示「⋯」菜单(从歌单移除 / 上移 / 下移),或支持多选后批量移除 |
|
||||
| L8 | **歌单左栏无滚动**:`grid-cols-[220px_1fr]`,左栏是普通 `div`,歌单多了会撑高整页 | `MyMusicLibrary.vue:189-205` | 左栏用 `ScrollArea` 并固定高度(`h-[calc(100vh-260px)]`),或整体改三栏布局 |
|
||||
| L9 | **设置页信息密度过低 + 语义重复**:「飞牛音乐连接」管连接,「上传到飞牛曲库」又要求用户回头去前一张卡点云朵登录;5 张卡片纵向拉得很长 | `MusicModule.vue:1123-1371` | 合并为「连接与存储」一张卡:连接列表 + NAS 文件服务登录 + 曲库目录 + 自动上传;「播放缓存」并入「播放」分组;「环境检查」做成折叠的"诊断"面板 |
|
||||
|
||||
### 5.2 视觉与色彩
|
||||
|
||||
| # | 缺陷 | 位置 | 改法 |
|
||||
|---|---|---|---|
|
||||
| V1 | **音质徽标 5 色抢戏**:Hi-Res=琥珀、无损=紫、320K=天蓝、192K=橙、128K=锌灰,一行里 2 个高饱和徽标 + 1 个 `+N`,视觉噪音压过歌名 | `MusicModule.vue:336-355, 879-905` | 降为**两档**:无损/高解析用实心浅色徽标(一个色),其余用 `outline` 中性色;颜色不承担"档位"信息,档位靠文字。或改为歌名后的一个小图标 + Tooltip 详情 |
|
||||
| V2 | **列表变成卡片堆**:每行 `border` + `rounded-lg` + `p-2.5` + `space-y-2`,80 行 = 80 张卡,行间 8px 空隙,扫描效率低 | `MusicModule.vue:845-859` | 改紧凑单行:行高 44~48px、无独立边框、`border-b border-border/60` 或干脆无分隔、hover `bg-accent/60`;表头一行(`# / 标题 / 专辑 / 时长 / 操作`) |
|
||||
| V3 | **源分组头与大卡片行不匹配**:分组头只是 12px 小字,紧贴一堆 56px 卡片 | `MusicModule.vue:848-857` | 分组头改 `sticky top-0 z-10 bg-background/95 backdrop-blur` + 底部分隔线,字号 13px medium;或改用左侧来源 Tab 过滤,取消组内嵌头 |
|
||||
| V4 | **当前播放项用"选中态"表达**:`border-primary bg-primary/10` 在浅色主题下是"近黑边框 + 10% 黑底",像复选框选中而非"正在播放" | `TrackItem.vue:53-54` | 改为:左侧 3px 主色竖条 + 歌名主色 + 一个 12px 的跳动均衡器图标;hover 与 active 语义分离 |
|
||||
| V5 | **`PlayerFull` 硬编码白色边框**:`border-l border-white/10`,浅色主题下完全不可见 | `PlayerFull.vue:112` | 改用 `border-border` |
|
||||
| V6 | **`PlayerFull` 背景被"洗白"**:整图 `opacity-30` + 白→白渐变,浅色下糊成一片;同时是持续 GPU 开销 | `PlayerFull.vue:50-53` | 改为缩略图小尺寸模糊,或纯色/双色线性渐变(从封面取色);浅色主题下用极浅的封面色调,深色主题下用暗化封面色 |
|
||||
| V7 | **滑块基本不可见**:`Slider.vue:40` 硬编码 `bg-white`,16px 白色滑块 + 白色描边叠在浅底上;轨道仅 6px | `Slider.vue:36-41` | 滑块改 `bg-background` + 主色描边(或直接用 `bg-primary`);静息轨道 4px、hover/拖动时平滑增到 8px;加 `transition-[height]` |
|
||||
| V8 | **`PlayerFull` 歌词行区分度不足**:非当前行 `text-muted-foreground/60`,当前行仅 `font-medium text-foreground`,没有位置指示 | `PlayerFull.vue:136-145` | 当前行放大到 17~18px + `text-foreground` + 左侧或下方主色指示;两侧行逐级降透明度(60/40/25)形成景深;点击区域扩大到整行并加 hover 背景 |
|
||||
| V9 | **`PlayerFull` 进度时间只在下方一行小字**,且控制按钮组与进度条分属两个块,视觉不聚合 | `PlayerFull.vue:74-108` | 进度条与控制键合并为一块:`[时间] ──●──── [时间]` 上、`[模式][上一首][播放][下一首][队列]` 下的居中控制簇 |
|
||||
| V10 | **「词」按钮用单个汉字**当歌词开关 | `PlayerBar.vue:125-132` | 换成 `Lyrics` 图标(`FileText`/`MicVocal`),与两侧图标风格一致 |
|
||||
| V11 | **下载任务标题行过载**:状态标签 + 歌名 + 歌手 + 音质标签 + 已下载/总量 + 最多 4 个图标按钮挤在一行;且 `Card` 被 `gap-0 py-0` + `CardContent py-2` 覆盖,内边距与模块内其它卡片不一致 | `MusicModule.vue:995-1060` | 拆两行:第一行 `[状态] 歌名 · 歌手 ……… [音质] [⋯菜单]`;第二行进度条 + 大小 + "打开目录/取消/重下"收进 `⋯` 菜单。`Card` 统一 `p-0` + 内部 `p-3` |
|
||||
| V12 | **多曲目任务行进度条只有 56px**(`w-14`),几乎读不出进度;错误信息被截断到 112px | `MusicModule.vue:1086-1097` | 进度条改占满剩余宽度(去掉固定宽度,放歌名下方一行);错误信息用 Tooltip 或点击展开 |
|
||||
| V13 | **`TrackItem` 悬停才出现的播放按钮占 40px 固定宽度**,未悬停时标题左侧留出空白列,触屏/键盘用户无法触发 | `TrackItem.vue:68-75` | 改为:封面左下角 24px 半透明播放浮层(hover 显现),或整行 hover 时标题左侧淡入小图标;并支持双击整行播放 |
|
||||
| V14 | **飞牛曲库行/本地曲库行/歌单行的"来源"标识不统一**:本地用行内小标签「本地」,飞牛没有标识,歌单混排时无法区分 | `TrackItem.vue:81` | 统一在行尾或封面角标显示来源(NAS 云图标 / 本机图标),并在歌单详情里按来源分组或加筛选 |
|
||||
| V15 | **搜索历史下拉不适合键盘操作**:`@focusout` 关闭 + `@mousedown.prevent` 的组合同样让 Tab 导航无法进入历史项 | `MusicModule.vue:738-760` | 改用 `Combobox`/`Popover` + `Command` 组件(仓库已有 `components/ui/combobox`),支持 ↑↓ 选择、Enter 确认、Esc 关闭 |
|
||||
| V16 | **`+N` 音质档位靠 Tooltip**,触屏不可达;`visibleTiers` 固定展示 2 个,长歌名下把歌名挤窄 | `MusicModule.vue:888-904` | 行内只留 1 个"最高音质"徽标,全部档位收进右侧 `⋯` 菜单或点击弹出 popover |
|
||||
| V17 | **`PlayerBar` 缺少播放态反馈**:`loadingPlay` 未接 UI;NAS 慢时点播放毫无反应 | `PlayerBar.vue:76-84` | 播放键上叠加 spinner(`loadingPlay` 时),或封面加旋转/脉冲 |
|
||||
| V18 | **`PlayerBar` 三段留白失衡**:左侧信息 `max-w-44`,中间控制区 `flex-1` 里塞了模式/上下首/播放/队列 + 进度条,右侧音量+词+最大化;窄窗口下中间进度条被压到很短 | `PlayerBar.vue:34-141` | 采用标准三段:左 `w-[240px]` 固定、中 `flex-1 min-w-0`(控制 + 进度)、右 `w-[200px]`(音量/词/大屏);窄容器下用 `container query` 渐进隐藏音量条与歌词按钮 |
|
||||
| V19 | **`TabsList` 同时带 `grid w-full max-w-md` 与组件默认 `inline-flex w-fit`**,并靠 `!p-0 !bg-transparent !shadow-none` 强行覆盖 | `MusicModule.vue:691` | 不要用覆盖式改写;把"模块级分段导航"抽成一个 `SegmentedNav` 组件(`grid grid-cols-N`、`bg-muted p-0.5`、激活项 `bg-background shadow-sm`) |
|
||||
| V20 | **封面圆角/阴影规格不统一**:列表 40px `rounded-md`、播放条 44px `rounded-md shadow`、大屏 256px `rounded-xl shadow-2xl` | 多处 | 定规格:列表 6px 无阴影、播放条 6px 无阴影、大屏 12px + `ring-1 ring-border/60` 无重阴影 |
|
||||
| V21 | **下载任务空态文案指向错误**:「去"搜索"页选择歌曲下载」,但 tab 已改名为「发现音乐」 | `MusicModule.vue:991` | 改为"去「发现音乐」页选择歌曲下载" |
|
||||
| V22 | **`PlayerFull` 未提供音量、队列收起、下一首信息**;队列项没有播放态指示(只有 `bg-primary/10`) | `PlayerFull.vue:147-163` | 补:右上角音量滑条;队列项用序号↔均衡器切换表示当前项,hover 显示"下一首播放/移除" |
|
||||
| V23 | **`ConnectionsSettings` 图标按钮语义不明**:`Power` 既表示"激活"又表示"登出",两个含义相同的图标相邻 | `ConnectionsSettings.vue:157-171` | 激活用 `CircleCheck`/`RadioButton`,登出用 `LogOut`,删除保留 `Trash2`;并为所有 icon-only 按钮补 `Tooltip` + `aria-label` |
|
||||
| V24 | **歌单/曲库的「播放全部」用文字 `▶ 播放全部`**,与其他按钮的图标风格不统一 | `MyMusicLibrary.vue:115-117,214-216` | 统一为图标 + 文字:`<Play class="size-4"/> 播放全部` |
|
||||
| V25 | **`MusicModule` 里两处搜索框文案不一致**("输入歌名/歌手/专辑关键词" vs "粘贴歌单链接")且歌单解析行的 Input 靠 `border-0 bg-transparent` 融进容器,缺少"这是一行输入"的暗示 | `MusicModule.vue:730-782` | 歌单解析改为折叠的"粘贴歌单链接"次级入口(默认收起),避免与主搜索竞争注意力 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 重设计方案
|
||||
|
||||
### 6.1 参考基准
|
||||
- **Spotify / Apple Music**:左侧来源树 + 中间列表 + 底部全宽播放条 + 右侧可折叠"正在播放"。
|
||||
- **网易云音乐 / QQ 音乐**:紧凑表格行(`# / 标题 / 歌手 / 专辑 / 时长`)、行内 hover 操作、双击播放、底部播放条 + 播放列表面板。
|
||||
- **foobar2000 / MusicBee**:信息密度高、可排序表头、键盘优先。
|
||||
本项目的定位是"NAS 曲库 + 本地曲库 + 多平台搜索下载"的**工具型播放器**,建议取 **Spotify 的骨架 + 网易云的列表密度**,避免做成消费级流媒体的强视觉风格。
|
||||
|
||||
### 6.2 目标信息架构(三级 tab → 一级导航)
|
||||
|
||||
```
|
||||
音乐
|
||||
├── 曲库
|
||||
│ ├── 飞牛曲库 (NAS 曲目,分页/无限滚动 + 搜索 + 排序)
|
||||
│ ├── 本地曲库 (扫描目录,按文件夹浏览)
|
||||
│ └── 我的歌单 (歌单列表 → 歌单详情,可拖拽排序)
|
||||
├── 发现音乐 (多平台搜索 + 歌单解析 + 下载)
|
||||
├── 下载任务 (进行中 / 已完成 分组)
|
||||
└── 设置 (连接与存储 / 播放 / 下载 / 诊断,用左侧锚点或分段)
|
||||
```
|
||||
把「我的音乐」内部的 3 个子 tab 提升为导航的一级子项(来源树),「设置」内部的 5 张卡片改为分组锚点。
|
||||
|
||||
### 6.3 布局结构(目标)
|
||||
|
||||
```
|
||||
┌───────────────────────────────────────────────────────────────────────┐
|
||||
│ TitleBar(含模块浮动切换器) │
|
||||
├──────────┬──────────────┬─────────────────────────────────────────────┤
|
||||
│ Sidebar │ 来源树 │ 内容主区 │
|
||||
│ (模块) │ 240px │ ┌ 固定工具栏 ────────────────────────────┐ │
|
||||
│ │ │ │ 搜索 来源筛 音质筛 排序 多选操作 │ │
|
||||
│ │ 曲库 │ ├ 表头 (# 标题 歌手 专辑 时长 操作) ──────┤ │
|
||||
│ │ · 飞牛曲库 │ │ 44px 紧凑行,hover 显示操作 │ │
|
||||
│ │ · 本地曲库 │ │ 播放中:主色竖条 + 跳动的均衡器 │ │
|
||||
│ │ · 我的歌单 │ │ ... │ │
|
||||
│ │ 发现音乐 │ └────────────────────────────────────────┘ │
|
||||
│ │ 下载任务 │ │
|
||||
│ │ 设置 │ (可选右栏 320px:正在播放/歌词/队列) │
|
||||
├──────────┴──────────────┴─────────────────────────────────────────────┤
|
||||
│ MiniPlayer 72px(全宽坞站:左曲目 中控制+进度 右音量/歌词/大屏) │
|
||||
└───────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
要点:
|
||||
1. **MiniPlayer 提升到 `App.vue`**,全宽贴底、不参与任何模块内边距;音乐模块内容区 `pb-[72px]`。这样在任何模块都能控制播放。
|
||||
2. **`PlayerFull` 也提升到 `App.vue`**(全局 Dialog/Overlay),解决跨模块残留问题。
|
||||
3. **去掉"模块级横向 Tab"**,音乐模块内部用来源树;`useModuleTabs` 仍可保留,用于把来源树的第一级注册给 TitleBar 浮动切换器(保持现有能力)。
|
||||
4. **右栏"正在播放"可折叠**:窄窗口默认收起,宽窗口(≥1280px)默认展开,用 `container query` 或 `useMediaQuery` 决定。
|
||||
|
||||
### 6.4 视觉规范(建议写入项目 Design Token)
|
||||
|
||||
| 项目 | 规范 |
|
||||
|---|---|
|
||||
| 主色使用 | 只用于:主操作按钮、播放中指示、"正在播放"高亮。**不用于**徽标分类、分组头 |
|
||||
| 中性色阶 | 文本 `foreground` / 次文本 `muted-foreground` / 三级文本 `muted-foreground/70`;分隔线 `border/60`;hover 面 `accent/60` |
|
||||
| 圆角 | 行/输入 6px(`radius-md`)、卡片 10px(`radius-lg`)、大图 12px、头像/封面圆形仅用于单曲大图 |
|
||||
| 阴影 | 列表与播放条**不用阴影**,靠 `border` 分层;只有 Dialog/Popover 用 `shadow-lg` |
|
||||
| 行高 | 列表行 44px(紧凑)/ 48px(标准);表头 32px |
|
||||
| 字号 | 歌名 14px/500,歌手·专辑 12px/400,时长时间数字 12px `tabular-nums` |
|
||||
| 徽标 | 一类语义一个样式:状态用实心浅底,属性(音质/格式)用 `outline` 中性色 + 文字;不超过 2 个 |
|
||||
| 动效 | 仅 `opacity` / `transform`,120~180ms `ease-out`;进度条高度过渡 150ms;尊重 `prefers-reduced-motion` |
|
||||
| 明暗主题 | **禁止硬编码** `bg-white` / `border-white/10` / `text-white`;一律走 token |
|
||||
| 空状态 | 统一 `EmptyMedia(32px 图标) → EmptyTitle(14px) → EmptyDescription(12px)`,`gap-3`、`p-10`、无边框 |
|
||||
|
||||
### 6.5 关键组件规格
|
||||
|
||||
**MiniPlayer**
|
||||
```
|
||||
高度 72px | 全宽 | border-t | bg-background/85 backdrop-blur
|
||||
左 240px :封面 48px(6px 圆角) + 歌名 14px/500 截断 + 歌手 12px 截断 + ♥
|
||||
中 flex-1:① [模式][上一首][播放 40px 圆形主色][下一首][队列] 居中
|
||||
② [时间 00:00] ──进度条(hover 4→8px,含缓冲层)── [总时长] 居中 max-w-[560px]
|
||||
右 200px :音量图标(可点静音) + 音量条 96px | 歌词 | 大屏 | (窄屏只留大屏)
|
||||
播放中:封面右侧叠加 3 条均衡器动效;`loadingPlay` 时播放键显示 spinner
|
||||
```
|
||||
|
||||
**列表行(TrackItem 重写)**
|
||||
```
|
||||
[#/均衡器 24px] [封面 40px] [标题 14px/500 + 歌手 12px] [专辑 12px 可隐藏] [时长 12px tabular] [⋯ hover 显示]
|
||||
高度 44px | 无边框 | border-b border-border/50 | hover:bg-accent/60 | 当前项:左侧 3px 主色条 + 标题主色
|
||||
双击整行播放;⋯ 菜单:下一首播放 / 加入队列 / 加入歌单 / 查看文件位置 / 下载
|
||||
```
|
||||
|
||||
**进度条交互**
|
||||
```
|
||||
拖动中:只更新预览位置 + 显示时间气泡;轨道加粗;不写 currentTime
|
||||
释放时:一次性 seek;若 duration 未知则禁用拖动并提示"该音频不支持跳转"
|
||||
缓冲层:在轨道里用 bg-muted-foreground/25 画已缓冲区间(audio.buffered)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 落地路径(按性价比排序)
|
||||
|
||||
| 阶段 | 内容 | 影响面 | 预估 |
|
||||
|---|---|---|---|
|
||||
| **S1 修 Bug(不做设计变更)** | 1.1 搜索框绑定 store.keyword;1.2 `toggle()` 无 src 兜底;1.3 试听与播放器互斥(或合并内核);2.1/2.2 shuffle 自动切歌;2.4 缓存开关回填;2.7 `init` 幂等;2.8 持久化剥离 `rawSearch` | 仅改逻辑,风险低 | 半天 |
|
||||
| **S2 播放器骨架调整** | 3.1 拖动提交式 seek;3.2 歌词行索引缓存;3.3 大屏背景降级;4.1 播放键 loading 态;L1/V18 MiniPlayer 全宽贴底;V5/V7 主题 token 修正 | 播放条 + 大屏 | 1~2 天 |
|
||||
| **S3 列表与信息架构** | V2 紧凑列表;V1 徽标降噪;V3 分组头;V13/V4 TrackItem 重写;L6 首屏工具栏重构;2.5 上下文队列;2.6 歌单播放/暂停 | 发现音乐 + 曲库 | 3~4 天 |
|
||||
| **S4 结构性重构** | §6.2/6.3 左侧来源树 + 全局播放条;PlayerBar/PlayerFull 提升到 App.vue;右栏正在播放;设置页分组 | 模块外壳 | 1 周 |
|
||||
| **S5 体验补全** | 键盘快捷键、虚拟滚动、分页/无限滚动、拖拽排序歌单、音质档位 popover、缓冲层、桌面歌词 | — | 按需 |
|
||||
|
||||
**建议先做 S1 + S2**:S1 全是低风险的逻辑修复,S2 集中在两个组件文件里,能立刻解决"卡顿 + 没有控制感 + 拖动难用"的主观感受;S4 的结构调整等到前面稳定后再动,避免一次改动面过大。
|
||||
|
||||
---
|
||||
|
||||
## 8. 附:值得保留的设计
|
||||
|
||||
- 流代理走 `127.0.0.1` 动态端口 + `Cookie` 注入 + **Range 透传**(`proxy.rs`)——这是正确做法,seek 能真正生效,不要改成前端直连。
|
||||
- 桥接进程按需拉起、禁用模块时停止(`music/index.ts:34-45`)——避免了常驻 Python 进程。
|
||||
- `tasksSignature` 轻量签名代替深度 watch(`musicStore.ts:257-266`)——思路正确(只是 payload 需要瘦身)。
|
||||
- 搜索结果**懒解析**(搜索阶段拿档位、播放/下载前才解析真实链接)+ `rawSearch` 支持隔夜链接重解析(`musicStore.ts:355-370,458`)——设计合理。
|
||||
- 发现音乐列表的**增量渲染 + IntersectionObserver 哨兵**(`MusicModule.vue:265-305`)——方向正确,可平滑迁移为新列表的基础。
|
||||
|
||||
---
|
||||
|
||||
## 9. 重构落地记录(本次已实施)
|
||||
|
||||
### 9.1 新增文件
|
||||
|
||||
| 文件 | 作用 |
|
||||
|---|---|
|
||||
| `src/components/layout/TitleBarMusic.vue` | 标题栏音乐栏:唱片图标 + 歌名,点击弹出**方形播放控制窗** |
|
||||
| `src/modules/music/components/PlaylistEditorDialog.vue` | 歌单音乐编辑器:从全部音乐勾选加入 / 批量移出 |
|
||||
| `src/components/layout/NowPlayingDialog.vue` | 「正在播放」大屏(封面 + 控制 + 歌词 / 队列),由方形控制窗的「歌词 / 队列」入口打开 |
|
||||
| `src/components/common/ScrubBar.vue` | 通用拖拽进度条:**拖动只预览、松手才 seek**,含缓冲层与时间气泡 |
|
||||
| `src/components/common/SegmentedNav.vue` | 分段导航(现由 `NowPlayingDialog` 的「歌词 / 队列」切换使用) |
|
||||
|
||||
`PlayerBar.vue` / `PlayerFull.vue` 已删除;`GlobalPlayerBar.vue` 曾短暂作为全局坞站播放条存在,现已被标题栏音乐栏取代并删除。
|
||||
|
||||
**播放器入口最终形态**:标题栏搜索框左侧的音乐栏(`TitleBarMusic`)——未播放过时只显示唱片图标;有曲目后图标替换为封面并随播放旋转,右侧显示截断歌名。点击弹出约 300×340 的方形控制窗(封面 / 曲目信息 / 进度 / 循环·上下首·播放·队列 / 音量 / 歌词·队列入口),点击封面或「歌词 / 队列」进入大屏。
|
||||
|
||||
### 9.2 修复的缺陷
|
||||
|
||||
**P0**
|
||||
1. 飞牛曲库搜索框:直接 `v-model="store.searchKeyword"` 并新增 `loadTracks(page, append)` + `loadMoreTracks()`,搜索真正生效且支持分页续拉。
|
||||
2. 重启后播放键失效:`toggle()` 在 `audio` 无 src 时先按当前条目装载(试听或队列当前曲),失败原因经 `playError` 冒泡成 toast。
|
||||
3. 双音频引擎:删除模块内试听 `<audio>`,试听改走 `feiniu.playPreview()`,与队列播放**共用同一个 audio 实例**,天然互斥。
|
||||
|
||||
**P1**
|
||||
4. shuffle 自动切歌:`next()` 不再区分手动/自动,随机模式下都从「待播池」抽取;新增切歌历史栈,`prev()` 在随机模式下真正回退。播放模式持久化。
|
||||
5. 缓存模式:`setCacheMode` / `clearCache` 不再调用 `clearPlayback`,改设置不中断播放。
|
||||
6. 缓存开关状态:`CacheSettings` 的开关改为 `cacheMode` 的双向 computed,删除死代码 `cacheMax`,补 `ensureCacheStatus` 刷新。
|
||||
7. `init()` 幂等化(`inited` 标志),不会再覆盖内存队列。
|
||||
8. 任务持久化:超过 1.5MB 时自动剥离 `rawSearch` / `defaultDownloadHeaders` 后重试。
|
||||
9. 上传触发:`pendingUploadOnDone` 布尔标志改为 `uploadOnDoneTaskIds` 任务集合 + `uploadedTaskIds` 去重表,每个任务最多触发一次;启动时的历史任务不触发重传。
|
||||
10. Rust 引擎下载:`startDownload` 返回 `{ engine, skipped, taskId }`;Rust 引擎不再跳到空的任务页,改为提示到「下载器」查看;「下载到飞牛 + Rust 引擎」组合直接给出可读的错误提示并阻止。
|
||||
11. 队列语义:`playItem(item, context?)` 支持以所在列表为播放上下文,解决「从大曲库点一首就只剩一首」;新增 `playNext` / `addToQueue` / `removeFromQueue`。
|
||||
12. `done` 事件幂等,`doneCount` 不再虚增;`redownloadTask` 沿用原任务音质。
|
||||
|
||||
**性能**
|
||||
13. 提交式 seek(`ScrubBar`),拖动不再触发几十次 Range 请求中断重建。
|
||||
14. 歌词行索引由 O(n) 全量扫描改为「命中缓存微调 + 未命中二分」,并缓存上次索引。
|
||||
15. 大屏背景从「整图 `blur-2xl scale-110`」改为主题色径向渐变,去掉持续 GPU 开销。
|
||||
16. 列表行去掉 reka Tooltip(改用原生 `title` + 单个 Popover 展示音质档位),500 行不再产生 1000 个组件实例。
|
||||
17. 歌词滚动用函数 ref 收集节点,不再每次 `querySelectorAll`。
|
||||
18. 播放队列增量渲染(每批 200 行)。
|
||||
19. 曲库分页 200/页 + 滚动续拉;任务明细进度条由固定 56px 改为占满剩余宽度。
|
||||
|
||||
**交互与视觉**
|
||||
20. 播放入口改到**标题栏**:移除模块内播放条与全局坞站播放条,在自定义标题栏搜索框左侧新增音乐栏(唱片图标 + 歌名,未播放过时隐藏歌名);点击弹出方形控制窗;播放中图标替换为封面并旋转。
|
||||
21. 音乐模块顶部 tabs 改回与其余模块完全一致的写法:`<Tabs><div ref="tabsListRef"><TabsList class="grid w-full max-w-md grid-cols-4 !bg-transparent !p-0 !shadow-none">` + `TabsTrigger` + `TabsContent class="mt-4 min-h-0 flex-1 ... tab-animate"`,根节点恢复为 `h-full p-6 overflow-hidden flex flex-col`。
|
||||
22. 三级 tab 压成「模块 tab → 曲库内左侧来源树」两级。左栏由 `w-[210px]` 收窄到 `w-[168px]`,来源项的选中态对齐顶部 Tabs(`rounded-lg bg-muted` 容器 + `bg-background shadow-sm` 胶囊),歌单子树用 `border-l` 缩进 + 展开动画;右栏内容切换加 `tab-animate`(`:key="view"` 重挂载触发),不再生硬。
|
||||
23. 首屏工具栏:搜索框回到首行,下载目标 / 来源筛选 / 音质筛选收到筛选条,歌单解析收进 Popover。
|
||||
24. 列表改 44px 紧凑行:无独立边框、hover 高亮、双击整行播放、序号列悬停原位切换为播放按钮(不再出现 40px 空白列);播放中行用 `music-eq` 跳动均衡器 + 主色文字。
|
||||
25. 音质徽标由 5 色降为单枚中性 outline 徽标 + Popover 详情。
|
||||
26. `Empty` 全部改为组件既定结构(`EmptyMedia` / `EmptyTitle` / `EmptyDescription`)。
|
||||
27. 歌单:删除语义奇怪的「移除最后一首」,改为行内「⋯」菜单的支持上移 / 下移 / 移除;新增重命名;左侧歌单列表可独立滚动。
|
||||
28. **歌单页新增「编辑音乐」**(`components/PlaylistEditorDialog.vue`):双 Tab 编辑器——「全部音乐」按来源(全部 / 飞牛 / 本地)勾选加入,已在歌单的曲目置灰标记「已加入」不可重复勾选;「已在歌单」批量勾选移出。含搜索、全选当前列表、增量渲染(每批 120)、飞牛曲库续拉、本地未扫描时的内联扫描入口。
|
||||
29. 源分组头去掉 `sticky` 与白色底色(原 `bg-background/95 backdrop-blur` 会在浅色背景上形成白色横条)。
|
||||
30. **发现音乐每行新增「下载到飞牛」**(试听 / 下载右侧):走新增的 Rust 命令 `feiniu_upload_from_url`,由后端边拉音源边传给 NAS,**本地不落地文件**;懒解析歌曲会先按默认音质解析真实链接,并合并 `defaultDownloadHeaders` + `defaultDownloadCookies`(过滤非 ASCII 头,避免 reqwest panic)。
|
||||
31. 设置页从「5 张卡片」改为「连接 / 存储与上传 / 播放 / 搜索与下载 / 环境与诊断(折叠)」分组,并把「NAS 文件服务登录」并入存储分组。
|
||||
32. 主题一致性:删除 `border-white/10`、`bg-white` 等硬编码;滑块、均衡器、进度条全部走 `--primary` / `--muted` / `--background` token。
|
||||
33. 连接管理图标语义修正(激活用 `CircleCheck`、登出用 `LogOut`),全部 icon-only 按钮补齐 `title` + `aria-label`。
|
||||
34. 搜索历史下拉改为 `focusin/focusout` 容器判定,Tab 键可以进入历史项。
|
||||
|
||||
### 9.2.1 飞牛上传链路:fnOS 文件服务 → WebDAV(最终形态)
|
||||
|
||||
> 演进:multipart over WS 的 fnOS 文件服务存在会话易失、握手/登录协议联调困难等问题(三轮修复:
|
||||
> WS 读写分离与 Origin → 加密登录体补 `req`/`reqid` → 登录结果双通道等待),最终整体替换为 **WebDAV**。
|
||||
|
||||
| 结论 | 说明 |
|
||||
|---|---|
|
||||
| **协议选型** | fnOS 提供的 SMB / WebDAV / FTP / NFS 中选 **WebDAV**:纯 HTTP 语义、reqwest 即可实现、Basic 认证随请求携带**无会话**(重启免登录);SMB 的 Rust 客户端生态差、FTP 明文且被动模式易出问题、NFS 无 Windows 客户端方案 |
|
||||
| 后端实现 | `src-tauri/src/music/feiniu/webdav.rs`(`fnos.rs` 已删除):`test`(PROPFIND Depth 0)/ `upload_file`(本地文件 128KB 分块流式 PUT)/ `delete`;上传前逐级 MKCOL 补目录 |
|
||||
| 「下载到飞牛」机制 | **与自动上传同链路**:正常走 musicdl 下载(引擎侧解析/代理/音质)→ 任务完成后扫描下载目录(下界=任务创建时刻)→ WebDAV 上传 → 删除本地音频及同名歌词/封面(`feiniu_delete_local`,扩展名白名单)→ 移除任务记录。失败时保留本地文件与任务记录供排查。早期的「URL 拉流直传」(`webdav_upload_from_url`)因可靠性不足已移除 |
|
||||
| 前端配置 | `feiniuStore.webdav = { url, username, password, dir }` 持久化 localStorage(`thing.music.feiniu.webdav`);`webdavReady` 四项齐备才放行上传;设置页「存储与上传」分组内配置 + 「测试连接」 |
|
||||
| fnOS 侧设置 | 系统设置 → 文件服务 → WebDAV 开启(HTTP 5005 / HTTPS 5006);目标文件夹在「可见文件夹范围」内;团队文件夹需勾选「允许通过文件共享协议挂载」;账号需读写权限(已写成设置页折叠提示) |
|
||||
| 路径语义 | 目标目录是 **WebDAV 根下的相对路径**(不是 NAS 内部绝对路径),URL 逐段百分号编码 |
|
||||
| 错误可读化 | 401 认证失败 / 403 无权限 / 404 目录不存在或不在可见范围 / 409 父目录不存在,均带上下文 |
|
||||
|
||||
### 9.3 校验
|
||||
|
||||
```
|
||||
vue-tsc --noEmit → exit 0(0 error)
|
||||
vite build → exit 0(11.5s)
|
||||
cargo check → exit 0
|
||||
```
|
||||
|
||||
### 9.4 仍未做的部分(建议后续)
|
||||
|
||||
- 键盘全局快捷键(空格播放/暂停、↑↓ 选行)——需接入项目既有快捷键体系,避免与输入框冲突。
|
||||
- 列表虚拟滚动(当前为增量渲染,千级以上仍有优化空间)。
|
||||
- 歌单拖拽排序(`vue-draggable-plus` 已在依赖中,可直接接入)。
|
||||
- 本地文件的时长 / 内嵌封面解析(当前时长列退化为文件大小)。
|
||||
- 缓存模式改为「边下边播」的双源策略(当前 `cache` 模式首播仍需等整首下完)。
|
||||
|
||||
Reference in New Issue
Block a user