Files
Thing/.workbuddy/memory/2026-09-12.md
2026-09-12 15:47:16 +08:00

27 KiB
Raw Permalink Blame History

2026-09-12

设置页图标修复 + WebDAV 凭据加密存储 / 配置后隐藏

  • 图标不显示根因:设置页模板用了 HardDriveDownload 但从未 import——vue-tsc 不报错(未在 script 中引用、模板解析为未知组件静默失败),运行时图标直接消失。以后设置页新增图标必须同步加到 lucide import 列表。
  • WebDAV 凭据加密:新增 Rust 命令 webdav_get_secret / webdav_save_secretcommands.rs,非 specta,走 raw invoke;已注册 mod.rs 导出 + lib.rs generate_handler)。Windows 用 keyring v3windows-native,凭据管理器,DPAPI 保护)service="Thing" user="webdav-credentials"blob 为 {"username","password"} JSONNoEntry 视为未配置。
  • feiniuStoreWEBDAV_KEY localStorage 现在只存 {url, dir}patchWebdav 检测 username/password 字段时调 webdav_save_secretinit() 从凭据管理器加载凭据,并做一次性迁移(localStorage 有旧明文凭据 → 写入凭据管理器 + 立即改写 LS 去掉明文;用 hasStoredSecret 标志避免误覆盖)。davConfig() 仍从内存取明文传给 webdav_* 命令,链路不变。
  • 设置页 UIwebdavEditing ref 控制——webdavReady && !webdavEditing 时显示摘要卡(服务地址/目标目录 + 「账号/密码已保存(系统凭据管理器加密存储)」),带「修改配置」「测试连接」;编辑态完整表单(密码框 type=password 本身掩码)+「收起」。
  • 校验:cargo check(新增 keyring 3.6.3/ vue-tsc / vite build 全 0。

音乐模块全链路检查 + 排序/设置页/任务页调整

  • 模块排序music/index.ts order 55→15(代理=10 之后、整体第二);appStore.ts loadSettings 增加一次性迁移(musicOrderMigrated 标记随设置持久化,声明在 store setup 内的非响应式 let),已保存过 moduleOrder 的用户下次启动自动把 music 移到 proxy 后,之后尊重手动拖动。
  • 隐藏 bug 修复(MusicModule.vue
    1. downloadToFeiniu 早退分支(Rust 引擎/无下载源)未清理 feiniuUploadingKeys → 行按钮 spinner 永久卡死;
    2. resolvePendingUploads 只处理 done,临时「下载到飞牛」任务 error/cancelled/interrupted 或被手动移除时 spinner 同样卡死 → 增加失败/移除清理分支(注意:临时任务的 tempTaskIds.add 发生在任务进入 tasks 之后,watch 触发时序依赖后续状态变化再触发一次);
    3. feiniu.uploading 时不再叠加触发 uploadRecentToFeiniu
  • 下载任务 tabs:去掉标题上的进行中计数(连同 activeTaskCount computednoUnusedLocals 会拦未用变量);任务卡片容器 py-4pb-4,贴近 tabs。
  • 设置页 UI 重做section+Separator 改为 Card/CardHeader/CardTitle/CardContent 卡片式(max-w-2xl、gap-4、图标+text-base 标题,与 DownloaderModule 设置页一致);「播放条常驻在所有模块底部」等过时文案改为「标题栏播放控制」。
  • 全链路审查结论:旧审查的 P0(曲库搜索失效/重启后播放键失效/双音频引擎)均已在现码修复;musicdl 事件幂等、任务持久化签名 watch、歌词二分+邻近缓存等设计良好。
  • 校验:vue-tsc --noEmit 0 错误,vite build 通过。

「下载到飞牛」改为同链路落地即传(用户实测 URL 直传失败)

用户确认:WebDAV 测试连接 OK、下载+自动上传 OK,但发现音乐行的「下载并上传」失败(旧的 URL 拉流直传机制)。按要求改为与「下载完成后自动上传」完全同链路:

  • downloadToFeiniu:直接 store.startDownload([song])(musicdl 引擎,含引擎侧解析/代理/音质),不再手动 resolve + 拼 headers;记 tempTaskStartstaskId→创建时刻,扫描下界)。
  • 任务 done → finishTempUploaduploadRecentToFeiniu(0, { since, deleteLocal: true }) 上传 → feiniu_delete_local 删本地音频及同名 lrc/封面 → store.removeTask 移除记录。失败时保留本地文件与任务记录供排查重试。
  • uploadRecentToFeiniu 扩展 opts { since?, deleteLocal? };默认行为(自动上传)不变、不删本地。
  • 后端新增 feiniu_delete_local(音频/歌词/封面扩展名白名单 + 拒绝目录);删除 webdav_upload_from_url 命令与 webdav.rs upload_from_url(机制废弃)。
  • musicdl 引擎守卫保留:Rust 引擎任务在「下载器」模块,无法回传。
  • 校验:tsc / vite / cargo 全 0。

文件服务 → WebDAV(飞牛传输改版,最终形态)

fnOS 文件服务(WS + RSA/AES 登录)经三轮协议修复仍无法在用户 NAS 上稳定登录,且会话易失(重启要重登)。用户确认局域网场景改用标准文件协议,整体替换为 WebDAV(fnOS 系统设置 → 文件服务 → WebDAVHTTP 5005/HTTPS 5006Basic 认证,文件夹需在可见范围+团队文件夹要勾"允许协议挂载")。

  • 删除fnos.rsWS 客户端)、Feiniu.fnos_sessionsfeiniu_fnos_*feiniu_upload_from_url 命令、ConnectionsSettings 云朵登录弹窗。
  • 新增src-tauri/src/music/feiniu/webdav.rs——无状态:testPROPFIND Depth 0)、upload_file128KB 分块流式 PUTunfold 转 Stream 免 tokio-util)、upload_from_url(GET 拉流→直接 PUT,本地不落地,不再要求 Content-Length)、delete;上传前逐级 MKCOL;URL 逐段百分号编码;401/403/404/409 可读错误。
  • 命令webdav_test / webdav_upload / webdav_upload_from_url / webdav_deleteconfig 由前端每次传入,serde+specta 结构 WebDavConfig)。
  • 前端feiniuStore.webdav = {url, username, password, dir} 持久化 thing.music.feiniu.webdav(密码明文 localStorageLAN 工具可接受);webdavReady computed 四项齐备;patchWebdav / testWebdav;曲库目标目录语义改为 WebDAV 根下相对路径。设置页「存储与上传」为配置表单 + 测试按钮 + fnOS 侧设置折叠提示;TrackItem/发现音乐「下载到飞牛」门控全部改 webdavReady
  • 校验:vue-tsc / vite build / cargo check 全 0。

fnOS 登录协议修复(第三轮:"fnOS 登录未返回 token"

第二轮修复后登录请求有响应了(不再超时),但 rid 命中的回包里没有 token。对照 pyfnos 发现关键行为:登录最终成功包可能不按 reqid 关联——pyfnos 的 _is_final_login_success 是按内容特征(result=="succ" && token && secret)识别任意消息,而非按 reqid 匹配。rid 命中的那条很可能只是中间应答,带 token 的包以独立推送到达,被旧 reader 直接丢弃。

修复(fnos.rs):

  • 新增 login_waiter 通道;reader 对每条消息先做内容识别,命中最终成功包就投递给登录流程,reqid 匹配照旧(并容忍服务端把 reqid 回显成数字,resp_rid 辅助函数)。
  • login() 双通道等待:rid 应答里直接有 token → 用之;是中间应答 → 最多再等 8s 登录推送;send_and_wait 超时 → 再宽限 5s 等推送。
  • 识别两步验证挑战(isBindTwofaSecret / isTwofaEnforced / accessToken),明确报"账号开启了两步验证"而不是干等。
  • 诊断兜底:若始终等不到 token,报错里带上服务端实际回包的前 300 字符,用户可直接贴回继续定位。
  • 断开时同时唤醒登录等待者(Null)。

cargo check 通过。协议参考:pyfnos client.py(响应扁平结构:result/token/secret/longToken/reqid 顶层)。

fnOS 登录协议修复(第二轮:"fnOS 请求超时(encrypted)"

上一轮修完 WS 并发缺陷后,错误变成"请求超时(encrypted)"——说明握手和 util.crypto.getRSAPub 都通了,卡在登录这一条。根因:加密登录体里缺字段

已对照两个参考实现逐字核对并修复:

  • FNOSP/fnnas-api sdk/encryption.pylogin_encrypt() 示例(原开发注释里引用的就是它)
  • Timandes/pyfnos fnos/client.py_encrypt_login_data()

正确的加密登录体(必须逐字段对齐)

{"reqid":"...","user":"...","password":"...","deviceType":"Browser",
 "deviceName":"...","stay":false,"req":"user.login","si":"..."}

要点:

  • req: "user.login" 必须在加密体内 —— 服务端靠它路由;缺了就静默不回包(=超时)。
  • reqid 必须在加密体内,且与 pending 等待者登记的 key 一致;外层 encrypted 不带 reqid。
  • 没有 did 字段FNOSP 示例没有;pyfnos 有但非必需,为减少变量已去掉)。
  • stay: false
  • 加密方式:AES-256-CBC(key=32 字节随机 ASCII 串, iv=16 字节随机) + PKCS7RSA 只加密那个 32 字符 keyPKCS1v1.5),全部 base64。

顺带核对过、确认我们原本就正确的部分

  • 签名:hmac_sha256_b64(sign_key, json) + json(签名前置);sign_key = 登录响应 secret → AES 解密 → base64(明文) → base64 解码后的字节
  • 不签名白名单encrypted / util.getSI / util.crypto.getRSAPub(已按参考实现补上 UNSIGNED_REQ)。
  • 后续请求不携带 token 字段,服务端靠 WS 连接 + 签名识别会话;HTTP 上传走 Trim-Token 头。
  • reqid 格式宽松(各实现 18~28 位不等),我们现有的 0000000000000000<hex毫秒> 服务端接受(getRSAPub 已验证)。

其他改动

  • request() 拆成 send_and_wait(message, rid, label) + request(),登录走前者以便用加密体内的 reqid 关联。
  • 错误处理补齐 result=="fail" 分支并带出 msg/errmsg(密码错误等现在能看到具体原因)。
  • 新增应用层心跳:fnOS 网关会关闭长时间无消息的连接(参考实现默认 10s 一次 {"req":"ping"})。做成"发请求前按需补发"(>20s 才发),避免常驻任务的生命周期问题——多文件批量上传时 WS 不会被闲置断开。
  • 报错里的 label 更可读(如 user.login(encrypted))。

校验:cargo check exit 0。仍未对真实 NAS 验证,需要用户实测。


修复 fnOS 文件服务登录("输入密码没反应,很久后报发送请求失败")

根因(src-tauri/src/music/feiniu/fnos.rs

  1. WS 读写共用一个 tokio::sync::Mutex,且 reader 在持锁状态下 await stream.next() —— request() 要拿同一把锁才能发送。第一次 request()ensure_reader() 先 spawn 了 readerreader 抢到锁后就在等一条永远不会发出的消息;发送方要么永久阻塞(超时也覆盖不到锁获取),要么在 reader 已退出后发送才报错 —— 这正是"等很久 → 发送请求失败"的来源。
  2. 握手没带 Origin。浏览器发起的 WS 必带 Origin,Rust 客户端默认不带;fnOS 会据此判断来源,缺了它会"握手成功但立刻被对端关闭"。这是"连上了却什么都没发生"的典型原因。
  3. 没有回 Pong。tungstenite 的 Stream/Sink API 不会自动回 Pong,服务端 ping 得不到响应会判定死连接。
  4. 连接/请求均没有超时覆盖timeout 只包了 rx,不含锁获取与握手),主机不可达时会干等系统 TCP 重试。

修复

  • 读写分离:futures_util::StreamExt::split(ws) → writer 任务独占写半边(mpsc 队列),reader 独占读半边。发送不再与"等响应"争锁。
  • 握手请求改为构造 Request 并注入 Origin(由 ws url 反推 http://host:port)与浏览器 UA。
  • reader 显式处理 Ping → Pong,并记录断开原因到 closed: Arc<Mutex<Option<String>>>
  • connect_async 加 12s 超时;request 的 15s 超时改为覆盖整个等待,并在入口先检查 closed,断开时立刻带原因报错。
  • 所有报错都带上尝试的 ws 地址或断开原因,例如 连接 fnOS 文件服务失败:ws://ip:port/websocket?type=main → ...
  • 前端:ConnectionsSettings 的 fnOS 弹窗加 fnosConnecting(按钮显示"连接中…")与弹窗内联错误块(错误里含地址/状态码,留在弹窗里可复制),并显示"当前地址"供核对。
  • feiniuStore.fnosLogin(connectionId, ...) 改为显式接收 connectionId 并校验它等于 activeId —— 上传命令按 settings.feiniu_active_id 找会话,登录非激活连接会存成永远用不上的会话。

注意:以上都只做了 cargo check + 前端构建校验,没有对真实飞牛 NAS 验证过协议细节(Origin 校验、file.checkUpload/Trim-Path 约定可能随 fnOS 版本不同)。如果仍失败,报错文案已包含具体地址与失败阶段。


发现音乐:分组头去白底 + 新增「下载到飞牛」一条龙 + 飞牛上传链路修复

分组头

MusicModule.vue 里源分组头原来是 sticky top-0 z-10 bg-background/95 backdrop-blur,在浅色背景下会形成一条白色横条。已去掉 sticky 与底色,改为普通的 border-b border-border/60 px-2 分组标签(px-2 与行对齐)。

新增 Rust 命令 feiniu_upload_from_url(下载 + 上传一条龙,本地不留文件)

链路:前端 feiniuStore.uploadUrlToFeiniu()invoke('feiniu_upload_from_url')Feiniu::fnos_upload_from_url()fnos::upload_stream()

  • fnos.rs 新增泛型 upload_stream<S, E>()Part::stream_with_length(Body::wrap_stream(stream), size),配套 upload_client(proxy)proxy 为空则 no_proxy())。
  • upload_file() 重写为流式stream::unfold + tokio::fs::File 分块 128KB)。原来用 std::fs::read 把整个文件读进内存,100MB 无损会直接吃 100MB RAM。用 unfold 是为了不引入 tokio-util 依赖。
  • FnOsSession::connect 增加 scheme 校验:https:// 直接返回可读错误(ws_url 只把 http:// 换成 ws://https 会变成非法 WS scheme 并抛出难懂的 tungstenite 错误)。
  • guess_mime(&Path) 改为 mime_of(&str)(现在需要按 NAS 上的文件名推断)。
  • 依赖音源返回 Content-LengthfnOS checkUpload 需要精确 size),缺失时返回可读错误,而不是先把整首缓冲进内存。
  • 前端 downloadToFeiniu():懒解析歌曲先 resolveSong(source, index, quality) 拿真实链接(quality 取默认下载音质),合并 defaultDownloadHeaders + defaultDownloadCookies 为 Cookie 头,并过滤非 ASCII 头reqwest 遇到非法头值会 panic,不能把外部数据直接塞进去)。

飞牛上传链路审查结论(重要)

  1. fnOS 会话只存在内存里Feiniu.fnos_sessions),应用重启即失效 → 设置页显示的「未连接」是准确的,需要重新点云朵图标登录。已在设置页与登录弹窗里明确写出"仅本次运行有效"。
  2. 仅支持 http:// 局域网直连,https/frp 无法上传(协议限制),已给出明确报错。
  3. feiniu_scan_local 扫描 savedir + feiniu_local_dirs,因此下载到本地目录的文件能被"上传最近下载"找到;但该功能是按 mtime 10 分钟窗口筛选,与具体下载任务无关联——这是它不可靠的根因(这就是为什么要新增 URL 直传)。
  4. feiniu_fnos_uploadsettings.feiniu_active_id 找连接,fnos_login 用传入的 connection_id;只要激活连接一致就没问题。

校验(本机 bash 不可用,PowerShell + 绝对路径)

  • cargo checksrc-tauri%USERPROFILE%\.cargo\bin\cargo.exe)→ exit 0,仅 3 个既有 warning
  • vue-tsc --noEmit → exit 0vite build → exit 0

曲库左栏:去掉灰底容器、数量列对齐、行高统一

用户反馈截图问题(灰底 + 文字竖向不齐)后的修正,MyMusicLibrary.vue

  • 去掉 bg-muted 容器:来源项不再包在灰色圆角块里。选中态保留 bg-background font-medium shadow-sm,并ring-1 ring-border——否则白底上的白色胶囊只有 5% 透明度的阴影,几乎看不见(顶部 Tabs 那个"可见边框"其实是 :focus-visible 的 ring,不是常态样式,不能照抄)。
  • 数量列始终渲染:原来 v-if="nav.count" 会在 0 时隐藏整列,造成右侧数字列参差。改为无歌曲时显示 0,并加 min-w-[3ch] text-right tabular-nums 让个位数/三位数右边缘对齐。
  • 行高统一为固定值:来源项 h-8、歌单子项 h-7,配合 items-center,不再依赖 py-1.5 + 行高推算,彻底消除逐行细微的垂直错位。
  • 未选中项 hover 从 hover:bg-accent/50 统一为 hover:bg-accent/60(无容器后需要更明显的悬停反馈)。
  • 歌单子项的选中态也同步改成同一套胶囊样式,保持层级内一致。

曲库左栏收窄 + 选中态对齐顶部 Tabs + 切换动画

MyMusicLibrary.vue

  • 左栏宽度 w-[210px]w-[168px]"飞牛曲库 → 数量"之间的大段空白因此明显收窄)。内边距 p-2.5p-2,字号 13px → 保留。
  • 三个来源(飞牛曲库/本地曲库/我的歌单)从裸列表改为 rounded-lg bg-muted p-1 容器 + 选中项 bg-background font-medium shadow-sm rounded-md,与顶部 TabsList/TabsTrigger 的默认选中态一致。
  • 歌单子树加 border-l 缩进表示层级;展开/收起加了 pl-list Transitionopacity + translateY(-6px)0.18s)。
  • 标签截断修复<span class="truncate"> 在 flex 行里不会真正截断(flex item 默认 min-width:auto)。改成 min-w-0 flex-1 truncate + 右侧计数 shrink-0,并去掉 ml-auto。以后左栏加条目记得照这个写。
  • 右栏内容切换动画:给 <section>:key="view" + 全局 tab-animate,靠重挂载触发动画(与模块级 TabsContent 的做法一致,无需自造 Transition)。

歌单新增「编辑音乐」编辑器

  • 新组件 src/modules/music/components/PlaylistEditorDialog.vueprops open / playlistIdemit update:open)。
    • 双 Tab全部音乐(来源筛选 全部/飞牛/本地 → 勾选加入;已在歌单的项置灰并标「已加入」,不可重复勾选)、已在歌单(勾选批量移出,勾选框用 destructive 色)。
    • 含搜索(歌名/歌手/专辑)、全选当前列表、增量渲染每批 120 + 「显示更多」、飞牛曲库「继续加载」、本地曲库未扫描时的内联「扫描本地」按钮。
    • 勾选框不用 Checkbox 组件:整行是 <button>,视觉勾选框是纯 span(避免 label/按钮与 Checkbox 双重触发导致 net-zero toggle)。若以后要加交互型复选框,注意这个坑。
    • 关闭/打开时 resetState(),切 Tab 与切来源时清空选择(避免"看不见的项仍被选中"导致数量对不上)。
  • 入口:MyMusicLibrary.vue 歌单详情工具栏的「编辑音乐」按钮,以及歌单为空时 Empty 里的按钮。
  • 依赖 store 既有 APIaddToPlaylist(内部已按 guid+source 去重)、removeItemsFromPlaylist(id, keys)key 形如 ${source}:${guid})。

音乐模块播放器入口改版 + tabs 统一(在同日重构之后)

本节修订上面「结构变更」里的播放器结论,以本节为准。

  • 删除底部播放条GlobalPlayerBar.vue 已删除,App.vue 不再渲染播放条。不要再新增底部坞站播放条。
  • 新增标题栏音乐栏 src/components/layout/TitleBarMusic.vue,插在 TitleBar.vue搜索输入框的左侧(在保存设置按钮与搜索框之间),外层包 pointer-events-auto,触发按钮带 @mousedown.stop(标题栏是 Tauri 拖拽区,不加会导致点击被拖拽吞掉)。
    • 未播放过(store.nowPlaying 为空)→ 只显示唱片图标,隐藏歌名。
    • 有曲目 → 图标替换为封面(rounded-full + overflow-hidden),store.playing 为真时用 CSS animation-play-state 旋转(8s/圈,暂停保留角度)。
    • 点击弹出方形控制窗(Popoverw-[300px],约 300×340):封面(点击进大屏)、曲目信息、ScrubBar 进度、循环/上下首/播放/队列、音量 ScrubBar、歌词/队列入口。
    • feiniu.playError 的 toast 消费点从 GlobalPlayerBar 移到了这里——删播放条时容易漏掉这个 watcher,导致播放失败静默。
  • 音乐模块 tabs 回归统一写法:改回 <Tabs><div ref="tabsListRef" class="shrink-0"><TabsList class="grid w-full max-w-md grid-cols-4 !bg-transparent !p-0 !shadow-none">…<TabsTrigger class="gap-1.5"><Icon class="size-3.5"/>标签</TabsTrigger> + <TabsContent class="mt-4 min-h-0 flex-1 tab-animate">;模块根节点恢复 h-full p-6 overflow-hidden flex flex-colSegmentedNav 不再被 MusicModule 使用,仅 NowPlayingDialog 的歌词/队列切换仍在用。
  • 发现音乐 / 任务 / 设置三块内部的横向内边距改为 0(由模块根 p-6 统一提供,避免左右不对称)。

音乐模块重构(审查之后落地实施)

结构变更(重要,改这块前先读)

  • 播放器已提升到全局层GlobalPlayerBar.vue + NowPlayingDialog.vue → 已被上一节取代;NowPlayingDialog.vue(大屏,含歌词/队列)保留,由标题栏方形控制窗打开。
  • 新增通用组件src/components/common/ScrubBar.vue(提交式 seek 进度条,含缓冲层/时间气泡/键盘支持)、SegmentedNav.vue(分段导航,现由 NowPlayingDialog 使用)。
  • 音乐模块导航:模块级 Tabs(曲库/发现音乐/下载任务/设置);「曲库」内部是 MyMusicLibrary 的「左侧来源树 + 右侧列表」两栏,来源树含飞牛曲库/本地曲库/我的歌单(歌单列表在树内展开)。
  • TrackItem.vue 重写为 44px 紧凑行(网格列 28px 36px 1fr 74px 28px,有专辑时多一列);序号列悬停原位切换播放按钮,播放中显示 .music-eq 跳动均衡器(该 CSS 已加到 style.css 全局,NowPlayingDialog 复用)。
  • 所有列表表头/行网格用字面量 Tailwind 类或内联 gridTemplateColumnsTailwind v4 不识别运行时拼接的类名)。

feiniuStore 关键 API 变化

  • PlayableItem.source 增加 'preview',另加 coverUrl / url / ext 字段。
  • 新增 nowPlaying(试听优先于队列当前项)、playPreview / stopPreviewplayNext / addToQueue / removeFromQueue / clearQueueloadMoreTracks(分页 200/页)、searchKeyword(曲库搜索,之前是孤立局部变量导致搜索无效)、playError / clearPlayErrormuted / toggleMutesetPlayModebufferedPercentnowPlayingTab
  • playItem(item, context?) 第二参数是播放上下文;不传则退化为单曲播放。
  • init() 幂等;setCacheMode/clearCache 不再中断播放;shuffle 用「待播池 + 历史栈」,自动切歌也随机;currentLine() 改为缓存索引 + 二分。
  • 已移除 queueVisible / lyricVisible(改用 nowPlayingOpen + nowPlayingTab)。
  • musicStore.startDownload 返回值改为 { engine, skipped, taskId? }

校验方式(本机 bash 工具不可用,用 PowerShell + node 直接跑)

node node_modules/vue-tsc/bin/vue-tsc.js --noEmit -p tsconfig.json
node node_modules/vite/bin/vite.js build

两者在本次重构后均为 exit 0。注意:PowerShell 工具不返回 stdout,需 | Out-File 后再 ReadRemove-Item 被沙箱拦截(静默失败),删文件要用 node -e "fs.unlinkSync(...)"

排障经验:feiniuStore 任何「返回对象里引用了不存在的变量」(如漏掉 clearQueue 函数体)都会让 TS 对整个 store 的类型推断失败,进而让所有 store.xxx 变成 any,报出一堆看似无关的 TS7006 implicitly has an 'any' type。遇到成片 implicit any,先检查 store 自身的错误。

报告

MUSIC_MODULE_REVIEW.md 已追加「§9 重构落地记录」,逐条对应 30 项改动与「仍未做」清单。


音乐模块全链路审查(数据请求 / 播放控制 / 状态管理 / UI)

产出:

  • MUSIC_MODULE_REVIEW.md —— 完整审查报告(P0/P1 bug + 性能瓶颈 + 25 条界面缺陷 + 重设计方案 + 分阶段落地路径)
  • music-redesign-mockup.html —— 「当前实现 / 建议方案」可切换的界面改版对照稿(浅色主题)

关键结论(后续如要动这块,先看这些)

架构共识(不要改错方向)

  • 搜索/解析/下载 → Python 桥接 musicdl;播放 → Rust 本地流代理 127.0.0.1:<port>/feiniu/streamsrc-tauri/src/music/feiniu/proxy.rs),真流式透传 Range,seek 依赖它,别改成前端直连 NAS
  • 播放内核在 src/stores/feiniuStore.ts(不是 musicStore);musicStore 只管搜索/下载任务。
  • PlayerBar.vue / PlayerFull.vue 用的是 useFeiniuStore,不是 musicStore。

P0(确认可复现)

  1. 飞牛曲库搜索框完全无效:MyMusicLibrary.vue:21keywordInput 从未写入 feiniuStore.keyword,而 loadTracks 读的是 store 的 keyword(全仓无赋值点)。
  2. 重启后点播放键无反应:restoreQueue() 只恢复队列不设 audio.srctoggle()play() 被空 catch 吞掉。
  3. 双音频引擎可同时发声:MusicModule.vue:1413 的试听 <audio>feiniuStorenew Audio() 互不感知。

最值得优先做的一件事PlayerBar + PlayerFullMusicModule.vue 提升到 App.vue 全局层。原因:App.vue 的模块容器是 :key="activeModule" 重挂载,切到其他模块时播放条整体卸载,但音频继续播 → 用户失去所有控制入口。

其它已确认的坑

  • shuffle 模式下自动切歌是顺序的(next(manual) 的随机分支带 manual 条件)。
  • setCacheMode('stream') / clearCache() 都会 clearPlayback(),改设置即中断播放。
  • CacheSettings.vuecacheEnabled 开关从不回填 store 的 cacheMode,状态不同步;cacheMax 是死代码(后端字段是 feiniu_cache_max_gb)。
  • feiniuStore.init()MyMusicLibrary/CacheSettings 重复调用,内含 restoreQueue() 会覆盖内存队列,应加幂等标志。
  • musicStore 任务持久化把每首歌的 rawSearch 一起写 localStorage,大歌单会爆 5MB 配额且静默失败。
  • playItem 未命中队列时替换整个队列(单曲播放语义),歌单里的 TrackItem 还没有双击且播放键会被 disabled → 正在播的歌无法暂停。
  • rust 下载引擎下 doDownload 仍跳「下载任务」tab,但该 tab 是空的(任务在下载器模块)。
  • 拖动进度条即写 currentTime → 流式下反复中断 Range 请求,应改「拖动预览 + 松手提交」。

界面主线(详见报告 §5 播放条未贴底且离开模块即消失;模块左右内边距不对称(左 28 / 右 40 含滚动条);三套切换控件(模块 Tabs / 自绘 segmented / 裸文字);音质徽标 5 色抢戏;列表是「卡片堆」而非紧凑行;Empty 组件被裸 children 误用(gap-6 + md:p-12 + border-dashed 无 border)。改版方向:左侧来源树 + 紧凑表格列表 + 全宽坞站播放条 + 可折叠「正在播放」右栏。