Files
FruitServer/docs/plans/INBOX.md
T

3927 lines
265 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.
## [已处理] 2026-10-20 | K29.3 播放页恢复深色外观(与参考图一致)
**用户指令(2026-10-20,原文)**:
> 「现在播放页底部怎么是白色的 和我发你的图片不一致 而且 整个播放页仍然有黑色背景」
**现状(真机截图核对)**:视频铺满全屏,但底部信息面板是白色,与参考图的深色播放器不一致。
**口径(以参考图为准,播放页为深色)**
1. 页面底色:`#0D0D10`;仅播放器页使用深色,不影响其他浅色页面。
2. 底部信息面板:深色 `#0D0D10`(或 `0xED000000` 叠在视频上),白色主文字、`#BBBBBB` 次文字、深色分割线;进度条白色/橙色;底栏「选集 · 全 n 集 · 免费观看」使用 `#1C1C1E` 胶囊 + 白字。
3. 顶栏保留黑色渐变;右侧 rail 保持深色半透明圆底 + 白字。
4. 移除覆盖整页的四段式黑色渐变(`0x66000000 → 0xE6000000`),避免把视频整体压黑;只在顶栏与底部面板处做必要渐变。
5. 底栏右侧仍为「清屏」按钮(K29.2)。
6. 其余口径不变(暂停才显示中心按钮、清屏跨集保持、倍速、拖动时间)。
**测试/门禁**:更新底栏颜色相关断言(如有);`dart analyze lib test` 零告警 + `flutter test` 全过 + `flutter build apk --debug`。
**流程**:从 develop 拉 `fix/flutter-k29-dark-player`;作者 `Zhou Weidong <502333560@qq.com>`;`--no-ff` 合回 develop 并 push;PROGRESS 留痕;真机验收由用户执行。
---
## [已处理] 2026-10-20 | K29.2 播放页底栏右侧改为「清屏」
**用户指令(2026-10-20,原文)**:
> 「底部的右边是清屏」
**口径**
1. 播放页底栏最右侧按钮由 PiP/小窗改为「清屏」:
- Key `player-bottom-clear-screen`,图标 `Icons.fullscreen_rounded`,tooltip 用 `l10n.clearScreen`;
- 点击执行 `actions.onClear`(进入清屏态,保留右上退出清屏按钮)。
- 删除 `player-pip` 底栏按钮;PiP 若仍可用,保留在「更多」面板内,不重复放底栏。
2. 复核整页无多余黑底:底栏为白色卡片,页面底色 `AppColors.ink`;只有视频画面与顶栏渐变可为深色。
3. 测试:新增底栏清屏按钮存在且点击进入清屏态;删除 `player-pip` 断言;既有播放测试全过。
**门禁**:`dart analyze lib test` 零告警 + `flutter test` 全过 + `flutter build apk --debug`。
**流程**:从 develop 拉 `fix/flutter-k29-bottom-clear`;作者 `Zhou Weidong <502333560@qq.com>`;`--no-ff` 合回 develop 并 push;PROGRESS 留痕;真机验收由用户执行。
---
## [已完成·待用户验收] 2026-10-20 | K29.1 播放页视觉收紧:底部压缩、去掉全屏观看、右栏下移
**用户指令(2026-10-20,原文)**:
> 「视频播放页底部的区域太大了 而且不要显示全屏观看 还有右侧的收藏评论喜欢 分享 位置太靠上 整体ui 不好看」
**口径**
1. 删除视频下方「全屏观看」胶囊及相关状态/Key/测试;清屏仍由更多面板的「清屏播放」和右上退出按钮负责。
2. 去掉整页黑色底:
- `player_view.dart` 的 `ColoredBox(color: AppColors.onSurface)`(浅色主题下是深色)改为 `AppColors.ink`;
- 底部信息面板 `Color(0xED000000)` 改为白色 `AppColors.card` + 深色文字/浅色分割线;顶栏保留深色渐变以保证视频上的可读性;
- 右侧 rail 保持深色半透明圆底(视频上可读),不做黑色大底。
3. 底部信息区整体压缩约 30–35%:
- 只保留:剧名行(含 `>`)、`第 n 集 | 简介`(默认单行,点击展开)、细进度条、底栏「选集 · 全 n 集 · 免费观看」+ 小窗按钮。
- 作者声明默认不显示,展开简介时再显示;收紧行高与上下 padding(建议整体高度 ≤ 200px,按 390×844 基准)。
- 不遮挡视频主体,面板上沿不超过屏幕高度的 78%。
4. 右侧互动 rail 下移:与底部面板上沿对齐(约屏高 55–60% 起),按钮间距 18、图标 24、数字 11px;不得与底部面板重叠。
5. 其余播放口径不变(暂停才显示中心按钮、清屏跨集保持、倍速、拖动时间)。
**测试/门禁**:删除全屏观看相关断言;新增底部面板高度/rail 位置约束断言(宽松阈值);`dart analyze lib test` 零告警 + `flutter test` 全过 + `flutter build apk --debug`。
**流程**:从 develop 拉 `fix/flutter-k29-player-polish`;作者 `Zhou Weidong <502333560@qq.com>`;`--no-ff` 合回 develop 并 push;PROGRESS 留痕;真机验收由用户执行。
---
## [已完成·待用户验收] 2026-10-20 | K29 播放页按新参考重构 + 语言入口移到「我的」
**用户指令**:「播放页面按照这个来修改 剧场的语言不展示 在我的页面切换」(附播放页/更多面板/评论面板三张参考图)。
**口径**
### A. 播放页(严格按参考图)
1. 顶栏:左侧返回 + `第 n 集`;右侧「倍速」入口(图标 + 文字,显示当前倍速)+ `⋮` 更多。移除顶栏大标题/副标题/题材胶囊/分页点与清屏按钮。
2. 右侧互动 rail:收藏(含数)、评论(含数)、点赞(含数)、分享;无数据的数量隐藏,不伪造。
3. 视频下方居中「全屏观看」胶囊(进入沉浸/清屏态)。
4. 底部黑色信息面板:剧名 + `>`、`第 n 集 | 简介`(可展开/收起)、作者声明(有则显示)、细进度条、底栏「选集 · 全 n 集 · 免费观看 ⌃」+ 右侧小窗按钮。
5. 更多面板(白色):倍速分段 0.75/1/1.25/1.5/2/3;清晰度分段(接口只有 AUTO 时仅显示 AUTO,其余置灰);不感兴趣;清屏播放;弹幕开关(无后端,置灰并标注暂未开放);弹幕设置;离线缓存;字体大小;投屏;定时关闭。不可用项禁用,不做假功能。
6. 评论面板(白色):`n 条评论` 标题、评论列表(头像/昵称/内容/时间/回复/点赞)、底部输入框。
7. 既有口径不得回退:暂停才显示中心播放按钮、清屏/退出清屏、清屏跨集保持、倍速即时生效、拖动进度显示时间。
### B. 剧场移除语言
- 删除 `theater-language-chips` 及 `TheaterLanguage` 过滤逻辑/测试;分类 chips、搜索、下拉刷新保留。
### C. 我的页新增语言切换
- 菜单新增「语言」项(Key `mine-language`),弹窗三选一:简体中文 / English / Bahasa Indonesia(Key `mine-language-zh|en|id`)。
- 选中后立即切换并持久化;首次启动仍跟随系统(`resolveAppLocale`)。
- 需要 LocaleController(ChangeNotifier)+ Provider + MaterialApp 的 `locale` 覆盖;不得破坏现有 l10n。
**测试/门禁**:播放页顶栏/底栏/更多/评论结构 Key 断言;剧场语言相关用例删除或改为「不存在」;语言切换用例(选中后文案变化 + 持久化);`dart analyze lib test` 零告警 + `flutter test` 全过 + `flutter build apk --debug`。
**流程**:从 develop 拉 `feature/flutter-k29-player-language`;作者 `Zhou Weidong <502333560@qq.com>`;`--no-ff` 合回 develop 并 push;禁 `git add -A`;PROGRESS 留痕;真机验收由用户执行。
---
## [已完成·待用户验收] 2026-10-20 | K28-R 按简图重设计剩余 UI(A2 → B → C 连续执行)
**用户指令**:「UI 不是完全按照这个设计的 重新设计ui」(再次附 5 屏 + 4 面板简图)。
**背景**:K28-A1(4 Tab + 首页骨架)与 K28-S(浅色暖橙换肤)已合并;剧场、创作、我的、播放页仍是旧结构,首页 Hero 也需要按简图重做。
**要求**:严格按简图还原,样式用「二·五 视觉风格 v2」,底部保持 4 槽(不做中央 +)。顺序执行、每步单独分支与合并:
### A2 剧场(feature/flutter-k28-a2)
- 顶栏:搜索框 + 头像(与首页一致)。
- 分类 chips:全部 + 服务端分类,橙色实心选中。
- 语言 chips:全部语言/中文/印尼语/马来语/英语(纯 UI 选中态,不做过滤)。
- 两列剧卡:大封面(原图比例)、左上粉色胶囊、右上锁数量、右下「全 n 集/大结局」米色胶囊、标题粗体、副行「更新至 n 集 · 日期」灰字、右下橙色圆形 +;保留搜索与下拉刷新。
### B 创作 + 我的(feature/flutter-k28-b)
- 创作:「创作」大标题 + 「让故事成为漫剧」副标题;两张白卡:AI 制作(从故事或想法开始)、上传发布(发布已完成的漫剧),右侧箭头;点击进入诚实占位页。
- 我的:头像 + 昵称 + 签名;三宫格快捷入口(收藏/追剧、观看历史、本地下载);金币余额卡 + 充值;会员卡;菜单(我的作品、创作者中心、消息通知、设置、帮助与反馈)。
### C 播放 + 四面板(feature/flutter-k28-c)
- 播放页:大标题 + 橙点、副标题、右上「…」与圆形按钮;Hero 大图 + 题材胶囊 + 分页点;右侧互动 rail(点赞数/评论数/收藏/分享);底部剧名、第 n 集、标签 + 关注、简介、进度条 + 时间、选集/更多。
- 面板:选集(6 列、锁定态)、评论(列表 + 输入)、更多操作(倍速/清晰度/下载/文字大小/投诉)、内容解锁(金币/广告/会员 + 「具体方式以作品而定」)。
- 既有口径禁止回退:清屏/退出清屏、暂停才显示中心播放按钮、清屏跨集保持、倍速弹窗、拖动显示时间。
**测试/门禁(每批)**:`dart analyze lib test` 零告警 + `flutter test` 全过 + `flutter build apk --debug`;更新受影响的旧断言;新增结构 Key 断言。
**流程**:作者 `Zhou Weidong <502333560@qq.com>`;每批 `--no-ff` 合回 develop 并 push;PROGRESS 留痕;真机验收由用户执行。
---
## [待处理] 2026-10-20 | K28 客户端 UI 重构(按 5 屏 + 4 面板简图)
**用户指令**:「先读懂这个ui 简图 然后根据这个简图先出ui 在划分任务」
**计划文件**:`docs/plans/2026-10-20-K28-ui-wireframe.md`(先读,按其中分屏口径执行)。
**范围**:只做 UI 与信息架构,不改后端契约、不新增依赖、不做假接口;功能缺口在计划文件第五节登记。
**分批**
- K28-A1(立即执行):底部 4 Tab(首页/剧场/创作/我的)+ 首页浏览化改造;剧场本批只改名/保留现状。
- K28-S(A1 合并后立即执行):浅色暖橙视觉主题——按用户 2026-10-20 参考图,改全局 token、剧卡/chips/角标样式;**底部导航保持 4 槽(不做五槽/中央 +)**;口径见计划文件「二·五 视觉风格 v2」。
- K28-A2(A1 验收后):剧场页按简图改造(分类 + 语言 chips + 两列剧卡)。
- K28-B:创作页 + 我的页 UI。
- K28-C:竖屏播放页 UI + 选集/评论/更多操作/内容解锁四面板。
- K28-D:l10n 三语补齐、回归测试、门禁、PROGRESS 留痕。
**约束**
- 每批从 `develop` 拉 `feature/flutter-k28-*`,完成后 `--no-ff` 合回 `develop` 并 push。
- 门禁:`dart analyze lib test` 零告警 + `flutter test` 全过 + `flutter build apk --debug`。
- 提交作者 `Zhou Weidong <502333560@qq.com>`;禁 `git add -A`;真机验收由用户执行。
- 播放页现有「清屏/退出清屏、暂停才显示播放按钮、清屏跨集保持、倍速弹窗」口径不得回退。
---
## 【待 Trae 协助】2026-10-19 | 客户端 P0→P2 契约缺口(不阻塞其余项)
> 以下能力当前后端契约/配置不具备,客户端不做假按钮、不造接口;已落地的同类入口保持真实可用。请确认后端排期或补充凭据。
1. **看广告解锁**:`/app/**` 无激励广告相关接口,Flutter 工程也未集成广告 SDK;当前只实现金币解锁。需提供广告平台账号、广告位 ID 与发奖校验接口。
2. **Google Play 充值**:`POST /app/orders/verify` 需要真实 `purchaseToken` 与商店商品配置;服务端默认 `verifier=disabled`,客户端无 Google Play Billing 商品/凭据。已按「暂不接则隐藏入口」处理,钱包页不放假充值按钮。
3. **短信验证码通道**:`POST /app/auth/otp` 已对接,但服务端当前是 `LogOtpSender`,只写日志不发短信;UI 可发起请求,真机收码需替换短信供应商后才能验收。
4. **创作 tab**:契约明确「创作者中心由 Web 端承载,App 不提供创作者中心」,C 端只有 `POST /app/creator/apply`,没有 AI 制作、上传、发布接口。无法做真实创作 tab,需产品确认是否改为「入驻申请」或开放 C 端接口。
5. **会员购买**:无 C 端会员商品/订阅接口,后台商品契约也禁止启用 VIP 商品;无法做真实购买流程。
6. **搜索接后端**:`/app/**` 无搜索接口;当前只能对已加载目录本地过滤,无法做服务端搜索。
7. **剧场语言筛选**:剧目有 `language` 字段,但 `/app/home/feed`、`/app/discover/feed`、卡片 VO、详情 VO 均未下发语言;客户端无真实数据可筛。
8. **播放清晰度切换**:`/app/episodes/{id}/play` 当前只下发 `qualities: ["AUTO"]`,没有多清晰度地址;仅保留 AUTO,不伪造切换。
9. **Google 登录**:需要 Google OAuth 客户端 ID/服务器配置;当前登录页已移除该假入口。
---
## 补充指令(2026-10-19 23:30 周卫东)
> 1. **UI/UX 参考 Google 设计体系**:遵循 Material Design 3 规范 + Google Stitch 设计理念,界面现代简洁、层次分明,做成精品,不要粗糙界面。
> 2. **收尾提交 develop**:所有工作做完、门禁全过后,把 feature 分支合并回 develop 并 `git push origin develop`。用户明天拉 develop 打包验证。
---
# INBOX — 给 Codex 的指令队列(FruitServer 仓库内)
> **Codex 每次开始任务前必须先读本文件**(2026-10-16 起指令通道从 specs 仓库迁到本仓库)。
> 按条目顺序执行,处理完的条目标记 `[已处理]`。指令来源:项目经理(Trae)。有异议在条目下留言,不要静默跳过。
>
> **规范以 specs 仓库为准**(`AGENTS.md`、`CODING_STANDARDS.md`、`GIT_WORKFLOW.md`、`docs/01~20`、`docs/plans/codex-研发任务清单.md`)——
> specs 现在**只拉取、由项目负责人更新**,我们不再往它提交。工作留痕写在本仓库 `docs/plans/PROGRESS.md`。
> ⚠️ **双 Agent 协作红线(Trae + Codex 共守)**
> 1. **单一写者**:同一时刻只有一个 agent 在改代码/写 PROGRESS/跑门禁。
> 2. **留痕**:谁干活谁写 `docs/plans/PROGRESS.md`(命令 + 原始输出 + 解读,逐动作追加)。Trae 接手时用「【Trae 接手】」标记。
> 3. **git 不冲突**:提交前先 `git status`/`git log`;只 `git add` 自己的文件,禁止 `git add -A`/`.`;不夹带对方改动。
> 4. **文件不覆盖**:写 INBOX/PROGRESS 前先读最新,追加不覆盖。
> 5. **求助通道**:遇到阻塞/不确定,在本文件顶部写「【待 Trae 协助】+ 问题描述」。
> 6. **禅道同步**:任务「开始/完成」时同步更新禅道任务状态。
> 7. **验收归用户**:每批做完**停在干净交接点**,由用户亲自验证;回执必须含「用户可照做的验证步骤 + 每步预期结果」。
> 8. **门禁(不达标不提交)**:后端 = `mvn test` 全过(核心接口须有契约测试);Web = `pnpm build:pro` 通过 + eslint 零错误;Flutter = `dart analyze lib test` 零告警 + `flutter test` 全过。
> 9. **分支(按 `GIT_WORKFLOW.md`)**:从 `develop` 拉 `feature/<模块>-<简述>`,小步提交;完成后 `git checkout develop && git merge --no-ff feature/...` 再 `git push origin develop`;**绝不动 `main`**(dev→main 走 PR,由用户操作)。
> 10. **设备验证必须「设备侧取证」(2026-10-16 新增,血的教训)**:真机 / 模拟器 / 小程序等目标设备的验证,**证据必须产自设备侧**(设备侧集成测试、设备日志、设备截图、设备侧抓到的连接)。**禁止**用"在开发机上 `curl` 某地址返回 200"证明设备可达 —— 本机 curl 本机网卡地址必然 200,这是**假阳性**(已实际误导过一次:`10.157.242.1` 是开发机 VPN 网卡地址,手机根本不可达,导致真机「剧目列表加载失败」)。**拿不到设备侧证据的,一律标「未验证」**,不得写成"已验证"。
---
## 🚨🚨 立即开工(最高优先 · 不要等巡检 · 不要做任何收口动作)—— 2026-10-19 周卫东拍板
> **读到这里就直接开始做下面 P0 → P1 → P2,逐条落地并跑门禁,每条完成写 PROGRESS 留痕。**
>
> **背景**:周卫东对照「漫剧 App 功能规划图」盘点 ReelShort(Flutter)现状,结论:「看剧」链路真实可用,但「商业化」和「创作」大面积是**假功能/缺失**(金币写死 0、充值弹「示例数据」、解锁弹「即将开放」、下载不落盘、Google/手机登录弹「即将开放」、创作 tab 整个没有)。
>
> **拍板**:全量补齐 P0→P1→P2。原则——**不贪多,但每个都要能用、要精品**。后端接口已就绪,**全部是 App 端对接活,不重做后端**。
>
> **工作目录**:`C:\work\ReelShort`(Flutter,分支 `develop`)。禅道任务:29/30/31。
### P0 — 消灭假功能(做出来的必须能用,最优先)
1. 内容解锁闭环:对接 `POST /app/episodes/{id}/unlock`,金币解锁 + 看广告解锁跑通,不再「即将开放」
2. 金币余额:对接 `GET /app/wallet`,不再写死 0
3. 充值流程:对接订单接口;若支付暂不接则**先隐藏入口**,不给假按钮
4. 本地下载落盘:对接 `GET /app/downloads/{episodeId}/url`,真下载 + 离线播放
5. 手机号登录:对接 `POST /app/auth/otp`,不再「即将开放」
### P1 — 补齐规划核心功能
6. 「创作」tab(底部导航 3→4):AI 制作入口 + 上传发布入口,对接 creator-center 接口
7. 开通会员购买流程
8. 首页「榜单」tab
9. 剧场「语言筛选」
### P2 — UX 打磨(精品感)
10. 搜索接后端搜索接口(不再本地过滤)
11. 点赞/评论显示计数
12. 设置页完善(移除 debug「服务器地址」)
13. 首页轮播 banner 真正用起来
14. 播放清晰度切换
**门禁**:每条做完 `dart analyze lib test` 零告警 + `flutter test` 全过,再下一条。测试命名 `should_<期望>_when_<条件>`。
### 执行状态(2026-10-19 收尾)
- P0:金币解锁、钱包余额、本地下载落盘、手机号登录已完成;充值入口按契约隐藏;看广告解锁缺广告接口,见顶部第 1 条。
- P1:榜单 tab 已完成;创作 tab、会员购买、剧场语言筛选缺契约,见顶部第 4/5/7 条。
- P2:点赞/评论计数、设置页清理、首页轮播 banner 已完成;服务端搜索、播放清晰度切换缺契约,见顶部第 6/8 条。
- 门禁:`dart analyze lib test` 零告警;`flutter test` 全过(`+385 ~1`);`flutter build apk --debug` 成功。
- 交付:feature/flutter-p0-commerce 已以 --no-ff 合并 develop(ec2db68)并推送 origin/develop。
## [待处理] 2026-10-19 | K27.16 客户端 develop 合并进 release/v1.0.0
**用户指令(2026-10-19,原文)**:
> 「然后把 dev 相关改动合并到release/v1.0.0」
**前置状态**
- `ReelShort` release/v1.0.0 = `814b2c0`(含 `3d0e080` 明文 HTTP/双网络修复 + 公网地址 `http://39.162.50.99:8081`)。
- develop = `190e4f9`,领先 release 55 个提交(K22–K27 全部 UI/播放器/发现页改动)。
**口径**
1. 在 `release/v1.0.0` 上执行 `git merge --no-ff develop`,冲突按以下原则解决:
- `lib/core/config/app_config.dart`:**地址取 release**(`devHostBaseUrl` 与 `lanBaseUrl` 均为 `http://39.162.50.99:8081`),保留 release 的 `probeAndApplyBaseUrl()`;结构/其余方法取 develop。
- `lib/main.dart`:保留 `await AppConfig.probeAndApplyBaseUrl();`。
- `android/app/src/main/AndroidManifest.xml`:保留 `android:usesCleartextTraffic="true"`。
- `lib/core/config/server_endpoint.dart`:采用 develop 的 `SharedPreferencesAsync` 版本。
- `pubspec.yaml` / `pubspec.lock`:采用 develop 的依赖集合(`shared_preferences ^2.5.5`、`shared_preferences_platform_interface` dev 依赖、foundation override),不要保留 release 的低版本降级。
- `lib/core/data/services/auth_session_store.dart`:采用 develop 版本。
- 其余 K22–K27 的 UI/播放器/发现页/测试改动全部保留 develop 版本。
2. 解决冲突后执行 `flutter pub get`,同步把 `test/config/server_endpoint_test.dart` 的地址断言改为 `http://39.162.50.99:8081`(保留 develop 的异步 SharedPreferences mock)。
3. 门禁:`dart analyze lib test` 零告警 + `flutter test` 全过 + `flutter build apk --debug`。
4. 合并提交后 push `release/v1.0.0`;作者 `Zhou Weidong <502333560@qq.com>`;禁 `git add -A`;PROGRESS 留痕(含冲突清单与门禁原始输出)。
5. 真机验证由用户执行。
---
## [待处理] 2026-10-19 | K27.15 客户端默认 API 地址改回 192.168.31.187:8081
**用户指令(2026-10-19,原文)**:
> 「`http://192.168.31.187:8081` 改成这个」
**口径**
1. `lib/core/config/app_config.dart` 的 `devHostBaseUrl` 改为 `http://192.168.31.187:8081`。
2. 地址优先级与模拟器/桌面环回逻辑不变。
3. 同步更新 `test/config/server_endpoint_test.dart` 中断言旧地址的用例。
**测试**:`dart analyze lib test` 零告警 + `flutter test` 全过 + `flutter build apk --debug`。
**流程**:从最新 `develop` 拉 `chore/flutter-api-host-3`;`--no-ff` 合回 develop 并 push;作者 `Zhou Weidong <502333560@qq.com>`;PROGRESS 留痕;真机验收由用户执行。
---
## [待处理] 2026-10-19 | K27.14 客户端默认 API 地址改为 172.18.0.7:8081
**用户指令(2026-10-19,原文)**:
> 「改成这个 172.18.0.7:8081」
**口径**
1. `lib/core/config/app_config.dart` 的 `devHostBaseUrl` 改为 `http://172.18.0.7:8081`。
2. 地址优先级与模拟器/桌面环回逻辑不变。
3. 同步更新 `test/config/server_endpoint_test.dart` 中断言旧地址的用例。
**测试**:`dart analyze lib test` 零告警 + `flutter test` 全过 + `flutter build apk --debug`。
**流程**:从最新 `develop` 拉 `chore/flutter-api-host-2`;`--no-ff` 合回 develop 并 push;作者 `Zhou Weidong <502333560@qq.com>`;PROGRESS 留痕;真机验收由用户执行。
---
## [待处理] 2026-10-19 | K27.13 客户端默认 API 地址改为 192.168.31.250:8081
**用户指令(2026-10-19,原文)**:
> 「现在把客户端的api 地址改为 192.168.31250:8081」
**说明**:原文 `192.168.31250` 不是合法 IPv4,按 **`192.168.31.250`** 处理。
**口径**
1. `lib/core/config/app_config.dart` 的 `devHostBaseUrl` 改为 `http://192.168.31.250:8081`,注释保持原文。
2. 其余优先级不变:运行时设置 > `--dart-define=API_BASE_URL` > 平台默认值;模拟器/桌面仍走环回。
3. 不改其他代码;如有测试断言旧地址,一并更新。
**测试**:`dart analyze lib test` 零告警 + `flutter test` 全过 + `flutter build apk --debug`。
**流程**:从最新 `develop` 拉 `chore/flutter-api-host`;`--no-ff` 合回 develop 并 push;作者 `Zhou Weidong <502333560@qq.com>`;PROGRESS 留痕;真机验收由用户执行。
---
## [待处理] 2026-10-19 | K27.12 发现页封面按原图比例铺满 + 下拉刷新
**用户指令(2026-10-19,原文)**:
> 「不是这样 根据封面尺寸全遮住啊 然后发现页加一个下拉刷新」
**口径**
1. 封面按原图比例铺满(K27.10 的 `contain` 留白不算):
- 现有封面源图比例不一(约 1220×2712、1520×2720、1080×1413),固定 120 高无法既铺满又不裁剪。
- 改为**两列瀑布流**:每张卡宽度固定为列宽,封面高度由图片原始宽高比决定(`fitWidth`,不裁剪、不留黑边),剧名/题材仍在封面下方。
- 封面加载中/失败用 3:4 占位,避免高度为 0;「免费」角标仍叠加在封面左上角;封面不再叠加剧名与渐变。
- 保留 `home-all-dramas-section` / `home-all-dramas-grid` / `home-drama-card-<id>` / `home-drama-cover-<id>` 等既有 Key 语义(容器 Key 不变)。
- 两列分配按奇偶分列实现(无第三方依赖);列间距 10、页面左右 14、卡片间距 10 不变。
2. 下拉刷新:
- 发现页 `CustomScrollView` 外包 `RefreshIndicator`(`AlwaysScrollableScrollPhysics`),主色指示器。
- `onRefresh` 同时执行 `categories.load()` 与 `logic.refreshResume()`,完成后收起;失败仍保留现有错误态/空态。
3. 不改搜索、分类选择、快捷入口与底部导航逻辑。
**测试**
1. 封面图片节点不再使用裁剪模式(`fitWidth` 或等比铺满),且没有固定高度导致留白。
2. 下拉手势触发 `onRefresh`:`CategoryController.load` 与 `refreshResume` 被调用(可用假 repository 统计次数),指示器收起。
3. 卡片 Key 与点击进入详情行为不变。
4. 既有测试全过。
**流程**:从最新 `develop` 拉 `fix/flutter-discover-masonry-refresh`;门禁 `dart analyze lib test` 零告警 + `flutter test` 全过 + `flutter build apk --debug`;`--no-ff` 合回 develop 并 push;作者 `Zhou Weidong <502333560@qq.com>`;PROGRESS 留痕;真机验收由用户执行。
---
## [待处理] 2026-10-19 | K27.11 发现页搜索框:点击页面其他区域失焦
**用户指令(2026-10-19,原文)**:
> 「点击页面搜索输入框失去焦点」
**口径**
1. `discover_view.dart` 的搜索 `TextField`(Key `discover-search`)增加 `onTapOutside`:点击输入框以外的任意页面区域时调用 `logic.searchFocus.unfocus()`,收起键盘、取消焦点。
2. 点击输入框本身仍保持/获取焦点,不影响输入与搜索过滤;点击分类 chips、卡片、按钮等仍执行原有动作(失焦不吞点击)。
3. 不改搜索逻辑、不清空输入内容、不改变界面布局。
**测试**
1. 点击 `discover-search` 使其获得焦点;
2. 点击页面其他区域(如顶栏标题或分类 chip)后,`logic.searchFocus.hasFocus == false`(或输入框不再处于 focused 状态);
3. 输入内容与过滤结果不受影响;
4. 既有测试全过。
**流程**:从最新 `develop` 拉 `fix/flutter-discover-search-unfocus`;门禁 `dart analyze lib test` 零告警 + `flutter test` 全过 + `flutter build apk --debug`;`--no-ff` 合回 develop 并 push;作者 `Zhou Weidong <502333560@qq.com>`;PROGRESS 留痕;真机验收由用户执行。
---
## [待处理] 2026-10-19 | K27.10 发现页短剧卡片:封面不裁剪、图上不显示剧名
**用户指令(2026-10-19,原文)**:
> 「现在改发现页面的短剧列表 要求图片不做切割 图片上不显示剧名」
**口径**
1. `discover_view.dart` 的 `_HomeDramaCard` 封面:
- `RemoteImage` 的 `fit` 从 `BoxFit.cover` 改为 `BoxFit.contain`,图片完整显示不裁剪;封面区域加中性底色(如 `AppColors.card`),避免留白突兀。
- 封面尺寸(高 120、圆角 10)与卡片网格尺寸保持不变。
2. 删除封面上的剧名:移除盖在图片上的 `Positioned` 标题 `Text`,以及只为该标题服务的底部黑色渐变遮罩。
3. 保留左上角「免费」角标;图片下方的剧名/题材两行照旧显示(`home-drama-title-<id>` 仍存在且只有一处)。
4. 不改变卡片点击、网格列数与间距。
**测试**
1. 封面内不再出现剧名文本(`find.descendant(of: cover, matching: find.text(title))` 为空)。
2. 封面 `RemoteImage.fit == BoxFit.contain`。
3. 图片下方 `home-drama-title-<id>` 仍存在且只有一个;「免费」角标逻辑不变。
4. 既有测试全过。
**流程**:从最新 `develop` 拉 `fix/flutter-discover-cover-contain`;门禁 `dart analyze lib test` 零告警 + `flutter test` 全过 + `flutter build apk --debug`;`--no-ff` 合回 develop 并 push;作者 `Zhou Weidong <502333560@qq.com>`;PROGRESS 留痕;真机验收由用户执行。
---
## [待处理] 2026-10-19 | K27.9 发现页顶栏固定(与我的页面一致)
**用户指令(2026-10-19,原文)**:
> 「发现页上方的怎么和我的页面不一样 不是固定的」
**问题定位(Trae 勘察)**
- `discover_view.dart` 的 `_header` 被放在 `CustomScrollView` 的 `SliverToBoxAdapter` 里,滚动时会跟随内容消失。
- `mine_view.dart` 的 `_topBar` 在 `List(ListView)` 之外,固定不滚动。
**口径**
1. 把发现页 `_header`(logo + 「发现」标题 + 搜索按钮 + 头像)从 `CustomScrollView` 的 slivers 中移出,放在 `_page` 的 `Column` 顶部、`Expanded(_feed(...))` 之前,实现固定顶栏。
2. 顶栏尺寸与我的页面统一:`SizedBox(height: 46)` + `Padding(horizontal: 14)`,内部布局与样式沿用现有 `_header`。
3. 搜索框、分类 chips、继续观看、快捷入口等内容仍可滚动,不改变顺序与间距。
4. 底部导航行为不变。
**测试**
1. 发现页初始 `discover-search-button` 的位置;
2. 向上拖动内容列表后,`discover-search-button` 的 `dy` 不变,而可滚动内容(如分类 chips / 正在追卡片)位置发生变化;
3. 既有测试全过。
**流程**:从最新 `develop` 拉 `fix/flutter-discover-fixed-header`;门禁 `dart analyze lib test` 零告警 + `flutter test` 全过 + `flutter build apk --debug`;`--no-ff` 合回 develop 并 push;作者 `Zhou Weidong <502333560@qq.com>`;PROGRESS 留痕;真机验收由用户执行。
---
## [待处理] 2026-10-19 | K27.8 首页右侧互动下移 + 倍速移入右上角弹窗 + 拖动进度显示时间
**用户指令(2026-10-19,原文)**:
> 「现在把首页右侧的 喜欢 评论 分享 收藏位置再往下调整下 而且点剧集进去的播放页 倍速是放在 右上角点击的弹窗内 滑动进度条的时候要有时间」
**口径**
1. 首页右侧互动栏(喜欢/评论/分享/收藏)整体下移:`HomeFeedOverlay` 中该栏的 `Align` 从 `Alignment.centerRight` 调整为约 `Alignment(1.0, 0.45)`(或等效的下移),右侧间距保持 12,不得与底部信息卡重叠。
2. 倍速入口从画面右下角移除(删除 `player-speed` 那个 `1.0x` 文本按钮),改放到右上角「更多」弹窗内:
- `_openMore` 弹窗新增 `ListTile`,Key `player-more-speed`,leading `Icons.speed_rounded`,title 为 `l10n.playbackSpeed`,trailing 显示当前倍速(如 `1.0x`)。
- 点击后关闭更多弹窗并打开现有倍速弹窗 `player-speed-sheet`;档位与选中逻辑保持不变,选择后立即生效并更新 trailing。
3. 拖动进度条时显示时间:播放页 `SeekProgressBar` 改为 `showTimes: true`,拖动过程中显示 `当前时间 / 总时长`(复用组件已有的 `mm:ss / mm:ss` 预览),松手后按现有逻辑 seek。
4. 其余已确认规则不变:中心按钮仅暂停时显示、清屏跨集共享、退出清屏按钮、无定时器。
**测试**
1. 首页 `rail-like` 的垂直中心比修改前更低(断言其 center.dy 大于播放器可视区中线上方阈值,或与底部信息卡顶部保持间距)。
2. 播放页不再存在 `player-speed` 主画面入口;`player-more` → `player-more-speed` → `player-speed-sheet` 可打开;选 `1.5x` 后关闭弹窗,再次打开更多弹窗 trailing 显示 `1.5x`。
3. 拖动 `player-progress` 过程中出现形如 `mm:ss / mm:ss` 的时间文本;松手后消失。
4. 既有测试全过。
**流程**:从最新 `develop` 拉 `fix/flutter-feed-rail-speed-time`;门禁 `dart analyze lib test` 零告警 + `flutter test` 全过 + `flutter build apk --debug`;`--no-ff` 合回 develop 并 push;作者 `Zhou Weidong <502333560@qq.com>`;PROGRESS 留痕;真机验收由用户执行。
---
## [待处理] 2026-10-19 | K27.7 倍速选择改为弹窗
**用户指令(2026-10-19,原文)**:
> 「倍速的要写在弹窗内」
**口径**
1. `_PlaybackOverlay` 画面右下角常驻的静态 `1.0x` 文本改为可点击的倍速入口(Key `player-speed`,文案显示当前倍速,如 `1.0x`)。
2. 点击弹出倍速弹窗(底部 sheet,Key `player-speed-sheet`),选项:`0.5x / 0.75x / 1.0x / 1.25x / 1.5x / 2.0x`,每项 Key `player-speed-<值>`(如 `player-speed-1.5`);当前项用主色高亮。
3. 选择后立即 `controller.setPlaybackSpeed(value)` 并关闭弹窗,入口文案同步更新。
4. 倍速状态由 `_PlayerPageState` 统一持有:切换剧集后新控制器自动沿用当前倍速;离开播放页后恢复默认 1.0x。
5. 清屏态不显示倍速入口(与顶栏/进度条一起隐藏);普通态与暂停态显示。
6. l10n 新增 `playbackSpeed`:en `Playback speed` / zh `倍速` / id `Kecepatan putar`;三语齐全。
**测试**
1. 点 `player-speed` 出现 `player-speed-sheet`,包含 6 个档位。
2. 选 `player-speed-1.5` 后弹窗关闭,入口显示 `1.5x`。
3. 清屏后倍速入口隐藏。
4. 切集后倍速入口仍显示所选值(若实现层可测)。
5. 既有测试全过。
**流程**:从最新 `develop` 拉 `fix/flutter-speed-sheet`;门禁 `dart analyze lib test` 零告警 + `flutter test` 全过 + `flutter build apk --debug`;`--no-ff` 合回 develop 并 push;作者 `Zhou Weidong <502333560@qq.com>`;PROGRESS 留痕;真机验收由用户执行。
---
## [待处理] 2026-10-19 | K27.6 首页集数文案左贴 + 清屏状态跨集共享
**用户指令(2026-10-19,原文)**:
> 「首页的底部的卡片 第n集/共n集 要紧挨着左侧啊 还有 选集进去播放页面还是 点击清屏后 再次滑动 还不是清屏状态」
**问题定位(Trae 勘察)**
1. `home_feed_overlay.dart` 的 `_infoCard` 用 `Row` + `Expanded` + 右对齐 `Flexible`,把「第 n 集 / 共 n 集」顶到了卡片最右侧。
2. 选集进入的播放页(`homeMode=false`)是 `PlaybackQueue.series`,上下滑动按集切换;每集各自渲染一个 `_PlaybackOverlay`,而 `_controlsVisible` 是 `_PlaybackOverlayState` 的局部状态,所以清屏只作用于当前集,滑到下一集又恢复控件。
**口径**
1. 首页底部卡片:剧集类型标签与「第 n 集 / 共 n 集」放在同一个 `Wrap`(spacing 6、runSpacing 4、交叉轴居中),集数文案紧跟标签左侧排列,**不再右对齐/推到最右**;标签过长时允许整体换行,不得溢出。
2. 清屏状态提升为播放页级别:把 `_controlsVisible` 从 `_PlaybackOverlayState` 移到 `_PlayerPageState`,与首页 `_feedChromeVisible` 合并为同一个 `_chromeVisible`(或等价命名)。
- `_PlaybackOverlay` 改为无内部显隐状态,接收 `controlsVisible`、`onClear`、`onTogglePlay` 等参数。
- 播放页清屏后,上下滑动切集 / 切剧仍保持清屏;退出清屏按钮恢复后所有页面同步显示控件。
- 首页与选集两种入口共用同一状态来源。
3. 已确认的规则不变:播放中不显示中心按钮、暂停时显示播放按钮、清屏点画面切播放、右上角退出清屏、无定时器。
**测试**
1. 首页 `home-feed-episode` 的左边缘紧贴标签区(与 `home-feed-info-card` 左内边距接近,不得落在卡片右半区)。
2. 选集进入播放页 → 点 `player-clear-screen` → 上滑切到下一集 → 仍无 `player-controls-top` / `player-controls-bottom`,`player-exit-clear-screen` 仍在;点退出后恢复。
3. 首页清屏后滑动切剧仍保持清屏(回归)。
4. 中心按钮仅暂停时显示(回归)。
5. 既有测试全过。
**流程**:从最新 `develop` 拉 `fix/flutter-clear-screen-shared`;门禁 `dart analyze lib test` 零告警 + `flutter test` 全过 + `flutter build apk --debug`;`--no-ff` 合回 develop 并 push;作者 `Zhou Weidong <502333560@qq.com>`;PROGRESS 留痕;真机验收由用户执行。
---
## [待处理] 2026-10-19 | K27.5 中心按钮只在暂停时显示(播放状态一律不显示)
**用户指令(2026-10-19,原文)**:
> 「都说了播放状态不显示啊」
**口径(最终,覆盖 K27.4 第 2 条)**
1. **播放中:不显示任何中心按钮**(无论是否清屏、无论首页还是播放页)。
2. **暂停中:显示中心播放按钮**(`PlayerCenterToggle(playing: false)`,图标为播放);点击它或点画面恢复播放,播放后按钮立即消失。
3. 播放页:中心按钮单独渲染在 `_controlsVisible` 控制层块之外,保证**清屏 + 暂停时仍能看到播放按钮**;清屏播放中不显示。
4. 首页:`home-feed-center-play` 仅在 `!userWantsPlay` 时渲染,播放中不显示;清屏不影响该规则(清屏 + 暂停 → 显示播放按钮)。
5. K27.4 的其他结论保留:清屏状态跨滑动保持、退出清屏按钮、清屏态点画面切播放、无定时器。
**测试**
1. 播放中(普通态与清屏态):中心按钮都不存在。
2. 点画面暂停后:普通态与清屏态都出现播放按钮;点它恢复播放且按钮消失。
3. 清屏后滑动切剧:仍保持清屏。
4. 退出按钮恢复控制层且不改变播放态。
5. 更新 K27.4 里「两种播放状态都显示中心按钮」的断言;既有测试全过。
**流程**:从最新 `develop` 拉 `fix/flutter-center-toggle-paused-only`;门禁 `dart analyze lib test` 零告警 + `flutter test` 全过 + `flutter build apk --debug`;`--no-ff` 合回 develop 并 push;作者 `Zhou Weidong <502333560@qq.com>`;PROGRESS 留痕;真机验收由用户执行。
---
## [待处理] 2026-10-19 | K27.4 清屏状态跨滑动保持 + 中心按钮常驻(暂停/播放双态)
**用户指令(2026-10-19,原文)**:
> 「我点了清屏后 再次滑动 还是没有清屏 而且清屏后点击播放没显示暂停按钮」
**口径(覆盖 K27.1 / K27.3 的冲突部分)**
1. **清屏状态跨滑动保持**:撤销 K27.3 在 `onPageChanged` 里复位 `_feedChromeVisible` 的逻辑;首页清屏后上下滑动切剧,新页面仍保持清屏(只有顶栏/右侧 rail/底部卡片/遮罩隐藏,见第 2 条中心按钮除外)。
2. **中心按钮常驻双态**:
- 播放中显示暂停图标(`playing: true`);暂停中显示播放图标(`playing: false`);点击即切换播放/暂停。
- 播放页:中心按钮移出 `if (_controlsVisible)` 控制层块,**清屏时仍显示**;清屏仍隐藏顶栏、进度条、三按钮、评论卡、倍速角标。
- 首页:中心按钮不再依赖 `_feedChromeVisible`,清屏时同样显示;`chromeHidden`(PiP)时不显示。
- 清屏态下点画面仍是播放/暂停切换;退出清屏仍只由右上角退出按钮触发。
3. 不引入定时器;顶栏/底部控件仍常驻、不自动隐藏。
**测试**
1. 清屏后滑动切换下一页:仍无 `home-feed-title` / `home-feed-info-card`,退出按钮仍在。
2. 清屏后中心按钮存在:播放中显示暂停图标;点一下 → 暂停并变播放图标;再点 → 播放并变暂停图标。
3. 清屏后点画面:播放/暂停切换且保持清屏。
4. 点退出按钮:除中心按钮外的控制层恢复,播放态不变。
5. 既有测试全过(更新原「滑动复位」「清屏无中心按钮」的断言)。
**流程**:从最新 `develop` 拉 `fix/flutter-clear-screen-state`;门禁 `dart analyze lib test` 零告警 + `flutter test` 全过 + `flutter build apk --debug`;`--no-ff` 合回 develop 并 push;作者 `Zhou Weidong <502333560@qq.com>`;PROGRESS 留痕;真机验收由用户执行。
---
## [待处理] 2026-10-19 | K27.3 清屏态点击画面切播放、退出按钮位置与滑动重置
**用户指令(2026-10-19,原文)**:
> 「不是啊 点击完清屏 我在点击屏幕如果是播放中应该是暂停啊 而且首页点完清屏 退出清屏的位置不对 如果点击了清屏 在滑动切换也是清屏状态」
**口径**
1. 清屏态点击画面 = 播放/暂停切换,**保持清屏**:
- 播放页 `_onVideoTap`:无论控制层是否可见,都调用 `widget.onTogglePlay()`;删除「清屏态点画面只恢复控制层」的分支。
- 首页清屏态:`home-feed-video-tap` 改为播放/暂停切换,保持清屏。
- 退出清屏只由右上角 `player-exit-clear-screen` / `home-feed-exit-clear-screen` 触发,点击退出按钮**不改变播放态**。
2. 首页退出清屏按钮位置修正:放入 `SafeArea`(顶部安全区内 8、右侧 12),36×36,与首页顶栏按钮视觉对齐;不得压在状态栏/刘海下面。
3. 首页滑动切换剧目时重置清屏:`PageView.onPageChanged` 中把 `_feedChromeVisible` 复位为 true(仅 homeMode),新一页进入时展示控制层。
4. 不引入定时器;控制层仍常驻;已完成的中心按钮口径(仅暂停时显示播放按钮)不变。
**测试**
1. 清屏后点击画面:播放 → 暂停,顶栏/底部仍隐藏,退出按钮仍在;再点 → 播放,仍清屏。
2. 点退出按钮:控制层恢复,播放态不变。
3. 首页清屏后用 `PageView` 切换下一页:`home-feed-title` / `home-feed-info-card` 恢复显示。
4. 既有测试全过(更新原先「点画面恢复控制层」的断言)。
**流程**:从最新 `develop` 拉 `fix/flutter-clear-screen-tap`;门禁 `dart analyze lib test` 零告警 + `flutter test` 全过 + `flutter build apk --debug`;`--no-ff` 合回 develop 并 push;作者 `Zhou Weidong <502333560@qq.com>`;PROGRESS 留痕;真机验收由用户执行。
---
## [待处理] 2026-10-19 | K27.2 清屏态增加「退出清屏」浮动按钮
**用户指令(2026-10-19,原文)**:
> 「而且有了清屏自然要有退出清屏按钮啊」
**口径**
- 播放页与首页 feed 进入清屏态后,画面右上角保留一个「退出清屏」浮动按钮,点击恢复控制层。
- 按钮规格:36×36 圆形,底色 `Color(0x80000000)`,图标白色 `Icons.fullscreen_exit_rounded`(size 18),距顶部安全区 8、右侧 12。
- Key:播放页 `player-exit-clear-screen`,首页 `home-feed-exit-clear-screen`。
- tooltip 新增 l10n key `exitClearScreen`,三语齐全:en `Exit clear screen` / zh `退出清屏` / id `Keluar dari layar bersih`。
- 点击只恢复控制层,**不改变播放/暂停状态**;恢复后按钮消失。
- 保留「清屏态点击画面恢复控制层」的现有手势。
- 按钮不自动隐藏,与控制层显示状态严格互斥。
**测试**
1. 点 `player-clear-screen` 后 `player-exit-clear-screen` 出现,控件区仍隐藏。
2. 点 `player-exit-clear-screen` 后顶栏/底部/中心按钮恢复,退出按钮消失,播放态不变。
3. 首页 `home-feed-clear-screen` → `home-feed-exit-clear-screen` 出现;点击后 overlay 恢复、按钮消失。
4. 既有测试全过。
**流程**:可从 `develop` 拉 `fix/flutter-exit-clear-screen`;门禁 `dart analyze lib test` 零告警 + `flutter test` 全过 + `flutter build apk --debug`;`--no-ff` 合回 develop 并 push;作者 `Zhou Weidong <502333560@qq.com>`;PROGRESS 留痕;真机验收由用户执行。
---
## [待处理] 2026-10-19 | K27.1 中心按钮改口径:播放中不显示,仅暂停时显示播放按钮
**用户指令(2026-10-19,原文)**:
> 「播放按钮不用显示啊 就点击暂停的时候显示」
**口径(覆盖 K27 第三节,其余不变)**
- 播放中:**不显示任何中心按钮**;点击画面 = 暂停(点一下暂停)。
- 暂停后:中心显示播放按钮(Key 沿用 `player-play-toggle`,`playing: false`),点击或再点画面 = 播放;播放开始后按钮立即消失。
- 首页沉浸 feed 同步:暂停时显示中心播放按钮 `home-feed-center-play`,播放中不显示;点画面仍是播放/暂停切换。
- 清屏状态下中心按钮一并隐藏;点画面先恢复控制层,不改变播放态。
- 不引入任何定时器;控制层仍然常驻。
**实现要点**
1. `_PlaybackOverlay`:把 `if (widget.playing) PlayerCenterToggle(playing: true)` 改为 `if (!widget.playing) PlayerCenterToggle(playing: false, onTap: widget.onTogglePlay)`。
2. `_onVideoTap`:控制层可见时直接 `widget.onTogglePlay()`;清屏时只恢复控制层。
3. `player_view.dart` 首页分支:恢复暂停态中心播放按钮(`home-feed-center-play` → `setUserWantsPlay(true)`),播放态不渲染;PiP/清屏时不渲染。
4. 测试更新:
- 播放中 `player-play-toggle` 不存在;点画面后视频暂停且按钮出现;点按钮后恢复播放且按钮消失。
- 首页暂停后 `home-feed-center-play` 出现,播放后消失。
- 清屏隐藏中心按钮,点画面恢复控制层。
- `pump` 5 秒控件仍在。
5. 门禁/分支/提交规范同 K27:从 `develop` 拉 `fix/flutter-paused-play-toggle`,`dart analyze lib test` 零告警 + `flutter test` 全过 + `flutter build apk --debug`,`--no-ff` 合回 develop 并 push,作者 `Zhou Weidong <502333560@qq.com>`,PROGRESS 留痕;真机验收由用户执行。
---
## [已完成·待用户验收] 2026-10-19 | K27 播放器控制层交互重做:清屏按钮 + 取消自动隐藏 + 暂停按钮即时消失
**用户指令(2026-10-19,原文)**:
> 「现在播放页不对啊 不是点击播放或暂停 等一会所有的按钮都消失了 而是要要清屏按钮 点了清屏其他的才消失 点击暂停是点击一下 暂停 在点击下播放 暂停按钮消失这样」
**现状问题(Trae 勘察)**
- `player_view.dart` 的 `_PlaybackOverlayState` 有 `Timer? _hideTimer` + `_scheduleHide()`,2.5 秒后把 `_controlsVisible` 置 false,属于「等一会全部消失」。
- 首页 `player_view.dart` 中 `shouldShowFeedPauseIndicator` 分支会在暂停后常驻一个中心播放按钮(`home-feed-center-play`)。
- 播放页中心按钮 `player-play-toggle` 常驻,暂停后也不消失。
- 首页 `HomeFeedOverlay` 没有清屏按钮;播放页顶栏只有返回 / 标题 / 更多。
### 一、控制层常驻(删除一切定时隐藏)
1. `_PlaybackOverlayState` 删除 `Timer? _hideTimer`、`_scheduleHide()`、`initState/dispose` 中的定时器逻辑;`import 'dart:async'` 若无其它用途一并删除。
2. `_controlsVisible` 只由用户显式动作改变:点「清屏」→ 隐藏;清屏状态下点画面 → 恢复。**不得有任何延时自动隐藏**。
3. 进入播放页、切集、选集返回、PiP 返回时,控制层默认可见。
### 二、清屏按钮
**播放页(`_PlaybackOverlay`)**
1. 顶栏右侧、`player-more` 左侧新增清屏按钮,复用 `_TopAction`(36×36),Key `player-clear-screen`,图标 `Icons.fullscreen_rounded`。
2. 点击后隐藏:顶栏(`player-controls-top`)、中心按钮、进度条、三按钮、评论卡、`1.0x` 角标;画面上只剩视频。
3. 清屏状态下点击画面任意处 → 恢复控制层,**不切换播放/暂停**。
4. tooltip 走 l10n 新增 key `clearScreen`,en/zh/id 三语必须齐全(en `Clear screen` / zh `清屏` / id `Bersihkan layar`)。
**首页沉浸 feed**
1. `HomeFeedOverlay` 顶栏在「搜索」与「更多」之间新增清屏按钮,Key `home-feed-clear-screen`,36×36 圆钮,样式复用 `_RoundIcon`。
2. 点击后隐藏整个 HomeFeedOverlay(剧名行、右侧 rail、底部玻璃卡)、首页暂停提示与渐变遮罩,只留视频。
3. 该显隐状态由 `player_view.dart` 持有(例如 `_feedChromeVisible`),清屏后点画面恢复,且不切换播放状态。
### 三、中心播放/暂停按钮
1. 播放中且控制层可见 → 中心显示暂停按钮(`PlayerCenterToggle(playing: true)`,Key `player-play-toggle`)。
2. 点击中心暂停按钮 → 执行暂停,并且**该按钮立即消失**;不显示常驻的播放按钮。
3. 暂停中点击画面 → 播放;恢复播放后中心暂停按钮随控制层恢复显示。
4. 删除首页暂停后的常驻播放按钮分支(`shouldShowFeedPauseIndicator` / `home-feed-center-play`)。若 helper 及测试因此失效,一并删除并更新测试,不留死代码。
5. 首页与播放页必须共用 `PlayerCenterToggle`,样式不得分叉。
### 四、保持不变
- 票据三态、进度条 seek、选集、评论、下一集、下载授权、PiP、倍数角标等既有逻辑不改。
- 不新增第三方依赖;不改后端、不改 Web。
### 五、测试(先红后绿)
更新/新增 widget test,至少覆盖:
1. 播放页初始控制层可见;`pump` 5 秒后 `player-controls-top` / `player-controls-bottom` 仍然存在(证明无自动隐藏)。
2. 点 `player-clear-screen` → 顶栏、底部控制区、中心按钮都消失;点画面恢复,且播放态不变。
3. 播放中中心暂停按钮存在;点击后播放态变为暂停且中心按钮消失;再点画面恢复播放。
4. 首页点 `home-feed-clear-screen` → `home-feed-title`、`home-feed-info-card` 消失;点画面恢复。
5. 既有全部测试通过(含 `player_fullscreen_test.dart`、`player_center_toggle_test.dart`)。
### 六、门禁与流程
- 从 `develop` 拉 `fix/flutter-player-clear-screen`。
- 门禁:`dart analyze lib test` 零告警、`flutter test` 全过、`flutter build apk --debug`。
- 完成后 `git checkout develop && git merge --no-ff` 并 push;提交身份 `Zhou Weidong <502333560@qq.com>`;禁用 `git add -A`。
- 本批只做代码改造与本地门禁;**真机验证由用户执行**,Codex 不得自行真机验证,回执里给出用户可照做的验证步骤与每步预期结果。
- 在 `docs/plans/PROGRESS.md` 追加留痕(命令 + 原始输出 + 解读)。
---
## [已完成·待用户验收] 2026-10-19 | K26 客户端按「移动端设计预览.html」校正六屏
**用户指令(2026-10-19)**:「现在看 移动端设计预览.html 按这个来」。
> 新增权威设计文件(本地已同步):
> `/Users/jianghaifeng/Desktop/ReelShort/docs/移动端设计预览.html`
> 该文件优先于之前的 Stitch screens;颜色/布局/组件尺寸以它为准。
> 保留 K24 已接入的 Outfit/Plus Jakarta Sans 字体,其余按预览校正。
### 一、全局 Token(按预览)
- 主色 `#FF5B3D`、强调 `#9278EA`、背景 `#0B0C10`、底栏 `#0D0D11`、
卡片 `#1B1C21` / `#1C1D21`、输入 `#25252A`、正文 `#E6E1E3`、次要 `#D2CDD0`、浅橙 `#FFA490`。
- 圆角:卡片 10–14、按钮 9、chips 20、封面 10;不再统一全圆。
- 字体沿用 Outfit(标题)/ Plus Jakarta Sans(正文);字号按预览 9/10/11/12/13/14/15/18/20。
- 更新 `AppColors`/`AppDimens`/`GlassPanel` 默认值;保留现有语义名。
### 二、首页(预览 01)
1. 顶部:剧名 + 右侧搜索/菜单 28px 圆钮;不再显示 HOT/Season/剧集头像 Follow。
2. 右侧互动:Like / Comment / Share / Favorite 四项,22px 图标 + 9px 文案/计数;
下载/PiP 继续收进“更多”。
3. 底部玻璃卡(`rgba(15,15,20,.72)`、blur 6、radius 14、padding 11/12):
- 题材 tags(都市/甜宠,8px)
- `第 x 集 / 共 y 集`(9px)
- 剧集标题(13px 800)
- 两个等宽 34px 按钮:`▶ 追剧`(橙色)/ `选集`(#2a2a30)
4. 底部 3 Tab 文案与图标按预览(首页/发现/我的)。
### 三、发现页(预览 02)
1. 顶部:Logo + `发现` + 搜索圆钮 + 头像。
2. 搜索框 36px、`#25252A`、全圆、左侧搜索图标。
3. 分类 chips:推荐/悬疑/恋爱/动作,选中橙底白字。
4. Continue Watching 卡:`#1C1D21`、radius 12、52×66 封面、标题/集数/3px 进度、右侧 56×28 橙色“继续”。
5. 4 个快捷入口(筛选/排行榜/新剧/收藏):`#202126`、radius 12、高 62、16px 图标 + 9px 文案。
6. `热门短剧` 标题带 3×18 橙色竖条 + 右侧 `全部 ›`;2 列网格,封面高 120、radius 10、
左上“免费”角标、底部渐变标题、下方 11px 标题 + 9px 题材。
7. 移除预览中没有的额外区块(轮播/多余 banner)。
### 四、详情页(预览 03)
1. Hero 高 190、圆角 0、左上 28px 圆形返回;不再放分享/收藏浮层。
2. 内容:`N 集 · 已完结`(紫色 9px,完结无数据时只显示 N 集)、标题 18px、简介 11px / 1.6。
3. `▶ 开始观看` 40px 橙色按钮。
4. `选集` 标题 + 横向/换行集数 chips(1 起,选中橙底白字,锁定紫色)。
5. 简化 K25 的统计/CTA 行/剧集预览卡;保留播放、收藏、下载逻辑(收藏/下载可放“更多”)。
6. 不加底部 Tab。
### 五、播放器(预览 04 + 用户要求全屏)
1. 全屏播放(用户已明确);顶部 46px:28px 返回 + 居中 `剧名 · 第 x 集` + 28px 更多。
2. 视频区:中心 52px 播放圆钮;右下 `1.0x` 倍速文字;无右侧互动栏。
3. 进度条 3px、`#FF5B3D` 播放段、`#474248` 轨道。
4. 三按钮行:`选集` / `评论` / `下一集`,高 32、radius 9,下一集橙色,末集置灰。
5. 一条精选评论卡:`#1B1C21`、radius 10、头像/昵称/时间/正文;无评论隐藏。
6. 下载/PiP 收进更多;保留票据、自动连播、进度、互动逻辑。
### 六、我的页(预览 05)
1. 顶部:Logo + `我的` + 搜索圆钮 + 头像。
2. 用户行:48px 头像、昵称、欢迎语、右侧“登录/注册”chip(已登录显示账号信息)。
3. 金币卡:`#201B1E`、radius 13、金币图标、我的金币、`0 金币 · 去充值`、右侧 `›`。
4. Continue Watching 卡:`#1B1C21`、radius 12、50×64 封面、标题、第 x 集 · 时间、橙色“继续”。
5. Tabs:历史(橙色)/ 收藏 / 喜欢;一条历史卡(48×60 封面、标题、集数/时间、播放圆钮)。
6. 语言行:`#1B1C21`、radius 12、语言 + 当前值(橙色)。
7. 底部 3 Tab;原 My List 区块保留在 Me 页内部。
### 七、登录页(预览 06)
1. 居中 64px 渐变 Logo(`#FF5B3D → #9278EA`,radius 18)+ `水果短剧` 20px + 副标题 11px。
2. 三个 44px 入口:Google(白底)、手机号(`#25252A`)、邮箱(`#25252A`);
Google/手机号后端未就绪时置灰并提示“即将开放”;邮箱进入现有邮箱表单。
3. 底部 9px 法律文案:继续即代表同意《用户协议》与《隐私政策》。
4. 保留 K21 邮箱表单作为二级页,不改认证逻辑。
### 八、数据与测试
- 预览中的 12.4k、3.2k、已完结、VIP 等:有真实数据才显示,否则隐藏,不伪造。
- 更新首页/发现/详情/播放器/我的/登录相关测试;保留播放、进度、收藏、下载、PiP 行为断言。
- 新增:预览布局关键 Key、3 Tab、搜索、Continue Watching、选集 chips、全屏播放器三按钮、登录入口。
- `dart analyze lib test` 零告警;`flutter test` 全过;`flutter build apk --debug` 通过。
### 九、Git
- 从干净 `develop`(基线 `eefabb6`)拉 `feature/flutter-k26-mobile-preview`,小步提交;
`--no-ff` 合并回 develop 并 push;作者 `Zhou Weidong <502333560@qq.com>`;不动 main。
- 计划文档 `FruitServer/docs/plans/2026-10-19-K26.md`;留痕写 `PROGRESS.md`。
- 回执:六屏改动、token、测试条数与门禁原始输出、数据缺口、真机验收步骤。
---
## [待处理] 2026-10-19 | K25 客户端详情页 + 播放器对齐设计稿
**用户指令(2026-10-19)**:「开始实现详情页和播放器」。
> 设计权威:`docs/design/screens/03-detail.html`、`04-player.html`(origin/main 或 `/tmp/specs-design/docs/design/screens/`)。
> 设计系统沿用 K24 的 Obsidian Flare token、Outfit/Plus Jakarta Sans、GlassPanel;不改后端接口/模型字段,缺失数据不伪造。
### 一、详情页(`03-detail.html`)
1. **Hero**:16:9 横版头图(优先 `drama.coverUrl`,无则 `posterUrl`),底部渐变;
左上玻璃返回、右上玻璃分享/收藏;图上叠加 `Trailer 0:45`、`4K UHD` 玻璃 pill(无数据时只显示 4K/隐藏)。
2. **元数据**:集数 pill(`N Episodes`)、完结/榜单名次(模型无字段则隐藏)、标题 24px、
`Season 1 • DramaPulse Original Release`、播放量(`viewCount`)、题材标签;评分无字段则隐藏。
3. **简介卡**:`#12141A`/GlassPanel 圆角 16、正文 14px、默认 2 行 + `More` 展开;保留现有 l10n 文案。
4. **CTA 行**:`Start Watching`(或继续观看集数)橙色全宽胶囊 + 48×48 收藏 + 48×48 下载;
举报入口收进右上“更多”;保留现有 Favorite/Download/Report 逻辑。
5. **Episodes 区**:标题 `Episodes (N)` + 范围 chips(如 `1-30`,无数据可只显示全部);
横向集数 chips(46×44、选中橙色、锁定紫色);下方 2 列剧集预览卡(96×64 缩略图、Free/Coin、
时长、锁态);保留现有选集跳转与下载入口。
6. 详情页为 push 页面,不加底部 Tab;加载/失败/空态保留返回按钮(现状已有)。
### 二、播放器(`04-player.html`)
1. **模式拆分**:
- `homeMode=true`(首页沉浸 feed)保持现状全屏竖滑;
- `homeMode=false`(从详情进入)改为设计稿的“卡片式播放器”:
顶部导航(返回、居中剧名 + `Ep.x`、更多)、9:10 圆角 16 视频框、下方进度与操作区、评论卡。
2. **顶部/视频框**:`Auto-Next On` 紫色 pill、`1.0x` 倍速 pill、中心橙色播放按钮、
右侧 36px 点赞/全屏;场景标签(无数据隐藏)。
3. **进度条**:1.5px 轨、缓冲段 white/30、播放段 `#FF5B3D`、白色 thumb;
左侧当前时间、中间 `Ep x / N`、右侧总时长;保留拖动 seek。
4. **三按钮行**:`Episodes` 玻璃胶囊、`Comments (count)` 玻璃胶囊、`Next Ep` 橙色胶囊;
分别打开现有选集、评论弹窗、下一集;没有下一集时置灰。
5. **精选评论卡**:`#14161E` 16px 圆角、VIP 标记(有数据才显示)、点赞数、`View all N`;
复用 `CommentController`;无评论时隐藏。
6. 保留现有播放票据、自动连播、PiP、下载、互动逻辑;下载/PiP 收进顶部“更多”。
7. 不要改 `playback_session`/ticket 协议;只改 UI 组织与控件。
### 三、数据缺口(不得伪造)
- 评分、完结状态、Top 排名、VIP、剧级点赞/评论计数、场景标签:模型/接口没有就隐藏;
- `viewCount`、集数、题材、`Episode.locked`、`freeEpisodes/unlockCoin` 已有,按现有字段渲染。
### 四、测试与门禁
- 更新详情/播放器相关 widget 测试;保留 PiP、进度、票据错误、收藏/下载等既有行为断言;
- 新增:16:9 hero、CTA 行、剧集 chips、播放器卡片模式、三按钮行、进度条颜色;
- `dart analyze lib test` 零告警;`flutter test` 全过;`flutter build apk --debug` 通过。
- 真机验收由用户执行;不伪造设备侧证据。
### 五、Git 与回执
- 从干净 `develop`(基线 `4553606`)拉 `feature/flutter-k25-detail-player`,小步提交;
完成后 `--no-ff` 合并回 develop 并 push;作者 `Zhou Weidong <502333560@qq.com>`;不动 main。
- 计划文档 `FruitServer/docs/plans/2026-10-19-K25.md`;留痕写 `PROGRESS.md`。
- 回执:详情/播放器结构、保留/新增的 Key、测试条数与门禁原始输出、数据缺口、真机验收步骤。
---
## [待处理] 2026-10-19 | K24 客户端 UI 对齐设计稿(设计系统 + 首页 + 发现)
**用户指令(2026-10-19)**:「修改客户端 UI,读 specs 下 docs 下的 screens」。
> 设计权威文件(已全部读取):
> `docs/design/DESIGN.md`、`docs/design/README.md`、`docs/design/screens/01-home.html`、`02-discover.html`
> (以及 03-detail / 04-player / 05-me / 06-login,后三屏留到 K25/K26)。
> **口径冲突以 README + screens HTML 为准**:`#0B0C10 / #16181F / #FF5B3D / #9278EA`;
> `DESIGN.md` 顶部 YAML 的 `#ffb4a5/#cdbdff` 是自动生成色板,不采用。
### 一、本批范围
只做「设计系统 + 底部导航 + 首页 + 发现页」。详情页、播放器、我的页、登录页留到 K25/K26。
不改后端接口、Web、specs;不改业务数据模型字段;缺失的评分/完结/榜单名次不要伪造。
### 二、设计 Token(全局)
1. `lib/core/theme/app_colors.dart` 改为 Obsidian Flare:
- background `#0B0C10`、card `#16181F`、surface-elevated `#1D1E28`、bar `#0B0C10`(95% + blur)
- primary `#FF5B3D`、pressed `#FF4321`、secondary/lavender `#9278EA`
- gold `#FBBF24`、danger/free `#FF5B3D`、success `#34D399`
- on-surface `#FFFFFF` / `#E3E2E8`、muted `#A1A1AA`、subtle `#71717A`
- border `rgba(255,255,255,0.08)`、glass 高亮 `rgba(255,255,255,0.12-0.14)`
- 保留现有语义命名(`AppColors.blue` 等)以免大面积改名,但值统一到新色板;新增 `secondary`、`lavender`、`onSurface`。
2. `app_theme.dart`:主色/次色/背景/卡片/描边/进度条统一;固定深色;按钮/chip 使用全圆角;
进度条改为 1–2px;补充 `Outfit`(标题)与 `Plus Jakarta Sans`(正文)字体。
3. 字体:下载 Outfit + Plus Jakarta Sans 的 OFL TTF 放入 `assets/fonts/`,`pubspec.yaml` 注册;
禁止运行时依赖网络字体;若下载失败,使用系统字体并在回执标注未完成项。
4. 新增 `lib/core/widgets/glass_panel.dart`(BackdropFilter 16/24px、半透明渐变、白 8–14% 描边、
可选圆角 16/24/26),供首页底部卡、顶部按钮等复用。
5. 间距/圆角:页面 gutter 16、卡片圆角 16–24、按钮/chip 全圆、卡片间距 12–16;补充到 `app_dimens.dart`。
### 三、底部导航(3 Tab)
- 改为 **Home / Discover / Me** 三项,Keys 保留 `tab-home`、`tab-discover`、`tab-my`;
删除 `tab-my-list` 独立 Tab(原 My List 内容并入 Me 页)。
- 选中态:橙色图标 + 橙色文案 + 下方 1px 橙色圆点;未选中 `white/45`。
- 背景 `#0B0C10/95` + 顶部 `white/5` 分隔 + backdrop blur;高度与安全区按设计。
- 修正现有 tab 映射:Home 进沉浸 feed,Discover 进发现,Me 进个人页;
`My List` 入口保留在 Me 页内部(History/Watchlist/Liked)。
- 更新受影响的 widget 测试,不再断言 4 Tab;保留 Home/Discover/Me 的行为断言。
### 四、首页(Home)
按 `01-home.html` 重做首页沉浸层(保留 `PlayerPage(homeMode: true)` 视频 feed):
1. 顶部:状态栏安全区 + 剧名、`HOT` 徽章、`Season 1 • Vertical Cinema` 副标题;右侧搜索/更多 36px 玻璃圆钮。
2. 右侧互动栏:剧集头像 48px + 橙色 Follow `+`;Like / Comment / Bookmark / Share,
44–46px 玻璃圆形按钮 + 11px 计数/文案;保留下载/PiP 时收进“更多”菜单,避免超出设计动作集。
3. 底部玻璃信息卡(26px 圆角、blur 24):
- 题材 chips(`#9278EA/15` 与 `#FF5B3D/15` 半透明底)
- `Ep. x / y` pill
- 2 行简介
- 1px 进度条(播放段 `#FF5B3D`)+ 当前/总时长
- `Follow Series` 橙色胶囊主 CTA + `Episodes` 玻璃胶囊
4. 顶部/右侧/底部渐变 scrim 按设计稿比例;文字加 `0 2px 8px rgba(0,0,0,.75)` 或置于玻璃 pill。
5. 复用现有 Player 逻辑(进度、选集、点赞/收藏/评论),不要重写播放链路。
### 五、发现页(Discover)
按 `02-discover.html` 重做:
1. 顶部品牌行(橙点 + DramaPulse/FruitShorts + 副标题)+ 右侧头像入口。
2. 44px 搜索框(左侧搜索图标、右侧语音占位),接通现有 `DiscoverLogic.search` 过滤;
`searchRequest` 时聚焦。
3. 全圆分类 chips,选中橙色实心(未选中 `white/8`)。
4. Continue Watching 卡:海报 64×88、`Ep. x / y • xx min left`、65% 橙色进度、橙色 Play 按钮;
数据用现有观看记录;没有记录时隐藏该卡。
5. 4 个快捷入口:Filter / Top Charts / New / Watchlist(玻璃卡 + 图标 + 文案)。
6. Trending Dramas:标题 + See All;**2 列**网格,海报比例约 3/4.2、16px 圆角;
封面叠加 Free/VIP/HOT 徽章(有数据才显示)、评分(模型无字段则隐藏)、播放量(用 `viewCount`)。
7. 移除发现页顶层 3D 轮播(设计稿没有);轮播组件保留但不再挂在 Discover;若删除组件需确认无测试引用。
8. 标题 12px、副标题 11px,卡片底色 `#15161E`;与设计一致。
### 六、测试与门禁
- 更新受影响的既有测试(4 Tab → 3 Tab;Discover 结构变化),保留播放/进度/收藏等行为断言。
- 新增测试:3 Tab 映射、玻璃卡渲染、Discover 搜索过滤、2 列网格。
- `dart analyze lib test` 零告警;`flutter test` 全过;`flutter build apk --debug` 通过。
- 真机验收由用户执行;不伪造评分/完结/榜单数据。
### 七、Git 与回执
- 从干净 `develop` 拉 `feature/flutter-k24-design-system`,小步提交;完成后 `--no-ff` 合并回 develop 并 push;
作者 `Zhou Weidong <502333560@qq.com>`;不动 main。
- 计划文档 `FruitServer/docs/plans/2026-10-19-K24.md`;留痕写 `PROGRESS.md`。
- 回执:token/字体落地、3 Tab、Home/Discover 结构、测试条数与门禁原始输出、已知数据缺口、真机验收步骤。
---
## [已处理·待用户验收] 2026-10-19 | K23.1 后台修正:品牌文字、删除按钮红色、布局回归
**用户反馈(2026-10-19,附截图)**:
1. 「图片的文字没有改过来」——浅色侧边栏里 `FruitShorts` 仍是蓝色,应改为正常深色品牌字;
2. 「删除按钮统一用红色背景」——当前多处删除仍是文字/次要样式;
3. 「有的布局乱了」——K23 后菜单、弹窗或顶栏存在对齐/溢出问题。
> 只改 `ReelShortWeb` 视觉/交互;不改接口、后端业务、客户端、main。
### 一、品牌文字颜色
- `src/store/modules/app.ts`:
- `setMenuTheme` 浅色分支 `logoTitleTextColor: 'inherit'` → `'#303133'`(或 `var(--el-text-color-primary)`);
- `setHeaderTheme` 浅色分支 `topHeaderTextColor: 'inherit'` → `'#303133'`;
- 深色分支保持 `#fff`。
- 检查 `Logo.vue`、`ToolHeader.vue`、`Breadcrumb.vue`,确保浅色菜单/顶栏不再出现蓝色品牌字或蓝色继承文字。
- 登录页品牌色保持现状。
### 二、删除按钮统一红色背景
1. 全仓扫描所有「删除 / 移除 / 删除菜单」按钮,统一为 `type="danger"` 实心红底、白字,
表格内 `size="small"`;禁止 `link`、`plain`、`text`、`class="danger-action"` 的浅色文字样式。
2. 至少覆盖:
- `views/contentManagement/DramaManagementView.vue`
- `views/contentManagement/EpisodeManagementView.vue`
- `views/contentManagement/BannerManagementView.vue`
- `views/contentManagement/CategoryManagementView.vue`
- `views/operation/NoticeManagementView.vue`
- `views/commerce/ProductManagementView.vue`
- `views/governance/ContentGovernanceView.vue`(评论删除、敏感词删除)
- `views/userManagement/AdminUsersView.vue`
- `views/permission/RoleManagementView.vue`(当前 `link`)
- `views/permission/RoleCategoriesView.vue`(补 `size="small"`)
- `views/permission/MenuManagementView.vue`(当前 `plain`)
- `views/creator-center/DramaEditView.vue` 与 `views/creator-center/WorkspaceView.vue` 的删除/下线
- `views/commerce/OrderManagementView.vue`、`views/creator/CreatorAuditView.vue` 的
`danger-action` 危险操作(退款/回收/冻结等)统一实心红底白字。
3. 删除确认弹窗的确认按钮也改为红色背景(`confirmButtonClass: 'el-button--danger'` 并确认样式生效)。
4. 调整操作列宽度,确保红底按钮不换行、不挤出单元格;同一列按钮高度一致。
### 三、布局回归修正
逐项用浏览器检查并修复:
1. 浅色/深色侧边栏:品牌图与文字垂直居中,品牌字不换行、不溢出;折叠/展开正常。
2. 菜单展开:一级与二级文字左边缘对齐(42px 基线),行高 44/40,无重叠/多余空白;popper 同样。
3. 短剧编辑弹窗:680px 与 375px 宽度下无横向滚动;封面预览/文件名/操作按钮不挤压;
数字输入两列对齐;底部“取消/保存修改”固定可见。
4. 锁定屏幕弹窗:440px 下标题/说明/表单/底部按钮不裁切,取消与锁定按钮高度一致。
5. 表格页:红色删除按钮加入后,操作列不换行、不出现横向溢出;1024 与 375 宽度验证。
6. 顶栏/标签页/面包屑:图标与文字基线一致,无错位。
### 四、验收与门禁
- `pnpm lint:eslint` 零错误;`pnpm build:pro` 通过。
- 浏览器自查:1440×900、1024×768、375×812;浅色/深色;登录页、工作台、短剧管理编辑弹窗、
锁定屏幕、菜单展开;控制台 0 error、0 404;`scrollWidth <= clientWidth`。
- 回执附:品牌色改动、删除按钮清单、布局修复项、门禁原始输出、浏览器自查结论。
### 五、Git
- 从干净 `develop`(基线 `fe1ae95`)拉 `feature/web-k23-fixes`,小步提交,
`--no-ff` 合并回 develop 并 push;作者 `Zhou Weidong <502333560@qq.com>`。
- 计划文档 `FruitServer/docs/plans/2026-10-19-K23.1.md`;留痕写 `PROGRESS.md`。
- 用户负责最终验证;本批不再扩大范围。
---
## [已处理·待用户验收] 2026-10-19 | K23 后台视觉深化:主色、菜单、编辑弹窗与锁屏
**用户指令(2026-10-19,附三张截图)**:「现在着重优化 UI 及交互」。
截图问题:
1. 左侧菜单子项无图标、缩进/行高/选中态不统一;
2. 短剧编辑弹窗里封面直接暴露超长 URL,数字输入框布局松散;
3. 锁定屏幕弹窗标题被分隔线穿过、右上角图标错位、缺少取消入口。
> 只改 `ReelShortWeb` 视觉/交互 + `FruitServer` 菜单图标种子 SQL;不改接口契约、后端业务、客户端、main。
### 一、主色统一为品牌辣椒红
- `src/styles/business/theme.css` 的 `--el-color-primary` 由 `#409eff` 改为 `#c4322a`,
同步 `light-3/5/7/8/9`、`dark-2`(参考值:`#d3615b / #e19995 / #f0c2c0 / #f5d9d8 / #faeeed / #9d2822`)。
- 按钮、开关、链接、选中菜单、标签页激活态、焦点环统一走 `var(--el-color-primary)`;
检查白字/红底、红字/白底对比度 ≥ 4.5:1;成功/警告/危险语义色保持不变。
- 登录页品牌色已用 `--app-brand`,保持不变;全站不再出现默认 Element 蓝。
### 二、左侧菜单(口径更正:子菜单统一不用图标)
> 用户 2026-10-19 更正:**不是子菜单图标为空,而是子菜单统一不要图标;当前问题是没对齐、视觉不好看。**
> 不要给子菜单加图标,也不要在子菜单预留图标位置。
1. **子菜单图标一律清空**:
- `sys_menu` 中所有 `type='MENU'` 的子菜单 `icon` 置为空串(含 id 4/5/7/8/9/10/11/12/13/14/16/18/19/21/23/25/27/29/30/31/33);
- 同步 `sql/data.sql` 与 `sql/data-phase1.sql`,并实际执行到开发库;
- 菜单管理页保存子菜单时也不落图标(或保存后清空),避免以后又出现图标;
- 渲染层对嵌套菜单项强制不渲染图标(即使库里有历史值也只显示文字)。
2. **对齐与视觉**(新增 `src/styles/business/menu.css`,作用域 `#v-menu`):
- 一级菜单:行高 44px,左侧 padding 16px,图标 18px,图标与文字间距 8px;
- 二级菜单:行高 40px,**文字左边缘与一级菜单文字左边缘对齐**(即一级 padding + 图标 18 + 间距 8 = 42px),
不出现“子项文字比父项文字更靠左/更靠右”的错位;
- 二级菜单不渲染图标,也不保留空白图标位;选中/悬浮背景从文字左边缘开始,圆角 8px;
- 一级/二级的选中态、悬浮态、展开箭头、折叠态、popper 子菜单保持一致;
- 深色模式选中态用深主色底 + 白字,浅色模式浅主色底 + 主色文字;行距紧凑,不出现大块空白。
### 三、短剧编辑弹窗(`DramaManagementView.vue`)
1. 弹窗宽 680px,`close-on-click-modal=false`,内容区可滚动、底部按钮固定;
表单分三块:`基础信息`(名称/类型/简介)、`付费设置`(免费集数/单集解锁金币/是否热门)、`封面`。
2. 数字输入:`el-input-number` 固定宽 180、`controls-position="right"`,两列等宽对齐。
3. **封面编辑器重做**:
- 左侧 2:3 预览(约 96×128),无封面时显示虚线占位;
- 右侧显示状态 chip(已上传/未上传)、文件名(只显示 basename,不显示完整路径)、
`更换封面` / `移除` 操作;上传中显示进度遮罩;
- **不再展示原始 URL 输入框**;如需查看,提供“查看原图”链接在新窗口打开;
- 上传前校验类型(jpg/png/webp)与大小(≤5MB),失败在弹窗内提示并可重试;
- `移除` 只清空本地表单,保存后生效。
4. 表单加 `:model` + `:rules`(名称必填、类型必选),保存时 `validate()`,错误显示在字段下方;
保留现有保存接口与字段,不改 payload。
5. 保存按钮 loading/disabled 防重复(已有 `saving`,补齐禁用与关闭保护)。
### 四、锁定屏幕弹窗(`LockDialog.vue`)
- 宽度 440,`max-height` 调整到 320–360,去掉全屏按钮(该弹窗不需要全屏)。
- 重做头部:标题左对齐、无穿过的分隔线、关闭按钮右对齐;标题下加一行说明“输入密码解锁当前会话”。
- 内容居中:头像 64、姓名 14、密码框 100% 宽;密码框支持回车提交。
- 底部:`取消`(关闭)+ `锁定`(primary,宽度不小于 96)。
- 删除 `console.log('set: ', val)`,不改变锁定逻辑。
### 五、交互与一致性
- 弹窗统一:标题/内容/底部间距、圆角、按钮高度(36/40)、暗色模式可读。
- 破坏性操作不新增逻辑;本批只做视觉与上述表单交互。
- 不修改接口、不新增权限点、不改业务流程。
### 六、验收与门禁
- `pnpm lint:eslint` 零错误;`pnpm build:pro` 通过。
- 开发库子菜单 `icon` 已全部置空;`GET /admin/auth/menus` 自检:一级菜单有图标,二级菜单 icon 为空。
- 浏览器自查(不作为用户验收):登录页/工作台/短剧管理/锁定屏幕;1024 与 375 宽度;
暗色/浅色;控制台 0 error、0 404。
- 用户浏览器验收:菜单图标与行距、红色主色、短剧编辑弹窗封面、锁屏弹窗。
### 七、Git 与回执
- `ReelShortWeb` 从干净 `develop`(基线 `3e8312e`)拉 `feature/web-k23-admin-polish`;
`FruitServer` 只改菜单图标 SQL 种子,单独提交并 push。
- 完成后各自 `--no-ff` 合并回 `develop` 并 push;作者 `Zhou Weidong <502333560@qq.com>`。
- 计划文档 `FruitServer/docs/plans/2026-10-19-K23.md`;留痕写 `PROGRESS.md`。
- 回执:主色变量、子菜单图标落库结果、弹窗改动、门禁原始输出、浏览器自查结论。
---
## [已处理·待用户验收] 2026-10-19 | K22.1 后台收尾:登录文案、图标残留与文件清理
**用户反馈(2026-10-19,附登录页截图)**:
1. 「这个下面的文字可以不要」——登录页标题下方的说明段落与三个标签(短剧与剧集 / 发布状态 / 角色权限)删掉。
2. 「还有图标没有改过来」——仍有旧品牌图/旧图标来源:浏览器 favicon、首屏 loading 的 `/logo.png`、mock 路由里的旧图标、在线图标开关。
3. 「有些文件该删除的删除掉」——删除已确认无引用的文件与 mock 体系。
> 只改 `ReelShortWeb`;不改接口契约、后端业务、客户端、main。用户直接在浏览器验收。
### 一、登录页文案
- `src/views/Login/Login.vue` 删除 `<div class="hero-copy">` 中的说明段落 `<p>{{ t('login.message') }}</p>`
与 `<ul>` 三个标签;保留 `hero-kicker` 与 `h1`。
- 删除随之不再使用的 l10n key(`login.message` / `login.pointDrama` / `login.pointPublish` / `login.pointAccess`)
与对应样式;若 key 仍被其他页面引用则保留并说明。
### 二、图标与品牌残留(重点)
1. **favicon**:用客户端品牌图替换 `public/favicon.ico`:
- 复制 `src/assets/imgs/brand-mark.png` 为 `public/brand-mark.png`;
- `index.html` 改 `<link rel="icon" type="image/png" href="/brand-mark.png" />`;
- 删除 `public/favicon.ico`。
2. **首屏 loading**:`index.html` 的 `<img src="/logo.png">` 改为 `/brand-mark.png`,加圆角;
首屏背景改为客户端深色 `#100C0A`、标题文字浅色,避免闪白。
删除 `public/logo.png`。
3. **在线图标开关**:`.env.base`、`.env.dev`、`.env.pro`、`.env.test` 的 `VITE_USE_ONLINE_ICON` 全部改为
`false`,由 `PurgeIcons` 本地打包 Material Symbols,避免在线 API 不可达导致图标空白/旧缓存。
构建后必须确认菜单/顶栏/表格工具栏图标真实渲染。
4. **mock 旧图标**:`.env.dev`、`.env.pro`、`.env.test` 的 `VITE_USE_MOCK` 全部改为 `false`;
删除整个 `mock/` 目录;`vite.config.mts` 移除 `viteMockServe` 的 import 与条件配置。
目的:不再把 `mock/role/index.mock.ts` 里的 `ant-design:` / `ep:` 旧图标带进任何构建。
### 三、文件清理(删除前先全仓 grep 确认零引用)
必须删除:
- `src/assets/imgs/logo.png`(已无引用)
- `src/assets/imgs/avatar.jpg`(UserInfo 已改读 store,无引用)
- `public/logo.png`、`public/favicon.ico`(被 brand-mark 替代)
- `src/views/permission/CustomMenuView.vue`(无路由/菜单/组件引用)
- `src/views/Login/components/RegisterForm.vue`(后台无注册接口,假提交)
- 同时移除 `src/views/Login/components/index.ts` 的导出、`Login.vue` 的注册切换状态与 `LoginForm.vue`
的「还没有账号?注册」入口及 `to-register` 事件;相关 l10n key 若不再使用一并删除。
可选删除(仅在 grep 证明零引用后):
- 其他未挂路由、未被 import 的 demo 页面/组件/资源。
**禁止**删除仍被路由、菜单、组件或构建配置引用的文件;删除清单与 grep 证据写入回执。
### 四、验收
- 全仓扫描结果:旧图标前缀 0、`<el-icon>` 0、`@element-plus/icons-vue` 0、`/logo.png` 0、`mock` 引用 0。
- `pnpm lint:eslint` 零错误;`pnpm build:pro` 通过。
- 用 `pnpm dev`(base 模式)打开登录页与后台:
- 登录 hero 只剩 kicker + 标题;
- 浏览器标签页图标、首屏 loading、侧边栏品牌图三者一致;
- 菜单/顶栏/表格工具栏图标全部为 Material Symbols Rounded 且无空白;
- 控制台无 404(尤其无 `/logo.png`、mock 请求)。
### 五、Git 与回执
- 从干净 `develop`(ReelShortWeb 当前 `5494190`)拉 `feature/web-k22-cleanup`,小步提交,
`--no-ff` 合并回 develop 并 push;作者 `Zhou Weidong <502333560@qq.com>`。
- 计划文档 `FruitServer/docs/plans/2026-10-19-K22.1.md`;留痕写 `PROGRESS.md`。
- 回执:删除文件清单 + grep 证据、图标/品牌修复、env 变更、门禁原始输出、浏览器自查结论。
---
## [已处理·待用户验收] 2026-10-19 | K22 后台管理视觉与交互统一(图标对齐客户端)
**用户指令(2026-10-19)**:「先优化后台管理 UI 及交互;后台管理图标统一成客户端的;严格执行 specs 文档规范」。
> 只改 `ReelShortWeb` 的视觉/交互 + `FruitServer` 的菜单图标种子 SQL;不改接口契约、业务逻辑、客户端、main。
> 用户会直接在浏览器验收结果。
### 一、必读规范(严格执行)
按顺序读:`AGENTS.md` → `CODING_STANDARDS.md` → `GIT_WORKFLOW.md` → `FruitServer/docs/api-phase1.md` §2 →
`docs/plans/codex-研发任务清单.md` → 本 INBOX。
Vue/TS 硬约束:沿用 vue-element-plus-admin 结构;eslint 零错误;`pnpm build:pro` 通过;
新增手写源码首行 `// Author: fruit.com.cn`;函数 ≤50 行、参数 ≤5、类/文件规模红线、禁止魔法值;
提交作者统一 `Zhou Weidong <502333560@qq.com>`。
### 二、图标统一(核心)
**目标**:后台所有图标统一为客户端同款 **Material Symbols Rounded**(客户端使用 Material `*_rounded`),
不再混用 `ep:`、`ant-design:`、`mdi:`、`zmdi:`、`ion:`、`bi:`、`radix-icons:` 与 Element Plus 图标组件。
1. 新增 `src/constants/icons.ts`(语义名 → Iconify 全名)与 `src/components/AppIcon`:
- `AppIcon` 基于现有 `Icon` 组件,默认 `material-symbols:*`,统一尺寸/颜色/描边风格;
- 图标名禁止散落在页面里,统一从 `AppIcons` 取。
2. 菜单图标(`sys_menu.icon`)按以下映射落库,并同步 `FruitServer/sql/data.sql` 与 `sql/data-phase1.sql`:
| 菜单 | id | 新图标 |
| --- | --- | --- |
| 工作台 | 1 / 2 | `material-symbols:dashboard-rounded` |
| 内容管理 | 3 | `material-symbols:video-library-rounded` |
| 后台管理 | 6 | `material-symbols:settings-rounded` |
| 用户管理 | 15 | `material-symbols:group-rounded` |
| 创作者管理 | 17 | `material-symbols:movie-edit-rounded` |
| 内容治理 | 20 | `material-symbols:policy-rounded` |
| 订单与权益 | 22 | `material-symbols:payments-rounded` |
| 数据看板 | 24 | `material-symbols:monitoring-rounded` |
| 创作者中心 | 26 | `material-symbols:video-camera-front-rounded` |
| 运营管理 | 32 | `material-symbols:campaign-rounded` |
3. 更新完 SQL 后**实际执行到开发库**(`psql -f sql/data.sql`、`psql -f sql/data-phase1.sql`,脚本必须幂等),
并用管理员登录后的 `GET /admin/auth/menus` 自检返回的 icon 全部为 `material-symbols:` 前缀。
4. 菜单管理页图标候选改为上述合法 Iconify 名;保存时统一存全名;旧的短名(`Document` 等)渲染时做映射兜底。
5. 顶栏、标签页、表格工具栏、弹窗等共享组件 **以及 `src/views/**` 页面内所有图标**
(含 `<el-icon><Plus/></el-icon>`)统一替换为 `AppIcon`;移除页面内对 `@element-plus/icons-vue` 的直接引用。
语义映射至少包含:`menu-rounded` / `menu-open-rounded`、`fullscreen-rounded` / `fullscreen-exit-rounded`、
`format-size-rounded`、`language-rounded`、`dark-mode-rounded` / `light-mode-rounded`、
`refresh-rounded`、`close-rounded`、`chevron-left-rounded` / `chevron-right-rounded`、
`search-rounded`、`restart-alt-rounded`、`view-column-rounded`、`help-rounded`、`add-rounded`、
`edit-rounded`、`delete-rounded`、`upload-rounded`、`download-rounded`、`visibility-rounded`、
`arrow-upward-rounded` / `arrow-downward-rounded`、`check-rounded`、`cancel-rounded`。
6. **品牌图标**:把客户端 `ReelShort/assets/icon/splash_mark.png` 复制为
`ReelShortWeb/src/assets/imgs/brand-mark.png`,侧边栏(展开/收起)与登录页统一使用该图;
不再使用框架默认 logo 或自绘图标。保留原 `logo.png` 文件但不再引用。
### 三、登录页(`src/views/Login/**`)
1. 删除默认账号/密码占位提示(`admin` / `admin123`),改走 i18n 占位文案。
2. “记住我”默认关闭;只记住用户名,**不把密码写入 localStorage**;同步清理 user store 中登录信息的密码字段。
3. 表单支持回车提交;登录/注册按钮在请求与动态路由生成完成前保持 loading,防止重复提交。
4. 窄屏(≤960px)压缩品牌装饰区,保证登录卡在首屏完整可见。
5. 登录页品牌名/副标题与侧边栏共用同一来源(环境变量 + i18n),不再写死 `FruitShorts Studio`。
6. 视觉与客户端一致:深色暖底、辣椒红强调只用于按钮/焦点(**不整体改后台主题色**),品牌 mark 与侧边栏同尺寸同圆角。
### 四、全局壳与主题
1. 修复深色模式:菜单/头部浅色主题下的文字对比度、标签页关闭图标、主题开关轨道/边框、
面包屑当前项颜色;所有颜色走 `var(--el-*)` 或业务 token。
2. 去掉 `body` 的 `min-width: 980px` 强约束,补 1200/768 断点;保证 1024、800、375 宽度下顶栏右侧与内容区不被裁切。
3. 顶栏、标签页、表格工具栏、弹窗图标全部换成 Material Symbols Rounded;同一语义只用一个图标。
4. 清理全局 CSS 冲突:
- 只保留一处 `:root` 字体声明(`--font-display` / `--font-body`);
- 圆角收敛为 `--app-radius-sm/md/lg`(4/6/12),登录卡可单独 18;
- 把硬编码 `#409eff !important` 改为 `var(--el-color-primary)`;
- 删除/改名未使用的 `.app-shell/.sidebar/.main-area/.topbar/.brand/...` 死选择器,避免“改了不生效”。
5. 用户信息区头像/姓名从 `userStore` 读取,不再写死 `avatar.jpg` 与 `Archer`。
6. “清除缓存并重置”增加二次确认。
### 五、明确不做(留给 K23)
业务页的表格/筛选/分页统一、拦截器 reject、删除/上下架二次确认、行内防重复、表单 rules、
权限按钮补齐、上传校验、状态标签/日期格式统一。**本批不碰这些业务交互逻辑**,避免与视觉批次混在一起。
### 六、验收与门禁
- `pnpm build:pro` 通过;`pnpm lint:eslint` 零错误(只跑改动文件也可,但最终全量通过)。
- 开发库菜单 icon 已更新;`GET /admin/auth/menus` 自检全部为 `material-symbols:`。
- 浏览器自查(Codex 自检,不作为用户验收):登录页、侧边栏折叠/展开、顶栏、标签页、
深色/浅色、1024/800/375 宽度;控制台无报错。
- 用户浏览器验收:图标风格、登录页、全局壳、深色模式、窄屏。
### 七、Git 与回执
- `ReelShortWeb` 从干净 `develop`(基线 `5c087f4`)拉 `feature/web-k22-admin-visual`;
`FruitServer` 只改 SQL 种子,单独提交并 push。
- 完成后各自 `--no-ff` 合并回 `develop` 并 push;不动 main。
- 计划文档 `FruitServer/docs/plans/2026-10-19-K22.md`;留痕写 `PROGRESS.md`。
- 回执:图标映射与落库结果、登录页/全局壳改动、扫描前后对比、门禁原始输出、浏览器自查结论。
---
## [已处理·待用户验收] 2026-10-19 | K21 登录页 UI 优化
**用户指令(2026-10-19)**:「现在主要做登录页面 ui 优化」。
> 只改客户端 `ReelShort` 登录/注册页(`lib/pages/auth/**`)及其 l10n;不改接口、后端、Web、specs、main。
> 主题继续使用辣椒红暖色与 K20 的 `AppDimens` token。
### 一、现状问题
- 品牌区是左上角小横排,缺少登录页应有的品牌感;页面整体顶对齐,大屏下方留白过多。
- 返回按钮独占一行,推高内容且与品牌区挤在一起。
- 登录/注册共用一句 `authSubtitle`,注册态缺少针对性说明。
- 注册态没有密码规则提示(文案已有 `authPasswordHint`,但只作为输入框 hint)。
- 本地校验失败只有一个错误胶囊,出错字段没有红框定位。
- `_submit()` 没有 `busy` 短路,密码框 IME 回车可绕过禁用态重复提交;失败分支 `setState` 缺 `mounted` 判断。
### 二、改造项
1. **布局重排**(`auth_view.dart`)
- 页面改为「可垂直居中、键盘弹起可滚动」:用 `LayoutBuilder` + `SingleChildScrollView`(保留 Key `auth-scroll`)
+ `ConstrainedBox(minHeight: 可视高度 - padding)` + 居中内容,最大宽度仍 440。
- 返回按钮改为顶部浮层(`Positioned` + `SafeArea`,Key `auth-back` 保留),不再占据内容流;
品牌区与卡片整体垂直居中。
- 保留 `auth-background-glow` 主光晕(每个颜色 alpha ≤ 0.18),新增一层金色/暖色极淡光晕(alpha ≤ 0.06,独立 Key)。
2. **品牌区重做**(`auth_widgets.dart`)
- `_AuthBrand` 改为居中竖排:品牌图标 + `appName`(26/w800)+ `authBrandTagline`(14,`blueSoft`)。
- 品牌图标改用真实资源 `assets/icon/splash_mark.png`(`ClipRRect` 圆角 14 + 辣椒红柔光阴影),
**Key `auth-brand-mark` 必须保留且尺寸仍为 48×48**(既有测试断言)。
3. **卡片与文案**
- 卡片结构保持:模式切换 → 标题(25/w800)→ 副标题 → 字段 → 错误胶囊 → 提交按钮。
- 副标题按模式区分:登录用现有 `authSubtitle`;注册新增 `authSignUpSubtitle`(三语):
- en: `Create an account to keep your list, history, and progress.`
- zh: `创建账号,保存你的片单、观看记录和进度。`
- id: `Buat akun untuk menyimpan daftar, riwayat, dan progresmu.`
- 注册态在密码框下增加一行 12px 的 `authPasswordHint` 辅助说明;登录态不显示。
4. **字段级错误定位**(`auth_logic.dart` + `auth_form_widgets.dart`)
- `AuthLogic` 增加 `AuthFieldError? fieldError`;`validate()` 失败时记录具体字段错误,`onChanged` 时清除。
- 邮箱/密码 `InputDecoration` 增加 `errorBorder` / `focusedErrorBorder`(`colorScheme.error`,宽度 1.5),
出错字段显示红框;**错误胶囊 `auth-error-capsule` / `auth-error` 及其文案、圆角 12、alpha 0.1 全部保留**。
5. **交互健壮性**(`auth_view.dart`)
- `_submit()` 开头 `if (_logic.busy) return;`;`catch` 中 `setState` 前判断 `mounted`;
`onSubmitted` 在 busy 时忽略。其余提交与导航行为不变。
6. **视觉细节**
- 输入框聚焦时前缀图标保持 `blueSoft`(现状),提交按钮增加极轻的辣椒红阴影(不改背景色断言)。
- 页面底部保留 `28 + viewInsets.bottom` 的内边距,确保小屏 + 键盘下提交按钮可达。
- 不引入新依赖、不新增图片资源。
### 三、硬约束(既有测试不得改断言)
`test/auth/auth_visual_test.dart` 与 `test/auth/auth_page_test.dart` 必须原样通过,重点保留:
- `auth-brand`、`auth-brand-mark` 尺寸 `48×48`、品牌名与 tagline 文案;
- `auth-background-glow` 渐变每个颜色 alpha ≤ 0.18;
- `auth-card` 圆角 20、边框色 `outlineVariantDark`、宽度 1;
- 输入框高度 52、圆角 14、背景 `AppColors.bar`、聚焦边框 `blue` / 1.5;
- 提交按钮高度 52、圆角 26、背景 `blue` / 按下 `bluePressed`;
- `auth-enter-animation` 时长 ≤220ms、tween 0→1;
- 模式切换 Key、选中色 `blue`、标题文案 `Welcome back` / `Create your account`;
- 隐藏兼容文案 `New here? Create an account` / `Already have an account? Sign in` 必须仍可被 `find.text` 找到;
- 错误胶囊 Key、圆角 12、alpha 0.1、错误图标与语义色;
- 小屏 320×568 + 键盘 260 时 `auth-submit` 可滚动到并可点击。
**测试失败时改实现,不改测试。** 允许新增测试覆盖注册副标题、密码提示与字段红框。
### 四、门禁
`dart analyze lib test` 零告警;`flutter test` 全过;`flutter build apk --debug` 通过。
真机验收由用户执行,Codex 不做设备侧取证。
### 五、Git 与回执
- 从干净 `develop`(基线 `e44fbdf`)拉 `feature/flutter-k21-auth-ui`,小步提交,完成后
`--no-ff` 合并回 develop 并 push;不动 main;作者统一 `Zhou Weidong <502333560@qq.com>`。
- 计划文档 `FruitServer/docs/plans/2026-10-19-K21.md`;留痕写 `PROGRESS.md` 并单独提交 FruitServer develop。
- 回执:布局前后差异、三语文案、保留的 Key/尺寸清单、测试条数与门禁原始输出、真机验收步骤。
### 六、K21 完成回执
- 客户端分支:`feature/flutter-k21-auth-ui`
- 功能提交:`8a96f34`
- 合并回 develop:`b1c69e2`
- 已推送:`e44fbdf..b1c69e2 develop`
- author/committer:`Zhou Weidong <502333560@qq.com>`
已完成:居中可滚动布局、顶部浮层返回、图片品牌 hero、模式独立副标题、
注册密码规则提示、字段级红框、busy/mounted 保护、双层光晕和提交阴影。
```text
$ dart analyze lib test
No issues found!
$ flutter test
00:24 +343: All tests passed!
$ flutter build apk --debug
Running Gradle task 'assembleDebug'... 7.5s
✓ Built build/app/outputs/flutter-apk/app-debug.apk
```
---
## [已处理·待用户验收] 2026-10-19 | K20 客户端视觉统一(辣椒红主题)
**用户指令(2026-10-19)**:「保留辣椒红,先做 K20 视觉统一」。
> 只改客户端 `ReelShort` 的视觉与布局;不改业务逻辑、接口调用、后端、Web、specs、main。
> 主题口径已拍板:**保留现有辣椒红暖色**(`AppColors.blue = #C4322A`),不恢复克莱因蓝。
### 一、目标
消除跨页面视觉不一致与明显不合理的紧凑布局,统一颜色、字号、间距、圆角、导航、卡片、按钮与进度条;
不改任何接口行为,不改既有测试断言。
### 二、统一 token(新增 `lib/core/theme/app_dimens.dart`)
- 页面左右边距:`16`(详情页 24 → 16;Auth 20 → 16)。
- 卡片:外层圆角 `16`、内边距 `16`;列表内嵌小块圆角 `12`、内边距 `12`;胶囊圆角 `20`。
- 字号档位:caption `12`、body `14`、列表标题 `15`、区块标题 `16`、页面标题 `20`(w800);
导航标签 `11`。**禁止再出现 9.5 / 10px 的业务文案**。
- 主按钮最小高度 `48`(整页主操作 `52`);点击区不小于 `44×44`。
- 主色只用 `AppColors`;新增 `AppColors.favorite`(收藏红心)如需保留粉色。
### 三、改造项(按文件)
1. **底部导航合一**:新增 `lib/core/widgets/app_bottom_nav.dart`,参数化 tab(图标/文案/选中);
高 `64`、图标 `22`、标签 `11`、选中态用主色胶囊(以现有 `home_view.dart` 为基准)。
替换 `home_view.dart`、`discover_view.dart`、`mine_view.dart` 三处导航;
`player_view.dart` 的 `_fallbackNav` 若保留也必须复用该组件。**保留现有 Key**(`tab-home` 等)与文案。
2. **账号入口卡重做**:`account_entry_card.dart` 高度 48 → `64`,图标 `16` → `18`,标签 `9.5` → `11`,
允许 2 行;四个入口点击区高度 ≥44;保留 `mine-account-card`、`mine-account-profile/devices/export/deactivate` 五个 Key。
3. **字号下限**:把 `home_drama_grid.dart:146`、`mine_view.dart:144/648/912`、`discover_view.dart:369`、
`home_view.dart:364`、`comment_tile.dart:114`、`watch_records_page.dart:282/296`、
`notices_page.dart:244/260`、`mine_view.dart:591`、`account_devices_page.dart:204` 提升到 ≥12(导航标签 11)。
4. **卡片/间距收敛**:`downloads_page.dart:305`(10)、`library_drama_tile.dart:26`、
`notices_page.dart:217`、`watch_records_page.dart:230`、`downloads_page.dart:200`、
`mine_view.dart:614/802` 统一到 12;`home_drama_grid.dart:104/109`、`mine_view.dart:377`、
`account_devices_page.dart:115`、`account_deactivate_page.dart:93` 统一到 16;
卡片内边距 9/14 收敛为 12/16。`drama_detail_view.dart:147/233` 的 24 → 16。
5. **颜色收敛**:替换 `downloads_page.dart:201/222/224/370`、`watch_records_page.dart:295`、
`mine_view.dart:143/590/637/717/743/828/835`、`favorites_page.dart:117`、
`home_quick_drawer.dart:54/77/118/262`、`player_view.dart:792/872/879` 中的直接 `Color(0x...)`
为最接近的 `AppColors` 常量(必要时新增 `favorite`);`account_devices_page.dart:196-203` 的
`scheme.tertiary` 改 `AppColors.gold` 或 `blueSoft`;`drama_detail_view.dart:169` 的
`onSurfaceVariant` 改 `AppColors.muted`。
6. **进度条统一**:详情续看 `drama_detail_view.dart:366-373`、观看记录 `watch_records_page.dart:267-272`、
下载 `downloads_page.dart:357-362` 统一为高 4、圆角 4、前景 `AppColors.blueSoft`、背景 `Colors.white24`;
Player 主拖动条保留金色。
7. **按钮与对比度**:`app_theme.dart` 增加 `filledButtonTheme.minimumSize = Size(0, 48)` 与
`outlinedButtonTheme`(深色前景用 `AppColors.blueSoft`);`mine_view.dart:390-401` 去掉
`VisualDensity.compact`;`empty_state.dart:150-158` 的重试色由 `scheme.primary` 改 `AppColors.blueSoft`。
8. **标题层级**:页面标题统一 20/w800,区块 16/w700,列表 15/w600-700;
`drama_detail_view.dart:159-164` 的 `headlineLarge` 显式改 24。
9. **深色唯一口径**:`app.dart:221-223` 改 `themeMode: ThemeMode.dark`;移除页面级
`Theme(data: AppTheme.dark())` 的重复包裹(保留根主题),确保日期选择器、弹窗也是深色。
若短期内删除页面级包裹风险大,可保留包裹,但 `themeMode` 必须为 dark。
10. **状态栏与安全区**:根 Scaffold 统一 `AnnotatedRegion<SystemUiOverlayStyle>(light)`,
`appBarTheme.systemOverlayStyle = light`、`systemNavigationBarColor = AppColors.bar`;
`favorites_page.dart:102`、`watch_records_page.dart:118`、`notices_page.dart:118`、
`downloads_page.dart:96`、`account_profile_page.dart:108`、`account_devices_page.dart:93`、
`account_export_page.dart:89`、`account_deactivate_page.dart:73` 的底部 padding
增加 `MediaQuery.paddingOf(context).bottom`(或 `SafeArea(top: false)`)。
11. **详情页返回与封面压暗**:`drama_detail_view.dart:80-107` 的加载/失败分支补带返回按钮的 AppBar;
`SliverAppBar` 封面顶部加黑色渐变,保证返回/更多图标在浅色封面上可读。
12. **选中态可见性**:`player_view.dart:834-845` 点赞/收藏选中时改实心图标
(`Icons.favorite` / `Icons.bookmark`);`discover_view.dart:262-296` 与
`mine_view.dart:537-564` 的选中项补 `Semantics(selected: true)`。
13. **点击区补足**:`discover_view.dart` 的 `_HeaderPill`(403-447)、`_LoginButton`(449-469)
最小高度 44;分类 Chip(262-296)点击区 44(视觉高度可保持 36,用透明 padding 扩大)。
14. **列表占位对齐**:`watch_records_page.dart:208-222` 占位高度与真实卡片一致(114),并复用相同 padding。
### 四、明确不做(留给 K21)
搜索 UI、Android 返回键行为、播放器切集票据失败黑屏、登录后继续原操作、下拉刷新、注销状态闭环、
时长/日期格式统一、语言上送后端。**本批不碰这些逻辑**。
### 五、约束与门禁
- 不改既有测试断言;视觉改动导致测试失败时改实现,不改测试(保留所有现有 Key 与文案)。
- `dart analyze lib test` 零告警;`flutter test` 全过;`flutter build apk --debug` 通过。
- 代码规模红线、文档注释、`// Author: fruit.com.cn` 与既有规范不变。
- 真机验收由用户执行,Codex 不做设备侧取证。
### 六、Git 与回执
- 从干净 `develop`(基线 `42ce1d1`)拉 `feature/flutter-k20-visual`,小步提交,完成后
`--no-ff` 合并回 develop 并 push;不动 main;作者统一 `Zhou Weidong <502333560@qq.com>`。
- 计划文档 `FruitServer/docs/plans/2026-10-19-K20.md`;留痕写 `PROGRESS.md` 并单独提交 FruitServer develop。
- 回执:改动清单、统一后的 token 值、测试条数与门禁原始输出、真机验收步骤(逐屏对照色/字/间距)。
### 七、K20 完成回执
- 客户端分支:`feature/flutter-k20-visual`
- 功能末提交:`5f1df40`
- 合并回 develop:`e44fbdf`
- 已推送:`42ce1d1..e44fbdf develop`
- author/committer:`Zhou Weidong <502333560@qq.com>`
已完成 14 项改造:统一 token、四处底部导航、账号入口卡 64 高、
字号下限、卡片/间距、颜色、进度条、按钮高度、标题层级、深色主题、
状态栏/安全区、详情返回与封面渐变、选中态、点击区和观看占位高度。
门禁:
```text
$ dart analyze lib test
No issues found!
$ flutter test
00:24 +341: All tests passed!
$ flutter build apk --debug
Running Gradle task 'assembleDebug'... 7.8s
✓ Built build/app/outputs/flutter-apk/app-debug.apk
```
既有测试要求的两项例外:Auth 卡圆角保持 20;EmptyState 副标题/按钮保持
12.5/13。两者均提取为 token,未修改测试断言。
---
## [已处理·待用户验收] 2026-10-19 | K19 客户端账号中心:资料 / 设备 / 数据导出 / 注销
**用户指令(2026-10-19)**:「先写 K19 任务书并派单」。
> 只改客户端 `ReelShort`。后端、`ReelShortWeb`、`specs`、`main` 一律不动。
### 一、背景与缺口
后端 `/app` 共 45 个接口,客户端已接 26 个。本轮对接账号中心 6 个(`api-phase1.md` §1.2,第 157–162 行;实现见
`fruitshorts-app-api/.../AppMeController.java`):
| 方法 | 路径 | 客户端现状 |
| --- | --- | --- |
| GET | `/app/me` | 未对接 |
| PUT | `/app/me` | 未对接 |
| GET | `/app/me/devices` | 未对接 |
| GET | `/app/me/export` | 未对接 |
| POST | `/app/me/deactivate` | 未对接 |
| POST | `/app/me/deactivate/cancel` | 未对接 |
### 二、接口与字段口径(以后端代码为准确认,勿臆造)
1. **GET `/app/me`** → `AppUserRespVO`:`uid, nickname, avatarUrl, gender, countryCode, regionCode,
language, timezone, currency, status, maskedTarget, createdAt, lastLoginAt, vipLevel(Integer),
vipExpireAt`。`vipLevel` 是数字,其余时间字段是 `LocalDateTime` 字符串。
2. **PUT `/app/me`** → 请求 `ProfileUpdateReqVO`,全部字段可选,**只传本次改动的字段**:
`nickname, avatarUrl, gender, birthday(yyyy-MM-dd), language, timezone, regionCode`。
`nickname` 为空串时后端忽略;`gender` 取值按 `schema-phase1.sql` 注释用小写
`male / female / unknown`;`birthday` 后端只能设置、不能清空。响应同 `AppUserRespVO`。
3. **GET `/app/me/devices`** → **直接返回 `AppUserDevice` 实体数组**(不是专用 VO):
`id, userId, deviceId, platform, deviceModel, firstSeenAt, lastSeenAt, status, createdAt, updatedAt, deleted`。
按契约 §0,`Long` 字段以字符串下发。客户端**只展示** `deviceId/platform/deviceModel/firstSeenAt/lastSeenAt/status`,
`userId`、`deleted` 一律忽略、不落日志、不上屏。
4. **GET `/app/me/export`** → `{profile: AppUserRespVO, credentials: [{type, masked, verified}], consents: [...], devices: [...]}`
(以 `AppUserService.exportAccount` 实际结构为准)。
5. **POST `/app/me/deactivate`** → 请求 `{reason}`(`password` 后端当前不校验,**客户端不采集、不上送**);
成功返回 `scheduledAt`(15 天冷静期截止时间,`LocalDateTime`)。后端同时吊销全部会话。
已有申请时返回业务错误,文案「注销申请已在处理中」。
6. **POST `/app/me/deactivate/cancel`** → 成功 `data = null`;无待处理申请时错误文案「没有待处理的注销申请」。
**契约缺口(本批如实标注,不得自行补后端)**:
- `GET /app/me` 不返回「注销申请中」状态 → UI 不能做状态判断,只能把后端错误文案原样透传;
「撤销注销」入口常驻,无申请时展示后端返回的文案。
- 无「设备下线 / 踢出」接口 → 设备页**只读**;`TC-ACCOUNT-10` 中「单点下线」记为缺口,留待后续批次。
- 无头像上传接口 → 头像**只展示**(`avatarUrl` 为空用现有占位图标),本批不做上传、不做 URL 输入框。
### 三、客户端改造范围
1. **remote**:新增 `lib/core/data/remote/account_api.dart`,六个方法,路径与请求体严格按上表;
错误一律抛现有 `ApiException`(禁止吞异常、禁止空 catch)。
2. **models**:新增 `account_profile.dart`、`account_device.dart`、`account_export.dart`。
数值/ID 解析用 `json_numbers.dart` 的 `jsonToId` / `jsonToInt`,兼容字符串与数字两种下发形式。
3. **repository**:新增 `account_repository.dart`(接口 + `HttpAccountRepository`);
在 `app.dart` 的 Provider 树注册,供页面与测试注入(`FakeAccountRepository` 放 `test/support/`)。
4. **页面**(建议 `lib/pages/account/`,沿用 provider binding/logic/view 三层):
- **个人资料**:进入时 `GET /app/me`;可改 `nickname`(去空格后非空、≤30 字符,客户端校验)、
`gender`(male/female/unknown,本地化文案)、`birthday`(日期选择,`yyyy-MM-dd`);
`uid / maskedTarget / countryCode / currency / status` 只读展示;`language / timezone / regionCode` 只读。
保存调 `PUT /app/me`,成功后用响应刷新页面,并**同步本地登录态**(见第 6 条)。
- **登录设备**:`GET /app/me/devices`,列表展示平台、机型、设备号(脱敏显示,保留首尾各 4 位)、
首次/最近活跃时间、状态;空态用现有 `EmptyState`;本页只读。
- **数据导出**:`GET /app/me/export`,展示资料摘要、凭据(`type/masked/verified`)、授权记录数、设备数;
提供「复制完整 JSON」到剪贴板(`Clipboard.setData`)+ SnackBar 提示;不写本地文件。
- **注销账号**:15 天冷静期与「申请后立即登出全部设备」的显著提示;可选注销原因(≤200 字);
「申请注销」必须二次确认弹窗 → 成功后展示 `scheduledAt` 并清空本地会话(服务端已吊销全部会话,
用 `AuthController.clear()`,不要再调 logout),回到未登录态;
「撤销注销申请」常驻,成功/失败均以后端文案提示。
5. **入口**:`lib/pages/mine/mine_view.dart` 在已登录态新增「账号」卡(置于钱包卡之后、菜单卡之前):
个人资料 / 登录设备 / 数据导出 / 注销账号,四项带图标与分隔线;未登录不显示。样式沿用现有 `_menuCard`。
6. **登录态同步**:`AuthController` 增加公开方法(如 `applyProfile(AuthUser)`),资料保存成功后更新
内存 session 的 `uid/nickname/avatarUrl` 并持久化,使「我的」页昵称即时生效;不改令牌与刷新逻辑。
7. **l10n**:新增文案必须同时写入 `app_en.arb / app_zh.arb / app_id.arb` 并重新生成;
印尼语为印尼市场主语言,禁止留英文占位。
8. **测试(TDD,先红后绿)**:
- API/模型:六接口路径与请求体、`Long` 字符串/数字两种解析、缺字段容错、错误文案透传;
- logic:资料保存成功后回调登录态、注销成功后清会话、撤销失败展示后端文案、导出复制内容;
- widget:我的页四个入口仅登录态可见、资料页字段回填与保存、设备空态、注销二次确认、
数据导出页加载/失败/重试。
- 既有测试不得改断言;如需适配只改注入的 Fake。
### 四、约束与门禁
- 代码规模红线:函数 ≤50 行、参数 ≤5、类 ≤300 行、文件 ≤500 行、嵌套 ≤4、圈复杂度 ≤10;禁止魔法值。
- public 类/方法必须有文档注释,文件首行统一 `// Author: fruit.com.cn`;禁止裸 `then()`、空 catch。
- 门禁(不达标不提交):`dart analyze lib test` 零告警;`flutter test` 全过;
`flutter build apk --debug` 构建通过。
- 真机验证由用户执行,Codex **不得**自行做设备侧验证,也不得用本机 curl 充当设备证据。
### 五、Git 与交接
- 从干净 `develop`(基线 `7d68699`)拉 `feature/flutter-k19-account`;小步提交,
`type(scope): 描述`(≤50 字符),只 `git add` 本次文件。
- 完成后 `git checkout develop && git merge --no-ff feature/flutter-k19-account && git push origin develop`;
**不动 `main`**;提交作者与提交者统一 `Zhou Weidong <502333560@qq.com>`。
- 任务书与留痕属于 FruitServer 仓库:`docs/plans/PROGRESS.md` 追加命令 + 原始输出 + 解读,
单独提交到 FruitServer `develop`(`docs(plans): ...`),不夹带客户端代码。
### 六、回执要求
完成后停住并回执:分支与提交号、六个接口的调用位置、测试条数与结果、`dart analyze` / `flutter test` /
`flutter build apk --debug` 原始结论、已知缺口(设备下线 / 头像上传 / 注销状态查询),
以及「用户可照做的真机验证步骤 + 每步预期结果」。
### 七、K19 完成回执
- 客户端分支:`feature/flutter-k19-account`
- 功能末提交:`7151bc9`
- 合并回 develop:`9b3ab9d`
- 已推送:`7d68699..9b3ab9d develop`
- author/committer:`Zhou Weidong <502333560@qq.com>`
六个接口调用集中在 `ReelShort/lib/core/data/remote/account_api.dart`,分别由:
- `AccountProfilePage` 调 GET/PUT `/app/me`
- `AccountDevicesPage` 调 GET `/app/me/devices`
- `AccountExportPage` 调 GET `/app/me/export`
- `AccountDeactivatePage` 调 POST `/app/me/deactivate` 与 `/deactivate/cancel`
门禁原始结论:
```text
$ dart analyze lib test
No issues found!
$ flutter test
00:26 +335: All tests passed!
$ flutter build apk --debug
Running Gradle task 'assembleDebug'... 10.0s
✓ Built build/app/outputs/flutter-apk/app-debug.apk
```
已知缺口:
- `GET /app/me` 不返回注销申请状态,UI 不判断状态;撤销失败展示后端文案。
- 没有设备下线接口,登录设备页只读;`TC-ACCOUNT-10` 的“单点下线”未实现。
- 没有头像上传接口,头像只展示;空值使用占位图标。
- 资料响应不含生日,当前只支持设置生日,不能回填或清空。
用户真机验证步骤(设备侧取证由用户执行):
1. 登录后进入「我的」。
预期:钱包卡之后、菜单卡之前显示「账号」卡,包含个人资料 / 登录设备 /
数据导出 / 注销账号四项。
2. 打开「个人资料」。
预期:昵称、性别、UID、脱敏账号、国家、币种、状态、语言、时区、地区回填;
修改昵称与性别后保存,返回「我的」后昵称立即更新。
3. 打开「登录设备」。
预期:设备号脱敏,展示平台、机型、首次/最近活跃时间和状态;无设备时显示空态。
4. 打开「数据导出」。
预期:展示资料摘要、凭据、授权记录数和设备数;点「复制完整 JSON」出现成功提示。
5. 打开「注销账号」。
预期:显示 15 天冷静期与立即登出提示;输入原因后必须二次确认。
6. 确认注销并重新打开 App。
预期:成功页展示预计注销时间,本地会话已清空,App 回到未登录态。
7. 重新登录后再次进入「注销账号」,点击「撤销注销申请」。
预期:有申请时显示撤销成功;无申请时原样展示后端“没有待处理的注销申请”。
---
## [已处理·待用户验收] 2026-10-17 | K16 注册限流改为可配置(支持不限制)
**用户指令(2026-10-17)**:「注册 429 限流做成可配置的,不限制或限制数量」。
> 只改后端注册风控配置化,不改注册业务逻辑与接口形态。
### 一、现状(实测)
- `fruitshorts-module-user/.../AppAuthService.java:305-307`:常量硬编码 `MAX_REGISTER_PER_IP_PER_DAY = 5`、`MAX_REGISTER_PER_DEVICE_PER_DAY = 3`。
- `guardRegisterRisk(...)` 在数值达到阈值时抛 429;无注解注入,无法配置。
- `fruitshorts-app-api/src/main/resources/application.yml` 已有 `fruitshorts.interaction.*` 等配置段。
### 二、本批范围(后台可配置,走 `sys_config`)
1. **读取方式**:`AppAuthService` 注入 `ConfigService`,读取:
- `app.register.max-per-ip-per-day`
- `app.register.max-per-device-per-day`
- 值 **≤0 = 不限制**(跳过对应 guard);键缺失或非数字时回退默认 **5 / 3**;错误码与文案不变。
2. **种子配置**:`sql/data-phase1.sql` 增加两键的幂等种子(`INSERT ... ON CONFLICT (config_key) DO NOTHING`):
- 开发默认值 0(不限制),remark 写明「0=不限制;生产建议 IP 5 / 设备 3」。
3. **后台配置**:无需新建页面——已有 `ReelShortWeb/src/views/systemManagement/SettingsView.vue` 使用 `/admin/configs`;只需确认菜单可进入「系统配置」并能看到这两个键。
4. **契约说明**:`docs/api-phase1.md` §1.1 注册风控段补充:两键可在后台「系统配置」修改、0=不限制、缺省回退 5/3。
5. **测试**:`ConfigService` 注入 mock 覆盖 0 / 缺省 / 自定义三种;`mvn test` 全绿。
6. **打包并重启 8081**:
- `mvn -pl fruitshorts-app-api -am package -DskipTests`;
- 停旧 8081 进程并使用新 jar 启动,`/healthz` 通过;
- 验证:确认库中两键为 0 后,用新设备号连续注册 ≥4 次不再 429(接口自检,不作为设备侧证据)。
7. **不使用 application.yml 固化限流值**(已下发口径更正)。
### 三、约束
- 不改客户端;不改注册接口路径与返回结构;不动 main。
- 提交作者与提交者统一 `Zhou Weidong <502333560@qq.com>`;从干净 develop 拉 `feature/backend-register-rate-config`,完成后 `git merge --no-ff` 回 develop 并 push。
- 留痕写入 `FruitServer/docs/plans/PROGRESS.md`。
### 四、K16 首版回执(历史,已被返工回执替代)
**结论**:注册 IP/设备限流已改为读取后台 `sys_config`,开发两键为 0/0,
支持 0=不限制;后台系统设置页可修改。注册接口与返回结构未变。
- 最终后端实现 `f1aaaee`,缺省回退测试 `f4003ed`,最终合并
`5b7be2e`,已推送 `78fb995..5b7be2e develop`。
- `AppAuthService` 注入 `ConfigService`,读取
`app.register.max-per-ip-per-day` 和
`app.register.max-per-device-per-day`;非法/缺失回退 5/3。
- `sql/data-phase1.sql` 已增加幂等种子(0/0);当前开发库已写入并确认 ID 3/4。
- 新增 Web 提交 `54db082`,合并 `5c087f4`;系统设置页可编辑两项,
`pnpm build:pro` 通过。
- `AppAuthServiceTest` 18 项通过,全量 `mvn test` BUILD SUCCESS。
- 新 jar 已启动,`/healthz` 返回 200;当前 8081 PID `80642`。
- 同一新设备号连续注册 4 次均 HTTP 200、`code=0`,不再出现 429。
命令、日志和服务自检原始输出见
`docs/plans/PROGRESS.md`「32. K16 注册限流后台配置化」。
### 五、K16 返工回执(2026-10-17)
**结论**:已按最新口径改为“后台 `sys_config` 优先,`@Value` fallback”,
后台「系统配置」可按 IP/设备修改;开发库两键均为 0。
- 返工提交 `35d92c0 fix(backend): 后台注册限流配置优先并保留注入兜底`。
- `AppAuthService` 读取 `app.register.max-per-ip-per-day` /
`app.register.max-per-device-per-day`;缺失或非数字时回退
`fruitshorts.auth.register-max-*` 的 `@Value` 注入值。
- `sql/data-phase1.sql` 幂等种子已执行;查询结果为 IP 0、设备 0。
- `/admin/configs` 实测返回两键,现有系统设置页可修改。
- 临时把 IP 上限改为 2:前两次注册成功,第三次返回 `code=429`;
恢复 0 后再次注册成功。
- `AppAuthServiceTest` 18 项通过,全量 `mvn test` BUILD SUCCESS。
- 新 jar 已重启,8081 PID `94657`,`/healthz` 返回 HTTP 200。
原始输出与解读见 `docs/plans/PROGRESS.md`「33. K16 返工」。
---
## [待处理] 2026-10-17 | K14 客户端文档:README + 任务清单 + 文件说明
**用户指令(2026-10-17)**:「现在更新客户端 readme 任务清单 及文件说明」。
> 只改 `ReelShort` 仓库文档,不改代码、不改 specs、不改后端/Web。
> 内容必须基于当前实际代码与已交付事实,**禁止编造**;不写任何密钥/账号。
### 一、现状(实测)
- `ReelShort/README.md` 仅 50 行,是原型阶段说明,已明显过时:
- 仍写“本地演示评论”“互动数据保存在本机”,实际 K3 已接后端评论/点赞/举报;
- 未提分类、通知、下载授权、克莱因蓝主题、印尼语默认、运行时服务器地址设置;
- 未写门禁命令、iOS SPM 兼容注意事项、USB 调试方式。
- `ReelShort` 无 `docs/` 目录,没有任务清单与文件说明。
- `lib` 现有 105 个 Dart 文件,目录:`core/{config,data/{models,remote,repositories,services},formatting,platform,theme,widgets}`、`l10n`、`pages/{auth,discover,downloads,drama_detail,favorites,home,interaction,mine,notices,player,watch_records}`。
### 二、本批范围(3 份文档)
#### K14-A 重写 `ReelShort/README.md`
至少包含:
1. 项目定位:面向印尼市场的短剧 App(Flutter)。
2. 技术栈:Flutter 3.47 / Dart 3.13、Provider、dio、video_player、shared_preferences、flutter_localizations(en/zh/id)。
3. 目录结构概览(core 三层 + pages + l10n + test)。
4. 已接后端接口清单(按契约分组,**以代码实际调用为准**):账号、首页/发现/分类/详情、播放、观看记录/收藏、下载授权 8 接口、互动(评论/点赞/举报)、公告。
5. 运行方式:
- Android USB:`adb reverse tcp:8081 tcp:8081` + `flutter run -d <device> --dart-define=API_BASE_URL=http://127.0.0.1:8081`;
- iOS:`flutter run -d <udid> --dart-define=API_BASE_URL=http://<MacIP>:8081`;
- **debug 下可在「我的 → 设置 → 服务器地址」运行时修改**(K13),换网络不必重编。
6. 环境注意:
- Xcode 16.2 / Swift 6.0.3 与 `shared_preferences_foundation 2.5.6` 的固定原因(2.6.0 需 Swift 6.1);
- iOS SPM 报 `Missing package product` 时的恢复步骤(清 DerivedData、`flutter pub get`、`xcodebuild -resolvePackageDependencies`);
- Android `libdartjni.so` 加载失败时卸载重装。
7. 门禁命令:`dart analyze lib test`、`flutter test`、`flutter build apk --debug`;测试命名约定。
8. 当前边界:下载不落盘/不续传/不离线播放;无搜索接口;通知无已读态。
#### K14-B 新增 `ReelShort/docs/TASKS.md`
表格形式,列:批次 / 范围 / 状态 / 关键提交或契约章节 / 备注。至少覆盖:
- K1 详情+播放+进度+收藏;K2 分类/记录/收藏页/下载授权;K3 评论/点赞/举报/分享;K5 克莱因蓝主题+印尼语;K6 登录页+底部导航+震动;K7 轮播+站内通知;K8 登录页视觉;K9 首页网格;K10 空态;K11 3 列+全部首位;K12 去标题+空态重做;K13 运行时服务器地址。
- 待办:K4 账号剩余(Google 登录需凭据)、B1 支付/金币、搜索(契约缺失)、下载落盘(另立批次)。
- 状态以 `develop` 实际合并情况为准,不确定的标注「需核对」,不要编造提交号。
#### K14-C 新增 `ReelShort/docs/FILES.md`
- 目录树(到两级),每目录一句话职责。
- 关键文件表:`lib/core/config/app_config.dart`、`server_endpoint`(如已实现)、`core/data/remote/*`、`repositories/*`、`services/*`、`core/theme/*`、`core/widgets/empty_state.dart`、`pages/*/`、`l10n/*.arb`。
- `test/` 结构说明(按模块目录)。
- 只写实际存在的文件;生成物(`*.g.dart`、`app_localizations*.dart`)标注为生成、勿手改。
### 三、约束
- 中文书写,简洁准确;不使用 AI 腔(“赋能/一站式/旨在”等)。
- 不泄露任何密钥、账号、服务器凭据。
- 只新增/修改 `README.md`、`docs/TASKS.md`、`docs/FILES.md`;不删既有内容以外的文件。
- 提交作者与提交者统一 `Zhou Weidong <502333560@qq.com>`。
### 四、门禁与 Git
- 文档改动仍需 `dart analyze lib test` 与 `flutter test` 通过(确认未误改代码)。
- 从干净 `develop` 拉 `docs/k14-client-docs`;完成后 `git merge --no-ff` 回 `develop` 并 push;不动 `main`。
- 在 `FruitServer/docs/plans/PROGRESS.md` 追加「叙述 + 命令 + 原始输出 + 解读」,完成后停住写回执。
### 五、K14 完成回执(2026-10-17)
**结论**:客户端 README 已重写,并新增任务清单与文件说明。只改 ReelShort 的
三份文档,未改代码、specs、后端或 Web。
- 文档分支 `docs/k14-client-docs` 从 `f95325d` 完成并 `--no-ff` 合并;
主提交 `5a28845`,接口统计修正 `213c86e`,最终合并 `9fe62d1`,已推送
`f95325d..9fe62d1 develop`。
- `README.md` 已覆盖印尼市场定位、Flutter 3.47.7/Dart 3.13.5 技术栈、
实际调用的 26 个 `/app/**` 路径(31 个方法级调用)、Android USB/iOS/运行时地址、iOS SPM、
shared_preferences 固定、Android libdartjni、门禁和当前边界。
- 新增 `docs/TASKS.md`:K1-K13 状态表、K4/支付/搜索/下载落盘待办;
未写不确定提交号,未确认项标「需核对」。
- 新增 `docs/FILES.md`:目录树、关键文件职责、test 结构和生成物说明。
- 仓库 `.gitignore` 排除了 `/docs/`,本次仅对 TASKS/FILES 使用
`git add -f`,没有修改 `.gitignore`。
- `dart analyze lib test` 零告警;`flutter test` 为
`00:20 +309 ~1: All tests passed!`;本轮仅文档,不需要 APK 构建。
原始命令、输出与内容核对见 `docs/plans/PROGRESS.md`
「30. K14 客户端文档」。
---
## [待处理] 2026-10-17 | K13 运行时服务器地址设置(仅 debug)
**用户反馈(2026-10-17,已重复 3 次)**:每次 Codex 门禁构建出的 APK 不带 `--dart-define`,App 就回退到 `http://jiangdeMacBook-Air.local:8081`,真机不可用。
> 这是开发效率问题,不是业务功能。只做 debug 工具,不改 release 行为、不改接口。
### 一、现状(实测)
- `lib/core/config/app_config.dart`:`apiBaseUrl = dart-define(API_BASE_URL) 非空 ? 它 : 默认值`;默认值在 iOS/Android 真机上是 `http://jiangdeMacBook-Air.local:8081`(常量)。
- 构建参数由 `--dart-define` 决定,编译期固化;Xcode/门禁构建不带参数时必然回退。
- 客户端已有 `SharedPreferencesAsync` 使用先例(`LocaleController`)。
### 二、本批范围
1. **新增运行时配置** `lib/core/config/server_endpoint.dart`(命名可调整):
- 读取 `SharedPreferencesAsync` 的 `dev.api_base_url`;
- 优先级:**运行时设置 > `--dart-define(API_BASE_URL)` > 现有默认值**;
- 启动时在 `main()` 里 `await` 加载完成后再创建 `ApiClient`(若现有结构不允许,可在 `AppConfig` 初始化时同步设置);
- URL 规范化:去首尾空格、去末尾 `/`;仅接受 `http://` 或 `https://` 开头,非法值忽略并保留原值。
2. **设置入口(仅 `kDebugMode`)**:
- 位置:我的页 → 设置(现有 `showLanguage` 弹窗内增加「服务器地址」一行),或登录页底部一行小字入口;二选一,保持克制;
- 交互:弹窗显示当前实际 baseUrl,可编辑并保存;保存后提示「重启 App 生效」(文案走 l10n,三语);
- 提供「恢复默认」操作,清除 `dev.api_base_url`。
3. **release 行为不变**:`kDebugMode == false` 时完全不显示入口,也不读取本地覆盖(仍用编译期配置)。
4. **测试**:
- 优先级:本地设置 > dart-define > 默认;
- URL 规范化与非法输入拒绝;
- debug 下入口可见、release 下不可见(可用 `debugShowSetting` 之类的可注入开关测试);
- 不得删改既有测试致绿。
### 三、明确不做
- 不做多环境切换 UI(dev/staging/prod 列表)、不做远程配置下发。
- 不改后端、不改接口、不改其他页面业务逻辑。
- 不在 release 包暴露任何服务器地址修改能力。
### 四、门禁与 Git
- `dart analyze lib test` 零告警;`flutter test` 全过;`flutter build apk --debug` 通过。
- 从干净 `develop` 拉 `feature/flutter-k13-dev-server-setting`;完成后 `git merge --no-ff` 回 `develop` 并 push;不动 `main`。
- 先写 `FruitServer/docs/plans/2026-10-17-K13.md`;每步「叙述 + 命令 + 原始输出 + 解读」追加到 `PROGRESS.md`;完成后停住写回执。
- 提交作者与提交者统一 `Zhou Weidong <502333560@qq.com>`。
### 五、K13 完成回执(2026-10-17)
**结论**:新增 debug-only 运行时服务器地址覆盖。启动优先级为本地设置 >
`--dart-define(API_BASE_URL)` > 现有默认;release 不读取本地覆盖且不显示入口。
未改接口、后端或其他页面业务逻辑;未跑真机。
- 客户端分支 `feature/flutter-k13-dev-server-setting` 从 `b762289` 完成并
`--no-ff` 合并;功能提交 `907562a`,合并提交 `f95325d`,已推送
`b762289..f95325d develop`。
- 新增 `ServerEndpoint`,使用 `SharedPreferencesAsync` key
`dev.api_base_url`;URL 会 trim、校验 http(s) 和 host,并去掉末尾 `/`。
- `main()` 在 `runApp` 前 debug 加载覆盖;ApiClient 创建时读取已解析地址。
- 我的页设置弹窗新增服务器地址行、编辑/保存、非法值提示、重启生效提示和
恢复默认;入口可通过 `debugShowSetting` 注入,默认 `kDebugMode`。
- 新增三语文案;既有语言设置流程保留。
- `dart analyze lib test` 零告警;`flutter test` 为
`00:19 +309 ~1: All tests passed!`;不带 `dart-define` 的 Android debug
APK 在隔离副本中构建成功。
用户真机验证步骤与逐步预期、原始输出见
`docs/plans/PROGRESS.md`「29. K13 运行时服务器地址设置」。
设备侧修改地址、重启和网络连通性验收由用户执行。
---
## [待处理] 2026-10-17 | K12 首页去标题 + 空态视觉重做
**用户指令(2026-10-17)**:「有了全部分类后 全部剧目不要显示了 而且现在空状态UI太丑了」。
> 在 K9/K10/K11 基础上做视觉修订,不改接口、数据与业务逻辑。
### 一、现状(实测)
- 首页 `lib/pages/discover/discover_view.dart`:分类 chips 下方有 `Text(l10n.allDramas)`(`Key('home-all-dramas-title')`),再往下才是 `_HomeDramaGrid`。
- 空态组件 `lib/core/widgets/empty_state.dart`:96×96 圆角 28 **实心方块** + 42px 图标 + 16px 标题 + 13px 副标题 + TextButton。用户反馈“太丑”。
### 二、本批范围
#### K12-A 去掉首页「全部剧目」标题
- 删除 `Key('home-all-dramas-title')` 的 `Text` 及其下间距;分类 chips 与网格间距调整为 12–14px。
- 网格、数据、空态逻辑不变;相关测试中“标题存在”的断言改为“不存在”,不得删测试文件。
#### K12-B 空态视觉重做(`empty_state.dart`)
目标:轻盈、精致、留白多,去掉“大色块 + 图标”的模板感。
图形结构改为**双层同心圆**:
- 外层圆:直径 84(compact 60),填充 `blue` 透明度 **0.05–0.06**,并加 1px 描边 `blue` 透明度 0.10–0.12;
- 内层圆:直径 56(compact 40),填充 `blue` 透明度 **0.10–0.12**;
- 中央图标:26–28px(compact 20–22),颜色深色主题 `blueSoft`、浅色主题 `blue`;
- 外层与内层同心居中,用 `Stack` 或两层 `Container` 嵌套实现。
文字与间距:
- 标题 15px / w600 / `onSurface`,与图形间距 14(compact 10);
- 副标题 12.5px / `muted` / 行高 1.45,与标题间距 6;
- 操作按钮:保留 `TextButton`,字号 13、主色、点击区 ≥44pt,与副标题间距 8;
- 整体 `Padding` 上下 28(compact 16),最大宽度 260;
- 不新增图片资源、不加动画。
#### K12-C 测试
- 更新 `empty_state` 相关断言:存在 `empty-state-art` 与内层圆(可用新 Key `empty-state-art-inner`)、图标尺寸、标题/副标题/按钮;
- 首页测试:`home-all-dramas-title` 不存在;chips 后直接出现 `home-all-dramas-grid`;
- 既有测试不得删改致绿。
### 三、门禁与 Git
- `dart analyze lib test` 零告警;`flutter test` 全过;`flutter build apk --debug` 通过(构建前确认无并发 `flutter run`/`build`)。
- 从干净 `develop` 拉 `feature/flutter-k12-home-empty-polish`;完成后 `git merge --no-ff` 回 `develop` 并 push;不动 `main`。
- 先写 `FruitServer/docs/plans/2026-10-17-K12.md`;每步「叙述 + 命令 + 原始输出 + 解读」追加到 `PROGRESS.md`;完成后停住写回执。
- 提交作者与提交者统一 `Zhou Weidong <502333560@qq.com>`。
### 五、K12 完成回执(2026-10-17)
**结论**:首页「全部剧目」标题已删除,chips 与网格间距为 14px;空态改为
双层同心圆视觉。未改接口、数据、轮播、三态或其他页面;未跑真机。
- 客户端分支 `feature/flutter-k12-home-empty-polish` 从 `b5a8656` 完成并
`--no-ff` 合并;功能提交 `2dd4627`,合并提交 `b762289`,已推送
`b5a8656..b762289 develop`。
- 外层圆 84(compact 60),blue 5.5% + 11% 描边;内层圆 56(compact 40),
blue 11%;图标 27(compact 21)。
- 标题 15/w600、副标题 12.5、按钮 13;上下 padding 28(compact 16),
最大宽 260。
- 新增 `empty-state-art-inner`、`empty-state-padding` Key;外层
`empty-state-art` 及图标/标题/按钮 Key 保留。
- 首页删除 `home-all-dramas-title`,chips 后直接进入网格;既有壳层中的
`All Dramas` 锚点改为稳定网格 Key,测试流程和用例均保留。
- `dart analyze lib test` 零告警;`flutter test` 为
`00:17 +302 ~1: All tests passed!`;`flutter build apk --debug` 成功。
用户真机验证步骤与逐步预期、原始输出见
`docs/plans/PROGRESS.md`「28. K12 首页去标题与空态重做」。
设备侧视觉、深浅主题与小屏验收由用户执行。
---
## [待处理] 2026-10-17 | K11 首页网格 3 列 + 分类首位「全部」
**用户指令(2026-10-17)**:「现在改首页,一行显示 3 个,而且我要的全部是显示在分类的第一个」。
> 在 K9 已实现的 2 列网格基础上做最小修订,不改接口、不改业务逻辑、不动后端。
### 〇、开工前必读
1. K9 计划与实现(`lib/pages/discover/widgets/home_drama_grid.dart`、`discover_view.dart`)
2. K5 主题 token
3. 本 INBOX 条目
### 一、PM 已勘察的现状(实测)
- `lib/pages/discover/widgets/home_drama_grid.dart`:`SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2, mainAxisSpacing: 16, crossAxisSpacing: 12, childAspectRatio: 2/3)`。
- `lib/pages/discover/discover_view.dart:273`:分类 chips 第一项为 `(code: CategoryController.allCode, label: l10n.forYou)`(文案「为你推荐」)。
- 其余(轮播、空态、三态)保持 K9/K10 现状。
### 二、本批范围
1. **网格改 3 列**:`crossAxisCount: 3`;`mainAxisSpacing: 14`、`crossAxisSpacing: 10`,`childAspectRatio` 保持 `2/3`;3 列下标题最多 2 行、分类 1 行,避免溢出。
2. **分类首位改「全部」**:chips 第一项仍使用 `allCode`,文案改为新增 l10n `all`(中文「全部」/ English「All」/ Bahasa Indonesia「Semua」);保留「热门 / 最新 / 服务端分类」顺序不变。
3. **同步测试**:
- 更新/新增断言 `crossAxisCount == 3`;
- 断言分类 chips 第一项文案为「全部」(按 locale);
- 既有 K9/K10 测试不得删改致绿。
4. **不做**:不改轮播、空态、三态、数据源、接口、其他页面;不新增分页。
### 三、门禁与 Git
- `dart analyze lib test` 零告警;`flutter test` 全过;`flutter build apk --debug` 通过(当前有 Android 真机 `flutter run` 在跑,构建前先确认没有并发构建占用 `build/`)。
- 从干净 `develop` 拉 `feature/flutter-k11-home-grid-3col`;完成后 `git merge --no-ff` 回 `develop` 并 push;不动 `main`。
- 先写 `FruitServer/docs/plans/2026-10-17-K11.md`;每步「叙述 + 命令 + 原始输出 + 解读」追加到 `PROGRESS.md`;完成后停住写回执。
- 提交作者与提交者统一 `Zhou Weidong <502333560@qq.com>`。
### 五、K11 完成回执(2026-10-17)
**结论**:首页网格已改为 3 列,分类首项仍绑定 `CategoryController.allCode`,
文案改为「全部 / All / Semua」。未改轮播、空态、三态、数据源、接口或其他页面;
未跑真机。
- 客户端分支 `feature/flutter-k11-home-grid-3col` 从 `0466b4b` 完成并
`--no-ff` 合并;功能提交 `f0db631`,合并提交 `b5a8656`,已推送
`0466b4b..b5a8656 develop`。
- `crossAxisCount: 3`、`mainAxisSpacing: 14`、`crossAxisSpacing: 10`,
`childAspectRatio: 2 / 3` 保持不变。
- 三份 ARB 原本已有 `all` key,分别为 All / 全部 / Semua,本轮直接复用,
未插入重复 JSON key。
- K9 网格测试更新为 3 列并断言「全部」位于「热门」之前;两个既有首页壳层
用例只把旧分类文案断言改为「All/全部」,导航流程和用例均保留。
- `dart analyze lib test` 零告警;`flutter test` 为
`00:18 +302 ~1: All tests passed!`;Android debug APK 在独立副本中构建成功。
用户真机验证步骤与逐步预期、原始输出见
`docs/plans/PROGRESS.md`「27. K11 首页 3 列与全部分类」。
设备侧 3 列布局、分类切换与详情跳转验收由用户执行。
---
## [待处理] 2026-10-17 | K10 统一空列表占位(EmptyState)
**用户指令(2026-10-17)**:「做空列表的占位图,所有的列表,要求简单大方、符合审美」。
> 依赖 K5 主题 token;**不引入图片资源**(用内置 `Icons` + 柔和圆形底),保证离线、体积与风格统一。
> 只改空态呈现,不改列表数据、接口与业务逻辑。
### 〇、开工前必读
1. `AGENTS.md`、`CODING_STANDARDS.md`、`GIT_WORKFLOW.md`
2. K5 主题 token(`AppColors` / `ColorScheme`)
3. 本 INBOX 条目
### 一、PM 已勘察的现状(实测,10 处空态)
| 页面/组件 | 代码位置 | 现有文案 |
|---|---|---|
| 首页 | `lib/pages/home/home_view.dart:56` | `noStories` |
| 首页剧目网格 | `lib/pages/discover/widgets/home_drama_grid.dart:52-53` | `noCategoryDramas` / `noSearchResults` |
| 收藏 | `lib/pages/favorites/favorites_page.dart:107` | `noFavorites` |
| 通知 | `lib/pages/notices/notices_page.dart:115` | `noNotices` |
| 详情剧集 | `lib/pages/drama_detail/drama_detail_view.dart:130` | `noEpisodesYet` |
| 我的-片单 | `lib/pages/mine/mine_view.dart:676` | `noSavedShows` |
| 我的-剧目 | `lib/pages/mine/mine_view.dart:736` | `noStories` |
| 下载 | `lib/pages/downloads/downloads_page.dart:151` | `noDownloads` |
| 观看记录 | `lib/pages/watch_records/watch_records_page.dart:97` | `noWatchRecords` |
| 评论弹窗 | `lib/pages/player/widgets/drama_comments_sheet.dart:225` | `noComments` |
现状均为纯文字(部分带按钮),风格不统一。
### 二、本批范围
#### K10-A 新建统一组件 `lib/core/widgets/empty_state.dart`
规格(简单大方):
- 布局:垂直居中,`Padding` 24;默认最大宽度 280,避免宽屏拉伸。
- 图形占位:96×96 圆角 28 的容器,背景 `AppColors.blue` 透明度 0.10–0.12;中央 `Icons` 40–44px,颜色 `AppColors.blueSoft`(深色主题)/ `AppColors.blue`(浅色主题按 `Theme.brightness` 取)。
- 标题:15–16px、w600、`onSurface`;居中,最多 2 行。
- 副标题(可选):13px、`muted`、居中,最多 2 行。
- 操作按钮(可选):`TextButton`/`OutlinedButton`,主色文字,最小点击区域 44pt;用于「重试」「去发现」等。
- `compact` 变体:图形 64×64、图标 28、字号缩小,用于评论弹窗等内嵌区域。
- 语义:外层 `Semantics(label: title)`;纯装饰图形 `excludeFromSemantics`。
- 参数建议:`icon`、`title`、`subtitle?`、`actionLabel?`、`onAction?`、`compact=false`、`key`。
#### K10-B 替换 10 处空态
- 按上表逐一替换为 `EmptyState`,图标对应语义:
- 首页/剧目:`movie_outlined`;分类:`grid_view_rounded`;搜索:`search_off_rounded`;
- 收藏:`favorite_border_rounded`;下载:`download_outlined`;记录:`history_rounded`;
- 通知:`notifications_none_rounded`;评论:`chat_bubble_outline_rounded`;剧集:`video_library_outlined`;
- 首页加载失败仍用错误态(`error_outline_rounded`),不要用空态冒充失败。
- 保留现有空态文案 key;如需副标题,优先复用已有 l10n,新增文案必须 en/zh/id 三份齐全(例如通用 `emptyHint`)。
- 保留现有按钮行为(重试/去发现等);评论弹窗使用 `compact`。
- 空态出现在原有滚动容器内时,保持可滚动与居中,不破坏三态切换。
#### K10-C 测试
- 组件单测:标题/副标题/按钮渲染、`compact` 尺寸、图标存在、Semantics。
- 页面测试:至少覆盖 收藏/下载/记录/通知/评论 五处空态渲染为 `EmptyState`(可通过 `find.byType(EmptyState)` + 文案);既有测试不得删改致绿。
- 深色/浅色、小屏不溢出。
### 三、明确不做
- 不引入图片/SVG/字体资源;不做动画插画。
- 不改列表数据、接口、三态逻辑、页面结构。
- 不改错误态与加载态,只统一空态视觉。
### 四、门禁与 Git
- `dart analyze lib test` 零告警;`flutter test` 全过;`flutter build apk --debug` 通过。
- 从干净 `develop` 拉 `feature/flutter-k10-empty-states`;完成后 `git merge --no-ff` 回 `develop` 并 push;不动 `main`。
- 先写 `FruitServer/docs/plans/2026-10-17-K10.md`;每步「叙述 + 命令 + 原始输出 + 解读」追加到 `PROGRESS.md`;完成后停住写回执。
- 提交作者与提交者统一 `Zhou Weidong <502333560@qq.com>`。
### 五、K10 完成回执(2026-10-17)
**结论**:新增统一 `EmptyState` 组件,任务书表格中的 10 处空态已全部替换;
错误/加载态保持原逻辑,未引入图片、SVG、字体或接口改动。未跑真机。
- 客户端分支 `feature/flutter-k10-empty-states` 从 `19babb8` 完成并
`--no-ff` 合并;功能提交 `6d08330`,合并提交 `0466b4b`,已推送
`19babb8..0466b4b develop`。
- 默认图形 96x96、圆角 28,blue 11% 背景,图标 42;compact 为 64x64、
图标 28;标题 16/w600,副标题 13,操作按钮至少 44pt。
- 深色主题图标 `blueSoft`,浅色主题图标 `blue`;外层 Semantics 只读标题,
图形和文本不重复朗读,操作按钮保持可访问。
- 10 处替换完成:首页、首页分类/搜索、收藏、通知、详情剧集、我的两处、
下载、观看记录、评论 compact。
- 新增三语 `emptyHint`;原有空态 l10n key、页面文案和操作行为保留。
- 新增 `test/empty_state/empty_state_test.dart` 4 个组件用例;收藏、下载、
记录、通知、评论五处页面测试已断言 `EmptyState`,评论还校验 compact。
- `dart analyze lib test` 零告警;`flutter test` 为
`00:18 +302 ~1: All tests passed!`;Android debug APK 在独立副本中构建成功。
用户真机验证步骤与逐步预期、原始输出见
`docs/plans/PROGRESS.md`「26. K10 统一空列表占位」。
设备侧深色/浅色、小屏与各页面空态验收由用户执行。
---
## [待处理] 2026-10-17 | K9 首页剧目列表改造(2 列全部剧目)
**用户指令(2026-10-17)**:「优化首页,要有全部剧目的列表,一行 2 个,不用显示继续观看的」。
> 只改首页(发现页)信息流布局与卡片,不改接口、不改业务逻辑、不动后端。
> 依赖 K5 主题 token 与现有 `RemoteImage` / 卡片视觉。
### 〇、开工前必读
1. `AGENTS.md`、`CODING_STANDARDS.md`、`GIT_WORKFLOW.md`
2. 当前首页 `lib/pages/discover/discover_view.dart`(649 行)
3. K5 主题 token、K7 轮播实现
4. 本 INBOX 条目
### 一、PM 已勘察的现状(实测)
- 首页结构(`discover_view.dart`):顶部 header → 轮播(home feed banners)→ 分类 chips(为你推荐/热门/最新/服务端分类)→「继续观看」标题 + 横向卡片列表(高 196、卡片宽 132,`_dramaContent` / `_continueHeader` / `_continueTile`)。
- 数据源:`CategoryController.visibleDramas`(当前选中分类的剧目);`_cardsFor(...)` 生成展示卡片;分类切换已有过滤逻辑。
- 「继续观看」区块使用 l10n `continueWatching`;横向列表不展示播放进度,本质是分类剧目横滑。
### 二、本批范围(首页单页改造)
1. **移除「继续观看」区块**:删除 `_continueHeader` 调用与其标题;`_continueTile` 若不再被使用则一并删除(不留死代码)。
2. **新增「全部剧目」2 列网格**:
- 位置:分类 chips 下方,占据原「继续观看」位置;
- 标题:新增 l10n `allDramas`(中文「全部剧目」/ English「All Dramas」/ Bahasa Indonesia「Semua Drama」);
- 网格:每行 2 个,左右边距 16、列间距 12、行间距 16;封面竖版比例 2:3,圆角 14;
- 卡片内容:封面、标题(最多 2 行)、分类/类型(最多 1 行),沿用现有深色渐变与文字层级;
- 数据:当前选中分类下的全部剧目(沿用 `visibleDramas`,去重后全量渲染);「为你推荐」显示全部;
- 点击:沿用 `logic.openCard(...)` 进详情;
- 长列表性能:外层改为 `CustomScrollView` + `SliverGrid`(或等价的懒构建方案),不要用 `GridView(shrinkWrap: true)` 嵌套整页;图片沿用 `RemoteImage` 懒加载。
3. **保留**:header、轮播、分类 chips、底部导航、现有三态(加载/失败重试/空态)与空态文案(无分类剧目/无搜索结果)。
4. **明确不做**:不动接口与数据模型、不改观看记录/收藏/下载、不改轮播与通知、不新增分页(feed 一次返回全量)、不改底部导航。
### 三、测试与门禁
- 新增/更新 widget 测试:首页不再出现「继续观看」;出现 `allDramas` 标题;网格为 2 列(断言 `SliverGridDelegateWithFixedCrossAxisCount.crossAxisCount == 2`);分类切换后网格内容更新;空态与失败态保持。
- `dart analyze lib test` 零告警;`flutter test` 全过;`flutter build apk --debug` 通过;构建前确认无并发 `flutter run`/`build` 占用 `build/`。
- 真机验收由用户执行:首页滚动、2 列布局、分类切换、点击进详情。
### 四、Git 与留痕
- 从干净 `develop` 拉 `feature/flutter-k9-home-grid`;完成后 `git merge --no-ff` 回 `develop` 并 push;不动 `main`。
- 先写 `FruitServer/docs/plans/2026-10-17-K9.md`;每步「叙述 + 命令 + 原始输出 + 解读」追加到 `PROGRESS.md`;完成后停住写回执。
- 提交作者与提交者统一 `Zhou Weidong <502333560@qq.com>`。
### 五、K9 完成回执(2026-10-17)
**结论**:首页「继续观看」已移除,分类下方改为 `CustomScrollView + SliverGrid`
的 2 列「全部剧目」列表;分类切换、空态、失败态、全量测试与 APK 构建均完成。
未改接口、数据模型、轮播、通知或其他页面;未跑真机。
- 客户端分支 `feature/flutter-k9-home-grid` 从 `99f64ab` 完成并
`--no-ff` 合并;功能提交 `cf03178`,合并提交 `2c381be`,已推送
`99f64ab..2c381be develop`。
- `discover_view.dart` 从 649 行降到 487 行;新增
`widgets/home_drama_grid.dart` 167 行。已删除 `_continueHeader`、
`_dramaContent`、`_continueTile`、`_ShowcaseCard` 与 `_cardsFor`。
- `SliverGridDelegateWithFixedCrossAxisCount` 为 2 列、列距 12、行距 16;
左右边距 16;封面 2:3、圆角 14,标题最多 2 行、类型最多 1 行。
- 新增三语 `allDramas`:`全部剧目` / `All Dramas` / `Semua Drama`。
- 新增 `test/discover/home_all_dramas_grid_test.dart` 5 个用例,覆盖标题、
无继续观看、2 列参数、2:3 封面、分类切换、空态、失败态和 320px 小屏。
- 既有 `test/widget_test.dart` 的 5 处「Continue Watching」可见文案断言原位
更新为「All Dramas」,所有既有用例与导航流程保留,未删除测试用例。
- `dart analyze lib test` 零告警;`flutter test` 为
`00:18 +297 ~1: All tests passed!`;`flutter build apk --debug` 成功。
用户真机验证步骤与逐步预期、原始输出见
`docs/plans/PROGRESS.md`「24. K9 首页 2 列全部剧目」。
设备侧滚动、布局、分类切换和详情跳转验收由用户执行。
---
## [已处理·待用户验收] 2026-10-17 | K8 登录页视觉重设计
**用户指令(2026-10-17)**:「接下来做登录页面 ui 优化,现在这个 ui 不好看」。
> K6 已完成功能层优化(品牌区、autofill、密码显隐、键盘流转),本批只做**视觉重设计**,不改业务逻辑与接口。
> 依赖 K5 主题 token(`blue #002FA7` / `bluePressed #00227A` / `blueSoft #5B7BE0` / `onPrimary #FFFFFF`)。
> specs 无设计稿;以下为 PM 按「深色 + 克莱因蓝 + 印尼市场」给出的可执行视觉规格,按此实现。
### 〇、开工前必读
1. `AGENTS.md`、`CODING_STANDARDS.md`、`GIT_WORKFLOW.md`
2. K5 主题 token(`lib/core/theme/app_colors.dart`、`app_theme.dart`)
3. 当前登录页 `lib/pages/auth/auth_view.dart`(356 行,K6 后版本)
4. 本 INBOX 条目
### 一、本批范围(登录页单页视觉重设计)
目标:从「表单堆叠」升级为有品牌感的深色登录页,保持现有逻辑与 Key 不变。
**布局结构(自上而下)**
1. 全屏背景:`AppColors.ink`,顶部叠加一层克莱因蓝径向/线性渐变光晕(透明度 ≤0.18,不抢文字对比度)。
2. 品牌区:44–52px 克莱因蓝圆角方块(内含白色播放/剧集图形图标,用 `Icons.` 即可,不引入图片资源)+ App 名 + 印尼市场 tagline(现有 `authBrandTagline`)。
3. 表单卡片:`AppColors.card` 背景、圆角 20、1px 边框(用 K5 `outlineVariant`)、内边距 20;卡片内为标题、副标题、输入框、错误提示、主按钮。
4. 主按钮:全宽、高 52、圆角 26、`AppColors.blue`、`onPrimary` 文字 16/w700;按下态 `bluePressed`;loading 时 18px 白色环且禁用输入。
5. 登录/注册切换:改为**分段控件(Segmented)或 pill 双段**(登录 | 注册),选中段主色底/白字,未选中透明/`muted`;点击切换现有 `toggleMode()`,不再用文字链接。
6. 底部辅助:保留当前模式说明或语言提示一行小字(`muted`,可选);不新增协议勾选。
**输入框规格**
- 填充式背景 `AppColors.bar`,圆角 14,高 52,无外边框;聚焦时 1.5px `AppColors.blue` 边框 + 前缀图标变主色;
- 前缀图标:邮箱 `mail_outline_rounded`、密码 `lock_outline_rounded`;密码后缀显隐按钮保留;
- 标签与错误文案沿用现有 l10n;错误提示改为浅红底胶囊(红 10% 透明度 + 圆角 12 + 图标),不再裸文本。
**动效与可用性**
- 首帧淡入 + 上滑(≤220ms,`TweenAnimationBuilder` 即可,不引第三方库);
- 小屏(iPhone SE)+ 键盘弹出不遮挡:沿用 `auth-scroll` 滚动与 `viewInsets` padding;
- 所有点击区域 ≥44pt;对比度:正文 ≥4.5:1;
- 保留 Key:`auth-scroll`、`auth-email`、`auth-password`;如新增控件需有稳定 Key 便于测试。
**明确不做**
- 不改 `AuthLogic`、校验规则、`AuthController`、接口调用、登录/注册流程;
- 不做忘记密码、Google/Apple 登录、协议勾选、第三方 SDK;
- 不新增图片/字体资源;不改其他页面。
### 二、测试与门禁
- 新增/更新 widget 测试:分段切换仍改变模式、邮箱→密码→提交焦点流转、密码显隐、busy 禁用、错误胶囊展示、品牌区文案存在;既有 `test/auth/auth_page_test.dart` 不得删改用例。
- `dart analyze lib test` 零告警;`flutter test` 全过;`flutter build apk --debug` 通过(注意当前有真机构建在跑,避免并发写 `build/`,先确认无构建进程)。
- 真机验收由用户执行:视觉、键盘、小屏、深色对比度。
### 三、Git 与留痕
- 从当前干净 `develop`(`6cc5bfc`)拉 `feature/flutter-k8-auth-visual`;完成后 `git merge --no-ff` 回 `develop` 并 push;不动 `main`。
- 先写 `FruitServer/docs/plans/2026-10-17-K8.md`;每步「叙述 + 命令 + 原始输出 + 解读」追加到 `PROGRESS.md`;完成后停住写回执。
- 提交作者与提交者统一 `Zhou Weidong <502333560@qq.com>`。
### 四、K8 完成回执(2026-10-17)
**结论**:登录页视觉重设计、交互回归、全量测试和 Android debug 构建均完成。
未改 AuthLogic、校验、接口、登录/注册流程或其他页面;未跑真机。
- 客户端分支 `feature/flutter-k8-auth-visual` 已从 `6cc5bfc` 完成并
`--no-ff` 合并;修复提交 `45c3c64`,合并提交 `99f64ab`,已推送
`6cc5bfc..99f64ab develop`。
- 深色背景、≤0.18 克莱因蓝光晕、48px 品牌方块、20px 圆角卡片、52px
输入框与主按钮、pill 分段切换、错误胶囊、220ms 淡入上滑均按规格实现。
- 既有 `auth-scroll/auth-email/auth-password/auth-switch-mode` Key、autofill、
next→done→提交、busy 禁用、密码显隐与 Haptics 保留;新增视觉控件 Key。
- 新增 `test/auth/auth_visual_test.dart` 6 个用例;既有
`test/auth/auth_page_test.dart` 9 个用例未修改且全过。
- `dart analyze lib test` 零告警;`flutter test` 为
`00:19 +292 ~1: All tests passed!`;`flutter build apk --debug` 成功。
用户真机验证步骤与逐步预期、原始命令输出见
`docs/plans/PROGRESS.md`「23. K8 登录页视觉重设计」。
设备侧截图、键盘与小屏验收由用户执行。
---
## [待处理] 2026-10-17 | K7 首页轮播图 + 站内信通知
**用户指令(2026-10-17)**:「现在做首页轮播图及站内信通知」。
> **依赖 K5/K6**:轮播与通知页的配色、控件、震动反馈沿用 K5 主题与 K6 `Haptics`。
> 接口只认 `FruitServer/docs/api-phase1.md` §1.3 / §1.9,**契约里没有的接口一律不许自造**。
### 〇、开工前必读
1. `AGENTS.md`、`CODING_STANDARDS.md`、`GIT_WORKFLOW.md`
2. `FruitServer/docs/api-phase1.md` §1.3(feed banners)/ §1.9.2(notices)
3. K5 主题 token、K6 `Haptics` 封装
4. 本 INBOX 条目
### 一、PM 已勘察的现状(实测)
- **轮播图已有基础实现**:发现页 `lib/pages/discover/discover_view.dart` 的 `_carousel` 使用 `lib/pages/discover/widgets/cover_carousel.dart`(3D 翻转风格,手动滑动)。
- 数据来自 `GET /app/discover/feed` 的 `banners[]`;**契约无独立 banner 接口**。
- 现状缺口:无自动轮播、无页码/圆点指示器、单张时无特殊处理、图片失败无占位。
- **站内信完全未接**:客户端没有 `notice_api` / 通知页;`GET /app/notices` 契约已实现。
- 契约:`GET /app/notices?pageIndex=1&pageSize=20`(匿名),返回 `{total, list:[{id,title,content,type,publishAt,expireAt}]}`,`pageSize` 最大 50,空数据返回 `total=0,list=[]`。
- 契约**没有已读/未读接口**,本批不做未读红点。
- 我的页菜单 `lib/pages/mine/mine_view.dart` 的 `_menuCard` 已有「观看历史 / 我的收藏 / 下载 / 设置」列表,可新增「通知」入口。
- 客户端尚未接 `GET /app/config`,本批**不接** `noticeEnabled` 开关(登记为后续项)。
### 二、本批范围(2 块 + 测试验收,按顺序做)
#### K7-A 首页轮播图增强
- 数据源保持不变:`feed.banners`,不新增接口、不改后端。
- 在现有 `CoverCarousel` 上做最小增强,**保留 3D 翻转风格**:
- 自动轮播:间隔 4.5s;用户拖拽/触摸期间暂停,松手后恢复;`items.length <= 1` 时不启动定时器;
- 页面不可见/组件 dispose 时取消定时器(避免后台耗电与 `setState after dispose`);
- 指示器:底部圆点或 `当前/总数`(二选一,克制);颜色用 K5 主题 token,未选中用 `muted` 低透明度;
- 图片加载失败显示占位底色 + 图标(复用 `RemoteImage` 的错误处理,不白屏);
- 点击行为沿用现有 `onOpen`(跳详情),不改变跳转逻辑;
- 无障碍:指示器与轮播容器提供语义标签(走 l10n)。
- `items` 为空时保持现状 `SizedBox.shrink()`,不占位。
#### K7-B 站内信通知
- 新增数据层:`notice.dart`(model)、`notice_api.dart`、`notice_repository.dart`、`notices_controller.dart`,ID 按字符串解析。
- 新增独立页 `lib/pages/notices/notices_page.dart`:
- 分页:首屏 `pageSize=20`,`list.length < total` 时展示「加载更多」并用 `pageIndex+1` 续拉;
- 三态:加载 / 空(「暂无通知」)/ 失败(可重试);支持下拉刷新;
- 列表项:标题、类型(`type` 原样展示或统一「公告」,不自造枚举映射)、`publishAt` 按 locale 格式化、内容摘要(最多 2 行);
- 点击打开详情弹窗(标题 + 发布时间 + 完整 `content`,保留换行);关闭按钮无障碍标签走 l10n。
- 入口:我的页 `_menuCard` 新增「通知」ListTile(图标 `notifications_none_rounded`,主题色),点击打开 `NoticesPage`;不新增首页铃铛,避免改首页信息架构。
- 匿名可访问,不要求登录;不做未读态、不做通知设置。
- 文案全部进 en/zh/id 三份 arb。
#### K7-C 测试与验收
- 轮播:`pump` 时间推进后自动切页、单张不启动定时器、指示器数量与当前页、触摸暂停、点击回调。
- 通知:首屏加载、分页追加、空态、失败重试、详情展示、时间格式化(id locale)。
- 深色主题、小屏、长标题/长内容不溢出。
- 真机验收由用户执行:轮播自动切换与指示器、通知列表/详情、三态与下拉刷新。
### 三、明确不做
- 不新增 banner 接口、不改 `/app/notices` 契约、不改后端/Web/specs。
- 不做已读/未读、通知红点、推送(FCM)、通知设置。
- 不接 `/app/config` 的 `noticeEnabled`(登记后续)。
- 不新增首页铃铛入口、不改首页信息架构。
### 四、规范约束
- 颜色只用 K5 token;震动仅用于关键操作(K6 `Haptics`),轮播滑动不加震动。
- 新增文案三语齐全;新文件首行 `// Author: fruit.com.cn`;public 类/方法文档注释;无 AI 痕迹。
- 代码规模红线:函数 ≤50 行、参数 ≤5、类 ≤300 行、单文件 ≤500 行、嵌套 ≤4 层、圈复杂度 ≤10。
### 五、门禁
1. `dart analyze lib test` 零告警;`flutter test` 全过。
2. `flutter build apk --debug` 通过。
3. 新增测试必须真红后绿;不得删改既有测试来「变绿」。
4. 真机/设备侧验证由用户执行(红线 7、10),回执给「用户可照做步骤 + 每步预期」。
### 六、Git 与留痕
- 从当前干净 `develop` 拉 `feature/flutter-k7-carousel-notices`;完成后 `git merge --no-ff` 回 `develop` 并 push;不动 `main`。
- 先写 `FruitServer/docs/plans/2026-10-17-K7.md`(范围、影响文件、契约缺口、风险、待确认)。
- 每步「叙述 + 命令 + 原始输出 + 解读」追加到 `docs/plans/PROGRESS.md`;完成后停住写回执。
- 提交作者与提交者统一 `Zhou Weidong <502333560@qq.com>`。
### 七、K7 完成回执(2026-10-17)
**结论**:K7-A 轮播增强、K7-B 站内通知、K7-C 测试验收均完成并停止。
未新增接口,未改后端/Web/specs。
#### 7.1 首页轮播
- 保持 `feed.banners` 数据源和 `CoverCarousel` 3D 翻转风格。
- 4.5s 自动轮播;单张不启动 Timer。
- 触摸/拖拽期间暂停,松手恢复。
- `active=false` 或 `TickerMode` 不可见时暂停;dispose 取消 Timer。
- 底部圆点指示器使用 `onPrimary` / `muted` K5 token,带三语语义标签。
- 图片失败复用 `RemoteImage` 并显示图标占位。
- 点击继续调用原有 `onOpen`。
#### 7.2 站内通知
- 新增 `notice.dart`、`notice_api.dart`、`notice_repository.dart`、
`notices_controller.dart` 和 `NoticesPage`。
- `GET /app/notices?pageIndex&pageSize=20`,匿名访问。
- ID 按字符串解析,`total` 按数字解析。
- 首屏、加载更多、空态、失败重试、下拉刷新均已接入;
已加载数量 `< total` 时才展示加载更多。
- 列表展示标题、原始 `type`、locale 时间、两行摘要。
- 详情弹窗展示完整 `content`,保留换行并支持长内容滚动。
- 我的页新增“通知”入口;匿名可进入,无登录闸。
- `noticeEnabled` 属 `/app/config`,本批未接,登记后续。
真实 8081 匿名接口验证:
```
$ curl -sS 'http://127.0.0.1:8081/app/notices?pageIndex=1&pageSize=20'
{"code":0,"message":"ok","data":{"total":0,"list":[]}}
```
#### 7.3 门禁原始结果
```
$ dart analyze lib test
Analyzing lib, test...
No issues found!
$ flutter test test/discover/cover_carousel_test.dart test/notice
All tests passed!
$ flutter test
00:14 +284 ~1: All tests passed!
$ flutter build apk --debug
Running Gradle task 'assembleDebug'... 6.9s
✓ Built build/app/outputs/flutter-apk/app-debug.apk
```
`~1` 是既有 live watch 用例在 8081 未启动时按机制跳过。
新增覆盖:
- 自动轮播、单张无 Timer、交互暂停、active/TickerMode、圆点、图片占位、onOpen。
- 通知 API 字符串 ID、controller 分页/空/失败/刷新。
- NoticesPage 首屏、加载更多、加载/空/失败三态、详情换行、印尼时间。
- 320×568 长标题/长内容不溢出;我的页匿名通知入口。
#### 7.4 Git
- 功能提交:`b870956665f5268070c691533cc2a334f2aa7f59`
`feat(flutter): 增加轮播自动播放与站内通知`
- `--no-ff` 合并:`3e8703b`,已推送
`b7b1c50..3e8703b develop -> develop`
- author/committer 均为 `Zhou Weidong <502333560@qq.com>`;未操作 `main`。
#### 7.5 用户真机验证步骤与预期
1. 打开发现页顶部轮播,等待 4.5 秒。
预期:自动翻到下一张,圆点同步变化;单张 banner 不会自动循环。
2. 按住/拖动轮播超过 4.5 秒后松手。
预期:按住期间不自动翻页;松手后恢复自动轮播;拖拽仍保持 3D 翻转。
3. 暂时断开图片源或使用失败封面。
预期:显示深色占位和图片失败图标,不白屏。
4. 点击当前 banner。
预期:按原有 DRAMA 跳转逻辑进入详情。
5. 进入“我的 → 通知”,未登录也可访问。
预期:显示通知列表;空数据时显示“暂无通知”;断网时显示失败及重试。
6. 有超过 20 条通知时点击“加载更多”。
预期:下一页追加到列表,无重复;已加载数量达到 total 后按钮消失。
7. 下拉刷新,再点击一条长通知。
预期:刷新后列表回到第一页;详情显示标题、发布时间和完整正文;
正文换行保留且长内容可滚动。
8. 切换 Bahasa Indonesia 查看通知时间。
预期:时间按印尼 Material 格式显示。
设备侧截图与真机结果由用户执行。
#### 7.6 已知边界
- 无已读/未读、红点、FCM、通知设置。
- 不接 `/app/config noticeEnabled`,通知入口当前常驻。
- 无独立 banner 接口,未新增首页铃铛。
---
## [已处理·待用户验收] 2026-10-17 | K6 客户端 UI:登录页 + 底部导航优化
**用户指令(2026-10-17)**:「接下来优化登录页面 UI 及底部导航 UI」。
> **依赖 K5 完成**:必须基于 K5 的克莱因蓝主题(`blue #002FA7` / `bluePressed #00227A` / `blueSoft #5B7BE0` / `onPrimary #FFFFFF`)与统一 `ColorScheme` 再做,不得沿用旧 `#2F7BFF`。
> specs 无 UI 设计稿(已确认),本批以「视觉统一 + 可用性」为目标,不自行改变信息架构。
### 〇、开工前必读
1. `AGENTS.md`、`CODING_STANDARDS.md`、`GIT_WORKFLOW.md`
2. K5 条目与 K5 计划文档(色值 token)
3. specs `docs/02-技术架构.md`(印尼首发)、`docs/19-术语表.md`
4. 本 INBOX 条目
### 一、PM 已勘察的现状(实测)
- 登录页 `lib/pages/auth/auth_view.dart`(246 行):AppBar + ListView,两个 `TextField`、`FilledButton`、错误行、登录/注册切换链接。
- 密码**无显隐切换**;无 `autofillHints`;无键盘 `next/done` 流转;错误提示写死 `#FF6B6B`。
- 主色仍直接引用 `AppColors.blue`,无品牌区;小屏键盘遮挡只靠 ListView 自然滚动。
- 底部导航在 `lib/pages/home/home_view.dart` 的 `_HomeTabs`(约 182–260 行):自定义 `Container` + `Row` + 4 个 `_TabButton`。
- 高度 63,背景 `#0D0D11`、上边框 `#242329` 均为硬编码;只有选中/未选中图标,无指示条;无统一主题 token。
- 现有 Key:`home-tabs-bar`、`tab-discover`、`tab-home`、`tab-my-list`、`tab-my`,**测试依赖,不得改名**。
- 播放沉浸模式(`videoImmersive`)下隐藏导航,需保留。
### 二、本批范围(3 块 + 测试验收,按顺序做)
#### K6-A 登录页 UI 优化
- 品牌区:登录页顶部增加 App 名与一句印尼市场定位副标题(文案走 l10n 三语),使用 K5 主题色;不引入图片资源。
- 表单体验:
- 邮箱 `autofillHints: [AutofillHints.email]`、密码 `[AutofillHints.password]`;
- 密码框增加显隐切换(图标按钮,语义标签走 l10n);
- 键盘动作:邮箱 `next` → 密码 `done` → 提交;提交中禁用输入与按钮;
- 错误提示统一样式,颜色取主题语义错误色,不再硬编码 `#FF6B6B`。
- 布局与可用性:小屏 + 键盘弹出不遮挡输入与提交按钮(`resizeToAvoidBottomInset` + 滚动/SafeArea);所有点击区域 ≥44pt;主按钮用 `blue`,按下态用 `bluePressed`。
- **不改**登录/注册业务逻辑、接口调用、字段校验规则与切换逻辑;**不做**忘记密码(契约无接口)、Google/Apple 登录(属 K4)、隐私协议勾选(无契约要求)。
#### K6-B 底部导航 UI 优化
- 统一使用 K5 主题 token:背景、边框、选中/未选中色全部来自 `AppColors` / `ColorScheme`,移除 `#0D0D11`、`#242329` 等硬编码。
- 视觉:选中项为主色图标 + 主色标签 + 明确的指示条/背景胶囊(二选一,保持克制);未选中为 `muted`;图标 22–24、标签 10–11,间距一致。
- 可用性:整体高度与底部安全区适配(含 iPhone home indicator),每个 tab 点击区域 ≥48pt;切换动效 ≤200ms,不引入第三方动画库。
- **保持**:4 个 tab 的数量、顺序、导航逻辑与全部 Key 不变;播放沉浸模式仍隐藏导航。
- **不做**:新增 tab、改信息架构、改页面内容。
#### K6-C 点击震动反馈
> specs 无触觉反馈规范;此为用户 2026-10-17 明确指令,按下方口径执行。客户端当前 `HapticFeedback` 调用数为 0。
- 使用 Flutter 内置 `HapticFeedback`(`package:flutter/services.dart`),**不引入第三方插件**。
- 统一封装 `lib/core/platform/haptics.dart`(如 `Haptics` 抽象 + 默认实现),页面通过依赖注入调用,便于 widget 测试替换为 fake;禁止在页面里散落直接调用静态 `HapticFeedback.*`。
- 触发范围(克制,只做关键操作):
- 底部导航 tab 切换:`selectionClick()`;
- 主操作按钮成功触发时:登录/注册提交、收藏/取消收藏、点赞/取消点赞、评论发送、举报提交、下载授权申请:`lightImpact()`;
- 危险操作确认(清空观看记录、清空下载、删除记录/授权):`mediumImpact()`。
- 不在普通列表项点击、滚动、页面切换上震动;不新增「震动开关」设置项(避免超出本批)。
- 调用不阻塞 UI:`unawaited(...)`;异常不得影响业务(触觉不可用时静默忽略,禁止空 catch 之外的吞错写法)。
- 真机验收:iPhone 上逐项可感知;iOS 低电量模式会自动禁用触觉,验收时确认未处于低电量模式。
#### K6-D 测试与验收
- widget 测试至少覆盖:
- 底部导航 4 个 Key 存在、选中态随 tab 切换、沉浸模式下隐藏;
- 登录页密码显隐切换、提交中按钮禁用、错误提示展示、键盘 next/done 焦点流转(可用 `tester.testTextInput`);
- 震动反馈:用 fake `Haptics` 断言 tab 切换触发 `selectionClick`、主操作触发 `lightImpact`、危险操作确认触发 `mediumImpact`,且普通列表点击不触发;
- 无新增硬编码色值(沿用 K5 的扫描测试口径)。
- 深色主题、小屏(iPhone SE 级别)、键盘弹出三种场景不溢出。
- 真机验收由用户执行:登录页视觉与交互、底部导航选中态与安全区。
### 三、规范约束
- 颜色只允许来自 K5 的 `AppColors` / `ColorScheme`;页面禁止硬编码色值。
- 新增文案必须 en/zh/id 三份 arb 齐全并重新生成;禁止硬编码文案。
- 保持现有 Key 与业务逻辑;函数 ≤50 行、参数 ≤5、类 ≤300 行、单文件 ≤500 行、嵌套 ≤4 层。
- 新文件首行 `// Author: fruit.com.cn`;public 类/方法文档注释;无 AI 痕迹。
### 四、门禁
1. `dart analyze lib test` 零告警;`flutter test` 全过。
2. `flutter build apk --debug` 通过。
3. 新增测试必须真红后绿;不得删改既有测试来「变绿」。
4. 真机/设备侧验证由用户执行(红线 7、10),回执给「用户可照做步骤 + 每步预期」。
### 五、Git 与留痕
- 从 K5 合并后的干净 `develop` 拉 `feature/flutter-k6-auth-nav-ui`;完成后 `git merge --no-ff` 回 `develop` 并 push;不动 `main`。
- 先写 `FruitServer/docs/plans/2026-10-17-K6.md`(范围、影响文件、风险、待确认)。
- 每步「叙述 + 命令 + 原始输出 + 解读」追加到 `docs/plans/PROGRESS.md`;完成后停住写回执。
- 提交作者与提交者统一 `Zhou Weidong <502333560@qq.com>`。
### 六、K6 完成回执(2026-10-17)
**结论**:K6-A 登录页、K6-B 底部导航、K6-C 点击震动反馈与 K6-D 测试验收均已完成
并停止。未做 K4,未接新接口,未改后端/Web/specs,未跑真机。
#### 6.1 登录页
- 新增 App 品牌区、印尼市场副标题与管理入口一致的三语文案。
- 邮箱/密码 autofill;密码显隐按钮与三语语义标签。
- 键盘 next → done → 提交,提交中禁用输入、显隐、提交和模式切换。
- 错误提示使用 `ColorScheme.error`,不再硬编码错误红。
- submit、显隐按钮、模式切换点击区均 ≥44pt;小屏键盘场景可滚动到 submit。
- 登录/注册接口、校验规则和模式切换逻辑未改。
#### 6.2 底部导航
- 背景 `AppColors.bar`、边框 `ColorScheme.outlineVariant`,移除旧深色边框/背景硬编码。
- 选中项使用 `bluePressed` 胶囊、`onPrimary` 图标、`blueSoft` 标签;
未选中使用 `muted`。
- 160ms 内置动画;64pt 容器 + SafeArea 底部适配;每个 tab 点击区 ≥48pt。
- 4 个 tab 数量、顺序、回调和 Key 不变:
`home-tabs-bar`、`tab-discover`、`tab-home`、`tab-my-list`、`tab-my`。
- `videoImmersive` 为 true 时导航继续隐藏。
#### 6.3 点击震动反馈
- 新增 `lib/core/platform/haptics.dart`:`Haptics` 抽象 +
`FlutterHaptics` 默认实现;通过 `LumaDramaApp` Provider 注入,测试使用 fake。
- 页面没有直接调用静态 `HapticFeedback.*`;静态扫描限定封装文件内部调用。
- 触发范围:
- tab 真正切换:`selectionClick()`;重复点击当前 tab 不触发。
- 登录/注册提交、收藏/取消收藏、剧集/评论点赞、评论发送、举报提交、
下载授权申请:`lightImpact()`。
- 清空观看记录、清空下载、删除记录、删除下载授权:`mediumImpact()`。
- 所有页面调用均为 `unawaited`;平台异常由默认实现静默忽略,不影响业务。
- 普通列表点击、滚动、页面跳转不触发。
#### 6.4 门禁原始结果
```
$ cd /Users/jianghaifeng/Desktop/ReelShort/ReelShort
$ dart analyze lib test
Analyzing lib, test...
No issues found!
$ flutter test
00:14 +263 ~1: All tests passed!
$ flutter build apk --debug
Running Gradle task 'assembleDebug'... 6.6s
✓ Built build/app/outputs/flutter-apk/app-debug.apk
```
`~1` 是既有 live watch 用例在 8081 未启动时按机制跳过,不是失败。
新增测试覆盖:
- 登录品牌、autofill、密码显隐、next/done、busy 禁用、语义错误色、
44pt 点击区和小屏键盘滚动。
- 底栏 4 Key、选中态切换、主题背景/边框、48pt 点击区、160ms 动效和沉浸隐藏。
- Fake Haptics 的 selection/light/medium 触发、重复 tab 不重复触发、
普通列表点击不触发。
- 静态调用扫描与平台触觉异常静默测试。
- Klein Blue 旧色值扫描继续为零。
#### 6.5 Git
- 登录页/底部导航提交:`e609e3a`
`feat(flutter): 优化登录页与底部导航`
- 震动反馈提交:`8a481d5`
`feat(flutter): 增加关键操作震动反馈`
- `--no-ff` 合并提交:`0cedaab`
`merge: 合并 K6 登录导航与震动反馈`
- 已推送:`e52d818..0cedaab develop -> develop`
- author/committer 均为 `Zhou Weidong <502333560@qq.com>`;未操作 `main`。
#### 6.6 用户真机验证步骤与预期
前置:iPhone 关闭低电量模式并确认系统触觉正常;安装最新 debug APK。
以下证据必须由用户在设备侧取得,Codex 未执行真机验证。
1. 打开登录页,检查品牌名、印尼市场副标题、输入框和主按钮。
预期:三语显示正确;按钮为克莱因蓝白字;密码眼睛按钮可切换显隐;
错误提示为系统语义红色;所有点击区易按。
2. 小屏手机打开键盘,使用邮箱键盘 next,密码键盘 done。
预期:焦点从邮箱进入密码;done 触发登录提交;键盘弹出时 submit 可滚动到可见区域。
3. 使用延迟网络或错误账号观察提交过程。
预期:提交时两个输入框、眼睛、submit 和模式切换全部禁用;加载指示可见;
完成后恢复或展示后端错误,不崩溃。
4. 在浅色/深色系统主题与小屏下检查底部 4 个 tab。
预期:4 个 Key 对应入口均存在且顺序不变;选中胶囊和指示清晰;
底部有安全区留白;无文字/图标重叠。
5. 逐一点击非当前 tab。
预期:每次真正切换触发轻微 selection 触觉;重复点击当前 tab 不再触发。
6. 登录提交、收藏/取消、剧集或评论点赞、发送评论、提交举报、申请下载授权。
预期:关键操作触发 lightImpact;触觉不影响请求速度或业务结果。
7. 清空观看记录、清空下载、删除记录或下载授权并确认。
预期:确认后触发更明显的 mediumImpact;取消确认不触发。
8. 点击普通列表项(如“我的”中的语言入口)并滚动列表。
预期:不产生震动;页面切换和滚动保持无触觉。
9. 临时开启 iOS 低电量模式后重复上述操作。
预期:系统可能禁用触觉,但业务、页面和无异常行为保持正常;
正式验收请关闭低电量模式。
设备侧截图、触觉感知结果与系统低电量测试由用户执行。
#### 6.7 iOS Xcode SPM 构建修复
- `Runner.xcscheme` 新增 “Bootstrap Flutter SPM Package” PreAction:
按 `FLUTTER_ROOT → PATH → $HOME/flutter → /opt/homebrew` 兜底解析
Flutter,执行 `flutter pub get`,找不到时明确报错并非 0 退出。
- 原有 “Run Prepare Flutter Framework Script” 保留,并改为同一兜底链解析
Flutter root 后执行 `xcode_backend.sh prepare`。
- `xmllint --noout` 通过;`xcodebuild -list` 可识别 Runner scheme 与全部
SPM source packages。
- 在已有 SPM 包时完整执行:
`xcodebuild -project ios/Runner.xcodeproj -scheme Runner -configuration Debug -sdk iphoneos build`
结果为 `BUILD SUCCEEDED`;日志确认执行 PreAction,输出
`Resolving dependencies...`、`Got dependencies!`。
- 删除 `ios/Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage`
后立即运行同一 `xcodebuild`,Xcode 在 “Resolve Package Graph” 阶段先失败,
尚未运行 PreActions:
`Could not resolve package dependencies ... folder doesn’t exist`。
因此 **scheme-only 修复无法覆盖“包从未生成”的首次冷启动**;
该冷启动自动生成场景标记为未通过验证。
- 当前可靠口径:新克隆、`flutter clean` 或手动删除 ephemeral 后,先执行一次
`flutter pub get`,再从 Xcode GUI 运行;正常构建期间 PreAction 会继续刷新包。
- 修复提交:`cc037a2956848128c4899df1f5092fea44d3620b`
`fix(ios): 刷新 Flutter SPM 包后再准备框架`
- 合并:`ac706a4`,已推送 ReelShort `develop`。
#### 6.8 导航白线与观看历史缺陷修复
- 底部导航白线:M3 默认深色 `outlineVariant` 实测亮度为 `1.0`。
已新增显式 token:
`outlineDark=#3A3C45`、`outlineVariantDark=#242329`、
`outlineLight=#8A8E99`、`outlineVariantLight=#D6D9E2`;
light/dark ColorScheme 均显式赋值,底栏边框使用主题
`outlineVariant`。
- 新增测试断言导航边框不是白色、深色亮度 `<0.2`,
并覆盖 light/dark ColorScheme token。
- 观看历史诊断:
- 只读查询 `UKZBA9IBE` 得到两条正常记录:剧 1/集 1、剧 5/集 7;
- 真实 8081 recent 返回结构正常,客户端诊断账号解析出 2 条记录并成功补详情;
- 未复现用户设备上的 401/100000、JSON 异常或详情失败;
- 当前代码缺陷是 `WatchRecordsController` 把 recent 异常统一折叠为
`progressError`,后端原文无法定位。
- 修复:controller 对 recent/详情异常打印真实类型、异常与堆栈;
`ApiException.message` 透传到页面错误态,页面新增稳定 retry Key。
- 临时真实联调诊断文件已删除,不进入提交。
- 用户设备上的具体错误仍待设备侧日志确认,未宣称根因已完全复现。
定向门禁:
```
$ flutter test test/theme test/home/home_nav_ui_test.dart test/watch/watch_records_controller_test.dart test/watch/watch_records_page_test.dart
00:01 +21: All tests passed!
```
全量门禁:
```
$ dart analyze lib test
Analyzing lib, test...
No issues found!
$ flutter test
00:13 +266 ~1: All tests passed!
$ flutter build apk --debug
✓ Built build/app/outputs/flutter-apk/app-debug.apk
```
Git:
- 修复提交:`732483465ced2b28438585a4b2f1573e9a0529cb`
`fix(flutter): 修复导航白线与观看记录错误透传`
- `--no-ff` 合并:`b7b1c50`,已推送 `ac706a4..b7b1c50 develop -> develop`
#### 6.9 已知边界
- 未增加震动开关;触觉遵循系统设置。
- 不做忘记密码、Google/Apple 登录、隐私勾选或新 tab。
- Xcode 首次包图解析早于 scheme PreActions;完全无 SPM 包时仍需先运行
`flutter pub get`,本轮未通过 scheme 实现冷启动自动生成。
- 观看历史缺陷未在用户真机上复现;需用户更新后抓
`[WatchRecordsController] recent failed` / `detail failed` 日志与页面原文。
- K4 未开始。
---
## [已处理·待用户验收] 2026-10-17 | K5 客户端视觉:克莱因蓝主题 + 印尼市场适配
**用户指令(2026-10-17)**:「改 UI 及 App 整体颜色,克莱因蓝,针对印尼市场,认真阅读文档严格执行」。
> **本批只做 K5 视觉与本地化,不做 K4,不接新接口。**
> **颜色口径**:specs 全文没有成文色板或设计稿(已检索 `AGENTS.md`、`CODING_STANDARDS.md`、`GIT_WORKFLOW.md`、`docs/01~20`、`docs/plans/*`、`FruitServer/docs/*`);
> 颜色值来自用户 2026-10-17 明确裁定的 **方案 A**,按下方色值执行,不得自行另选。
### 〇、开工前必读(按顺序)
1. `AGENTS.md`(目标市场:首发印尼)
2. `CODING_STANDARDS.md`(Dart/Flutter 红线、门禁)
3. `GIT_WORKFLOW.md`(Git Flow)
4. specs `docs/02-技术架构.md`(印尼首发)、`docs/15-成本清单与预算.md`(印尼语/Android 主导)、`docs/19-术语表.md`、`docs/16-测试用例库.md`
5. 本 INBOX 条目
### 一、PM 已勘察的现状(实测,直接用)
- 主题集中在 `lib/core/theme/app_colors.dart`:`blue #2F7BFF`、`blueSoft #8FB4FF`、`ink/bar/card/muted`。
- `lib/app.dart` 浅色与深色两套 `ColorScheme.fromSeed` 都硬编码 `seedColor: 0xFF2F7BFF`,另有 `scaffoldBackgroundColor`。
- **42 处硬编码蓝色**散落 14 个文件,分布:`mine_view.dart` 13、`discover_view.dart` 5、`player_view.dart` 4、`comment_tile.dart` 3、`drama_detail_view.dart` 3、`home_view.dart` 2、`auth_view.dart` 2、`app.dart` 2,其余 7 个文件各 1。
- `lib/core/config/locale_controller.dart` **只支持 `en` / `zh`**,选 `id` 会落成 null(跟随系统)——印尼首发缺陷。
- `app.dart` 的 `localeResolutionCallback` 在系统语言不支持时回退 `supported.first`(当前为 en),与「首发印尼」不符。
- 三语 arb 齐全:`app_en.arb` / `app_zh.arb` / `app_id.arb` 各 243 个 key。
### 二、本批范围(4 块,按顺序做)
#### K5-A 主题色系统(克莱因蓝)
色值(用户裁定,方案 A):
| Token | 值 | 用途 |
|---|---|---|
| `blue` | `#002FA7` | 主色(Klein Blue / IKB) |
| `bluePressed` | `#00227A` | 按下/激活态 |
| `blueSoft` | `#5B7BE0` | 深色背景上的高亮/次级文字与图标 |
| `onPrimary` | `#FFFFFF` | 主色上的文字/图标 |
| `ink/bar/card/muted` | 保持现值 | 深色底不改成蓝色,不破坏现有暗色风格 |
- `app_colors.dart` 增加上述 token,并把 `blue` 改为 `#002FA7`;`blueSoft` 若与现有语义冲突,明确注释用途。
- `app.dart` 两套主题改为显式 `ColorScheme`(或 `fromSeed` + 覆盖 `primary/onPrimary`),确保浅色与深色下主色一致为 `#002FA7`;`seedColor` 不得再出现其他蓝色字面量。
- **对比度硬要求**:正文文字 ≥ 4.5:1、大字号/图标 ≥ 3:1;主色大面积背景使用 `bluePressed`,主色上的文字用 `onPrimary`。
- 新增单测:至少断言 `blue == Color(0xFF002FA7)`、`onPrimary` 在白底/主色上的对比度达标。
#### K5-B 全量 UI 替换
- 把 42 处硬编码蓝色全部改为 `AppColors.*` 或 `Theme.of(context).colorScheme.*`,页面层禁止再出现 `0xFF2F7BFF` / `Colors.blue`。
- 覆盖页面:首页、发现、详情、播放(右栏/进度条)、我的、评论弹窗与评论项、观看记录、收藏、下载、登录、`AuthGate`、快捷抽屉。
- 统一控件:FilledButton/TextButton、Tab 指示器、Switch、进度条、选中态、链接、SnackBar 主色。
- **不改语义色**:错误红、成功绿、点赞红(如有)保持原语义,不因主题替换改成蓝色。
#### K5-C 印尼市场适配
- `LocaleController` 支持 `id`:`'id' => Locale('id')`,读写 `ui.locale` 持久化。
- 默认语言规则:用户未显式选择时跟随系统;系统语言不是 `en/zh/id` 时**回退 `id`**(首发印尼),不得回退 `en`。
- 语言选择入口增加 **Bahasa Indonesia** 选项(现有入口位置沿用,不新造页面)。
- 校验三语 arb:新增文案必须 en/zh/id 三份齐全;禁止硬编码文案。
- 日期/数字本地化:检查现有格式化是否按 locale(`intl`),`id` 下日期/数字格式正确。
- 不接 Google 登录、不做支付(属 K4/后续)。
#### K5-D 回归与验收
- 深色/浅色两套主题、加载/空/失败三态、小屏(iPhone SE 级别)均不破版。
- 新增 widget 测试:主题主色生效、页面无硬编码蓝色(可用静态扫描测试)、`LocaleController` 支持 id 与回退 id。
- 真机验收由用户执行:主题色、按钮/进度条/选中态、语言切换与印尼语显示。
### 三、明确不做
- 不改业务逻辑、接口调用、后端、`ReelShortWeb`、specs。
- 不做 Google 登录、支付、下载落盘。
- 不改 App 图标/启动图(无设计资源;如后续提供 Stitch 出稿再做)。
- 不替换信息架构与页面布局,只做颜色、主题与本地化;布局仅在换色后出现对比度/溢出问题时做最小修正。
### 四、规范约束
- 颜色只允许在 `app_colors.dart` 与主题层定义;页面禁止硬编码色值(测试断言除外)。
- l10n 三语齐全并重新生成;所有新增文案进三份 arb。
- 新文件首行 `// Author: fruit.com.cn`;public 类/方法文档注释;无 AI 痕迹。
- 代码规模红线:函数 ≤50 行、参数 ≤5、类 ≤300 行、单文件 ≤500 行、嵌套 ≤4 层、圈复杂度 ≤10。
### 五、门禁
1. `dart analyze lib test` 零告警;`flutter test` 全过。
2. `flutter build apk --debug` 通过。
3. 新增测试覆盖:主题 token 与对比度、无硬编码蓝色扫描、locale id 支持与回退、语言切换。
4. 真机/设备侧验证由用户执行(红线 7、10),回执给「用户可照做步骤 + 每步预期」,禁止用开发机 `curl` 冒充设备证据。
### 六、Git 与留痕
- 开工前先处理上一条诊断分支 `feature/flutter-ios-home-diagnostics`:按实际情况提交为诊断增强或清理回滚,确保 K5 从**干净的 `develop`** 拉分支(当前 develop 为 `e1147cf`)。
- 分支:`feature/flutter-k5-klein-blue`;完成后 `git merge --no-ff` 回 `develop` 并 push;不动 `main`。
- 先写 `FruitServer/docs/plans/2026-10-17-K5.md`(范围、影响文件、色值表、风险、待确认)。
- 每步「叙述 + 命令 + 原始输出 + 解读」追加到 `docs/plans/PROGRESS.md`;完成后停住写回执(含用户验证步骤与预期)。
- 提交作者与提交者统一 `Zhou Weidong <502333560@qq.com>`。
### 七、K5 完成回执(2026-10-17)
**结论**:K5-A 主题 token/ColorScheme、K5-B 蓝色收敛、K5-C 印尼语适配、
K5-D 回归均已完成并停止,未开始 K4,未接新接口,未跑真机。
#### 7.1 诊断分支收口
- iOS 首页诊断插桩已保留在未合并分支
`feature/flutter-ios-home-diagnostics`,提交
`7efbed97587dd3ed02967dcbbaa6fb85693c1883`。
- K5 从干净的 ReelShort `develop e1147cf` 拉出,未夹带诊断插桩。
#### 7.2 色值、主题与蓝色收敛
- `AppColors.blue = #002FA7`、`bluePressed = #00227A`、
`blueSoft = #5B7BE0`、`onPrimary = #FFFFFF`。
- `AppTheme.light()` / `AppTheme.dark()` 使用显式 ColorScheme,两套主题
`primary` 均为 `#002FA7`、`onPrimary` 均为白色;SnackBar 使用深蓝背景白字。
- 深色表面的小图标、链接和轨道使用 `blueSoft`;大面积选中背景使用
`bluePressed`;主按钮使用 `blue + onPrimary`。
- 页面层旧蓝 `0xFF2F7BFF`、`0xFF8FB4FF`、`Colors.blue` 扫描结果为空。
- 错误红、成功绿、点赞红等语义色未改;App 图标/启动图未改。
#### 7.3 印尼市场适配
- `LocaleController` 支持选择、持久化和恢复 `id`。
- 未显式选择时跟随系统;系统语言非 `en/zh/id` 时回退 `id`。
- “我的”语言弹层与首页快捷抽屉均增加 Bahasa Indonesia;
快捷抽屉改为 Wrap,避免 320px 小屏溢出。
- 新增 `bahasaIndonesia` 三语文案并重新生成 l10n。
- 观看记录与下载授权的日期/时间改由 `MaterialLocalizations` 按当前 locale
和系统 12/24 小时设置格式化。
#### 7.4 门禁原始结果
```
$ cd /Users/jianghaifeng/Desktop/ReelShort/ReelShort
$ dart analyze lib test
Analyzing lib, test...
No issues found!
$ flutter test
00:12 +251 ~1: All tests passed!
$ flutter build apk --debug
Running Gradle task 'assembleDebug'... 26.6s
✓ Built build/app/outputs/flutter-apk/app-debug.apk
```
`~1` 是既有 live watch 用例在 8081 未启动时按机制跳过,不是失败。
新增测试覆盖:
- Klein Blue token、白字/主色对比度、图标/深底对比度。
- 浅色/深色 ColorScheme 与 widget 实际主题值。
- `lib` 旧蓝字面量静态扫描。
- locale `id` 选择、持久化、系统语言跟随与未知语言回退 `id`。
- Bahasa Indonesia 选项、语言切换、印尼语日期/时间格式。
- 320×568 深色小屏首页与“我的”页不溢出。
#### 7.5 Git
- 功能提交:`10d448c8ba93aa2762e14babdb528805897b3992`
`feat(flutter): 应用克莱因蓝主题与印尼语适配`
- `--no-ff` 合并提交:`e52d81881c19202c7adc8d13a6f78f78e4f6182f`
`merge: 合并 K5 克莱因蓝主题与印尼语适配`
- 已推送:`e1147cf..e52d818 develop -> develop`
- author/committer 均为 `Zhou Weidong <502333560@qq.com>`;未操作 `main`。
#### 7.6 用户真机验证步骤与预期
前置:安装最新 debug APK,分别准备一套 Android 与 iOS 设备,保留系统深色/浅色
切换条件。以下证据必须由用户在设备侧取得,Codex 未执行真机验证。
1. 浅色与深色系统下分别打开首页、发现、详情、播放、我的、收藏、记录和下载。
预期:页面可读、无溢出;主按钮为克莱因蓝白字;选中态/进度条可见;
深色底上的链接和图标使用较亮的蓝紫高亮;旧亮蓝 `#2F7BFF` 不再出现。
2. 对主按钮、进度条、选中 Tab 截图并用取色工具检查。
预期:主色接近 `#002FA7`;按下/激活大面积背景接近 `#00227A`;
主色按钮文字为 `#FFFFFF`。
3. 在 iPhone SE 级小屏打开首页与“我的”,展开语言入口。
预期:三个语言选项自动换行,不挤出屏幕;底栏文字和图标不重叠。
4. 从“我的 → 语言”选择 Bahasa Indonesia,再冷启动 App。
预期:界面切为印尼语,重启后仍保持印尼语。
5. 从首页快捷抽屉选择 Bahasa Indonesia。
预期:同样切为印尼语,两个入口行为一致。
6. 清除 App 数据/重新安装,把系统语言设为法语,且不手动选择语言。
预期:App 自动使用印尼语,不回退英语。
7. 在印尼语下查看观看记录与下载授权时间。
预期:日期/时间使用印尼地区 Material 格式;系统 12/24 小时设置变化时格式跟随。
8. 未登录点评论发布/点赞/举报,登录页按钮与错误/成功提示仍清晰可辨。
预期:业务流程不变;Klein Blue 按钮、蓝色 SnackBar 与红色错误提示对比度正常。
设备侧截图、系统语言切换日志与真机结果由用户执行;开发机检查不能替代这些证据。
#### 7.7 已知边界
- 现有主体页面保持 dark-first 设计,并显式使用 `AppTheme.dark()`;
根部同时提供浅色/深色 ColorScheme,未重做信息架构或页面布局。
- App 图标、启动图、Google 登录与支付不在本批范围。
- K4 未开始。
---
## [已处理·待用户验收] 2026-10-17 | K3 客户端第三批:互动(评论 + 点赞 + 举报 + 分享)
**用户指令(2026-10-17)**:客户端继续对接接口及 UI,严格执行 specs。
> **本批只做 K3,做完停下写回执,不要连做 K4。**
> 规范以 specs 为准(`docs/plans/codex-研发任务清单.md` 阶段 A6、`docs/16-测试用例库.md` INTERACT、`docs/09-测试计划.md`);
> 接口只认 `FruitServer/docs/api-phase1.md` §1.7,**契约里没有的接口一律不许自造**。
### 〇、开工前必读(按 specs 顺序,缺一不可)
1. `AGENTS.md`(项目上下文、强制约束、自主执行与沟通节奏)
2. `CODING_STANDARDS.md`(三端红线、Dart/Flutter 专章、门禁总表)
3. `GIT_WORKFLOW.md`(Git Flow、提交格式、审查门槛)
4. `FruitServer/docs/api-phase1.md` §0 / §1.7(接口唯一权威)
5. `docs/plans/codex-研发任务清单.md`(A6)、`docs/16-测试用例库.md`(INTERACT-01~09)、`docs/09-测试计划.md`
6. 本 INBOX 条目
### 一、PM 已勘察的现状(实测,直接用)
- ReelShort `develop` `45f5f25`(K2 已合并推送),互动现状全部是**本地演示数据**:
- [engagement_controller.dart](file:///Users/jianghaifeng/Desktop/ReelShort/ReelShort/lib/core/data/services/engagement_controller.dart#L8-L8) 注释明确 `Local demo interactions ... No public/social backend`,内部维护 `liked` / `comments` 两个本地集合;
- 播放页右栏已有三个入口:点赞 `rail-like`、评论 `rail-comments`、分享 `rail-share`([player_view.dart](file:///Users/jianghaifeng/Desktop/ReelShort/ReelShort/lib/pages/player/player_view.dart#L339-L366));
- 评论弹窗 [drama_comments_sheet.dart](file:///Users/jianghaifeng/Desktop/ReelShort/ReelShort/lib/pages/player/widgets/drama_comments_sheet.dart#L28-L33) 直接 `addComment` 到本地 `List<String>`,无分页、无回复、无删除、无举报;
- 分享走 [platform_share_service.dart](file:///Users/jianghaifeng/Desktop/ReelShort/ReelShort/lib/core/data/services/platform_share_service.dart) 的系统分享,**不需要后端接口**;
- 全工程没有举报入口。
- 后端 §1.7 五个接口**均已实现**(`AppCommentController`、`AppInteractionController`),本批**不改后端**。
**契约字段(实测自后端 VO,直接用)**
- `CommentRespVO`:`id,dramaId,episodeId,parentId,rootId,userId,uid,nickname,avatarUrl,content,status,auditReason,likeCount,replyCount,reportCount,createdAt,mine,liked`(ID 按字符串解析)
- `CommentPublishReqVO`:`dramaId`(必填)、`episodeId`、`content`(必填)、`parentId`(回复时传)
- 列表 `GET /app/comments?dramaId=&episodeId=&rootId=&cursor=&size=` → `CursorResult{list,nextCursor,hasMore}`;**匿名可读**
- `POST /app/likes/toggle`:`{targetType: DRAMA|COMMENT, targetId}` → `{liked, count}`
- `POST /app/reports`:`{targetType: COMMENT|USER|DRAMA, targetId, reasonCode(必填), description?}` → 工单 id
- 敏感词三档:`BLOCK` 直接报错、`REPLACE` 替换后放行、`REVIEW` 转待审(`status=pending`,仅自己可见)
- 限流:同一剧集每分钟 5 条评论(后端判定,客户端只展示返回的 `message`)
### 二、本批范围(4 块,按顺序做)
#### K3-A 评论
- 评论列表弹窗改为服务端数据:首屏 `size=20`,`hasMore` 时展示“加载更多”,用 `nextCursor` 续拉;三态(加载/空/失败可重试)。
- 发布顶层评论:`dramaId + content`;发布回复:额外传 `parentId`(**只做一层楼层回复**,回复列表用 `rootId` 拉取,不做无限嵌套)。
- 敏感词三档处理:
- `BLOCK`:原样展示后端 `message`,不清空输入框;
- `REPLACE`:展示后端替换后的内容(以响应为准);
- `REVIEW`:该条以 `status=pending` 出现在自己的列表里,并显示“审核中”标识(文案走 l10n)。
- 删除自己的评论:仅 `mine=true` 显示删除入口,二次确认后调 `DELETE /app/comments/{id}`;失败回滚并提示。
- 未登录:列表照常可看(匿名),**发布/点赞/举报前拦截**并弹现有 `AuthGate` 登录提示,不发业务请求。
- 限流/重复提交:直接展示后端 `message`,前端不自造判断。
#### K3-B 点赞
- 剧集点赞:播放页 `rail-like` 接 `POST /app/likes/toggle`(`targetType=DRAMA`),乐观更新,失败回滚并提示;未登录先走 `AuthGate`。
- 评论点赞:评论列表项内接 `targetType=COMMENT`,用返回 `{liked, count}` 更新该项。
- **契约缺口(按此处理,不许改后端)**:`AppDramaDetailVO` / `AppDramaCardVO` 没有 `likeCount` / `liked`,客户端拿不到剧集点赞初始值。
本批**不展示剧集点赞数字**(避免假数据),只展示/切换当前会话内的 liked 状态;并在计划文档登记
「建议后端详情与卡片补 `likeCount` / `liked`(需 specs 负责人改契约)」。
#### K3-C 举报
- 入口:评论项“更多”菜单(举报评论)、详情页更多菜单(举报短剧)。**不做举报用户**(本批无用户主页入口)。
- 弹窗:原因单选 + 可选描述,提交 `POST /app/reports`;成功后提示并关闭。
- 失败透传:重复举报 `你已举报过该内容,我们正在处理`、举报自己 `不能举报自己发布的内容`,一律展示后端 `message`。
- 未登录先走 `AuthGate`。
- **契约缺口(登记并按下述降级)**:§1.7 未定义 `reasonCode` 枚举,后端不校验。本批前端固定 5 项:
`SPAM`(垃圾广告)、`ABUSE`(辱骂骚扰)、`PORN`(色情低俗)、`AD`(引流广告)、`OTHER`(其他);文案进三语 arb。
计划文档登记「建议后端补 `reasonCode` 枚举与后台映射」。
#### K3-D 分享
- 保持现有系统分享(`PlatformShareService`),**不新增后端接口**;只做回归验收(标题/简介/footer 走 l10n)。
### 三、明确不做(避免误判“没做完”)
- 拉黑(TC-INTERACT-06):§1.7 无接口,本批不做。
- 审核联动闭环(TC-INTERACT-09):属后台 `2.5 内容治理`,已实现,本批不做。
- 后台敏感词管理、评论审核、举报工单处理:后台已有,本批不动。
- 评论图片/表情/@提及/无限嵌套、我的评论列表:本批不做。
- 并发点赞(TC-INTERACT-07):由后端幂等保证,客户端不压测,只做防连点。
### 四、UI 与规范约束(严格执行 specs)
- **l10n**:所有新增文案(评论/回复/审核中/举报原因/失败提示)进 `lib/l10n/app_{en,zh,id}.arb` 三份齐全并重新生成;禁止硬编码。
- **视觉**:沿用现有深色主题、`AppColors`、播放页右栏与底部弹窗样式;不自行改主题。
- **三态**:评论列表必须有加载、空、失败(可重试);禁止白屏或假装成功。
- **分层**:新增 `comment_api / interaction_api`(放 `lib/core/data/remote/`)、`comment_repository`、`comment_controller`;页面只依赖 controller/repository 抽象;沿用 K2 的分层与注入方式。
- **ID**:全部按字符串解析(`json_numbers.dart`),禁止 `as int`。
- **不要动**:K2 的分类/记录/收藏/下载逻辑、登录注册、播放进度核心;不改 `ReelShortWeb`、`FruitServer` 后端、specs。
- 新文件首行 `// Author: fruit.com.cn`;public 类/方法必须有文档注释;无 AI 痕迹;测试命名 `should_<期望>_when_<条件>`。
- 代码规模红线:函数 ≤50 行、参数 ≤5、类 ≤300 行、单文件 ≤500 行、嵌套 ≤4 层、圈复杂度 ≤10。
### 五、门禁(不达标不提交)
1. `dart analyze lib test` 零告警;`flutter test` 全过(存量不得跑红)。
2. `flutter build apk --debug` 通过;若缺 SDK,贴原始输出并标「未验证」。
3. 新增测试至少覆盖:评论游标翻页与 hasMore、发布成功/敏感词 BLOCK/REPLACE/REVIEW、删除自己的评论与失败回滚、点赞乐观更新与回滚、举报成功/重复举报 message 透传、未登录 AuthGate、评论点赞计数更新。
4. 真机/设备侧验证由用户执行(红线 7、10),回执给“用户可照做步骤 + 每步预期”,禁止用开发机 `curl` 冒充设备证据。
5. Git:从 `develop` 拉 `feature/flutter-k3-interaction`,完成后 `git merge --no-ff` 回 `develop` 并 push;**不动 `main`**;只 `git add` 自己的文件。
### 六、节奏与留痕
- 先写 `FruitServer/docs/plans/2026-10-17-K3.md`:范围、影响文件、上述 3 个契约缺口、风险、待确认。
- TDD:每块先红灯 → 实现 → 绿灯 → 重构;每完成一块跑一次 `dart analyze lib test` + 定向测试。
- 每步「叙述 + 命令 + 原始输出 + 解读」追加到 `docs/plans/PROGRESS.md`。
- 做完停下写回执:真数据/本地数据页面清单、用户验证步骤与预期、门禁原始输出、提交号。
### 七、K3 完成回执(2026-10-17)
**结论**:K3-A 评论、K3-B 点赞、K3-C 举报、K3-D 分享回归均已完成并停止,
未开始 K4。后端、`ReelShortWeb`、specs 均未修改。
#### 7.1 真数据 / 本地数据页面清单
真数据(走契约 §1.7):
- 播放页评论弹窗:匿名评论列表、游标加载更多、顶层评论、一层回复、
BLOCK/REPLACE/REVIEW 结果、本人删除、评论点赞。
- 播放页剧集点赞:`POST /app/likes/toggle` 切换,乐观更新与服务端状态校准;
**不展示剧集点赞数字**。
- 评论更多菜单与详情页更多菜单:评论/短剧举报,固定五项原因,可选描述。
本地/系统能力:
- 剧集 liked 仅保存在当前 App 会话内。因详情/卡片契约没有 `likeCount/liked`,
App 重启或换账号后不展示服务端初始点赞状态。
- 分享继续走 `PlatformShareService` 系统分享,不新增后端接口。
- “我的 → 喜欢”只过滤当前 feed 的会话内 liked,没有契约外加“我的点赞列表”接口。
按降级方案未做:拉黑、举报用户、举报后台处理、无限嵌套、图片/表情/@、我的评论列表。
#### 7.2 门禁原始结果
```
$ cd /Users/jianghaifeng/Desktop/ReelShort/ReelShort
$ dart analyze lib test
Analyzing lib, test...
No issues found!
$ flutter test
00:12 +242 ~1: All tests passed!
$ flutter build apk --debug
Running Gradle task 'assembleDebug'... 6.8s
✓ Built build/app/outputs/flutter-apk/app-debug.apk
```
`~1` 是既有 `live_watch_flow_test` 在 8081 未启动时按测试机制跳过,不是失败。
#### 7.3 Git
- 功能提交:`d361034226ec2bc431548d45068dc60046e5e473`
`feat(flutter): 接入互动评论点赞举报`
- `--no-ff` 合并提交:`9db6935308e65342be46c81419c2992ed2df1df0`
`merge: 合并 K3 客户端互动能力`
- 举报本人错误透传修复:`1488ee8f85671de96c70347c1b17fb9c4556bd88`
`fix(flutter): 透传举报本人评论错误`
- 修复合并提交:`e1147cfcbabcdf457e427bbd118c32d37f8d9b15`
`merge: 合并 K3 举报错误透传修复`
- 已推送:`45f5f25..9db6935`、`9db6935..e1147cf develop -> develop`
- author 与 committer 均为 `Zhou Weidong <502333560@qq.com>`;未操作 `main`。
#### 7.4 用户真机验证步骤与预期
前置:启动 8081 客户端接口服务,准备可登录测试账号;在 Android 设备安装
`build/app/outputs/flutter-apk/app-debug.apk`,确认 App 使用设备可访问的后端地址。
以下证据必须由用户在设备侧取得,Codex 未执行真机验证。
1. 未登录打开任意有评论的剧集,点播放页评论入口。
预期:评论列表正常加载;空列表显示“还没有评论”;断网后显示失败与重试,不白屏。
2. 未登录点发布、评论点赞或举报,再点剧集点赞。
预期:均先出现登录提示;关闭提示后不发业务请求;登录后可重试。
3. 登录后发表顶层评论,再输入命中 `REPLACE` 或 `REVIEW` 策略的词。
预期:顶层评论出现在列表;REPLACE 展示服务端替换后的内容;REVIEW 显示“审核中”;
BLOCK 原样显示后端 message 且输入框内容保留。
4. 在顶层评论点“回复”并发布。
预期:请求带 `parentId`;回复归入该楼层,只做一层回复;点“查看回复”可展开。
5. 连续加载超过首屏 20 条的评论。
预期:按 `nextCursor` 续拉,不重复、不跳页;没有下一页时加载更多消失。
6. 点赞一条评论和当前剧集,再点一次取消。
预期:图标立即切换;评论计数在服务端响应后校准;断网时状态回滚并提示;
剧集点赞不显示数字。
7. 删除自己的评论:更多 → 删除评论 → 二次确认。
预期:成功后从列表移除;制造断网/服务失败时恢复原位置并提示。其他人的评论不显示删除项。
8. 举报评论与短剧:更多 → 举报;分别选择 `SPAM/ABUSE/PORN/AD/OTHER` 和可选描述。
预期:成功后提示“举报已提交,感谢反馈。”;重复举报原样显示后端
“你已举报过该内容,我们正在处理”;举报自己发布内容原样显示后端 message。
9. 点播放页分享。
预期:打开系统分享面板;文本顺序为标题、简介、`FruitShorts` footer;
切换 en/zh/id 后三类文案同步切换。
10. 在 Android logcat 保存评论/点赞/举报请求与响应,并保留关键页面截图。
预期:可见 `/app/comments*`、`/app/likes/toggle`、`/app/reports` 的真实响应;
设备侧取证由用户执行,开发机 `curl` 或本机返回 200 不替代本步骤。
#### 7.5 未验证与已知边界
- 真机、模拟器、设备日志、设备截图均未由 Codex 执行,状态为“设备侧取证待用户执行”。
- 剧集点赞初始状态和数字因契约缺口无法展示,App 重启后当前会话状态丢失。
- 敏感词具体词表、限流阈值、后台审核联动由后端/运营环境决定,本轮只做客户端透传与状态展示。
- 拉黑与 K4 不在本轮范围。
---
## [已处理·待用户验收] 2026-10-17 | K2 客户端第二批:分类 + 观看记录 + 收藏列表 + 下载授权(A3/A5 剩余)+ UI
**用户指令(2026-10-17)**:「现在先做客户端 对接相关接口 及ui 严格执行specs 内的要求」。
> **本批只做 K2,做完停下写回执,不要连做 K3/K4。**
> 规范以 specs 为准(`docs/plans/codex-研发任务清单.md` 阶段 A3/A5、`docs/16-测试用例库.md` CONTENT/RECORD、`docs/09-测试计划.md`);
> 接口只认 `FruitServer/docs/api-phase1.md` §1.3 / §1.5 / §1.6,**契约里没有的接口一律不许自造**。
### 〇、开工前必读(按 specs 顺序,缺一不可)
1. `AGENTS.md`(项目上下文、强制约束、自主执行与沟通节奏)
2. `CODING_STANDARDS.md`(三端红线、Dart/Flutter 专章、门禁总表)
3. `GIT_WORKFLOW.md`(Git Flow、提交格式、审查门槛)
4. `FruitServer/docs/api-phase1.md` §0 / §1.3 / §1.5 / §1.6(接口唯一权威)
5. `docs/plans/codex-研发任务清单.md`(A3/A5)、`docs/16-测试用例库.md`(CONTENT/RECORD)、`docs/09-测试计划.md`
6. 本 INBOX 条目
**硬性执行口径(摘自上述规范,违反即打回)**
- **Git**:从 `develop` 拉 `feature/flutter-k2-library` 开发;提交信息 `type(scope): 描述`(`type` ∈ feat/fix/docs/test/refactor/style/perf/chore,≤50 字符,一个 commit 只做一件事);完成后 `git merge --no-ff` 回 `develop` 再 push;**禁止直接改 `main`**;只 `git add` 自己的文件,禁止 `git add -A`/`.`。
- **TDD**:先写测试(红灯)→ 实现(绿灯)→ 重构;测试命名 `should_<期望>_when_<条件>`;核心类(repository/service/logic)必配 `*_test.dart`。
- **Dart**:`provider` binding/logic/view 三层;`dio`;全程 `async/await`,**禁裸 `then()`**;IO 失败用结果对象/控制器错误字段承接,**不把异常直接抛到 UI**;禁止空 catch。
- **代码规模**:函数 ≤50 行、参数 ≤5、类 ≤300 行、单文件 ≤500 行、嵌套 ≤4 层、圈复杂度 ≤10;除 `0/1/-1/null/空串/布尔` 外禁止魔法值。
- **注释**:每个 public 类/方法必须有文档注释;文件头写一行中文职责;新文件首行 `// Author: fruit.com.cn`;代码/注释/提交信息**不得出现任何 AI 痕迹**。
- **留痕**:每步「叙述 + 工具命令 + 原始输出 + 解读」,追加到 `docs/plans/PROGRESS.md`;做前先写 `docs/plans/2026-10-17-K2.md`(步骤/影响文件/风险/待确认)。
- **l10n 生成物**(`lib/l10n/app_localizations*.dart`)豁免作者头,但 `.arb` 必须手写三份。
### 一、PM 已勘察的现状(实测,直接用)
- `ReelShort`(`develop` `00280ca`)已接:`/app/home/feed`、`/app/discover/feed`、`/app/dramas/{id}`、`/app/episodes/{id}/play`、`/app/watch/progress`(POST/GET)、`/app/watch/recent`、`DELETE /app/watch/progress/{dramaId}`、`/app/favorites`(toggle/list/delete)、账号 4 个接口。
- **未接**:`GET /app/categories`、观看记录删除/清空、下载全部 8 个接口、`/app/me*`(属 K4)、互动(属 K3)。
- 现状缺陷:
- 发现页分类**是本地按 sections 下标硬编码**([discover_view.dart](file:///Users/jianghaifeng/Desktop/ReelShort/ReelShort/lib/pages/discover/discover_view.dart#L363)),不是 `GET /app/categories`;
- "我的-记录"只渲染**能在已加载首页/发现目录里匹配到**的剧([mine_view.dart](file:///Users/jianghaifeng/Desktop/ReelShort/ReelShort/lib/pages/mine/mine_view.dart#L634-L716)),匹配不到就消失;
- 收藏列表嵌在"我的"页签里,不能单独刷新/取消收藏;
- 下载**完全没有**。
### 二、本批范围(4 块,按顺序做)
#### K2-A 分类(A3 剩余)
- 接口:`GET /app/categories` → `[{ id: string, name: string }]`(J1:**id 是字符串**)。
- UI:发现页分类条改为**服务端分类**;点分类展示该分类的剧。
- 内容来源只允许 `GET /app/discover/feed` 的 `sections[]` 中 `code == "category-{id}"` 的分区(后端已按分类生成,见 `AppCatalogService#discoverFeed`)。
- 某分类在 discover 里没有对应分区 → 显示空态,**不要自己拼请求/自造接口**。
- 保留现有 `hot` / `new` 分区的入口与样式。
#### K2-B 观看记录页(A5 剩余)
- 接口:
- `GET /app/watch/recent?limit=20` → `[{ dramaId, episodeId, positionMs, durationMs, finished, playCount, updatedAt }]`(全部 ID 为字符串)
- `DELETE /app/watch/progress/{dramaId}`、`DELETE /app/watch/progress`(清空)
- **契约缺口(必须按下面方式处理,不许改后端)**:`recent` **不返回剧名/封面**。做法:
- 先去重 `dramaId`,再对每个 id 调 `GET /app/dramas/{id}` 补卡片;
- **并发上限 4**,结果按 dramaId 缓存(同页内不重复请求);
- 单条详情失败**不能拖垮整页**:该条显示占位(剧名"加载失败"+重试),其余正常渲染;
- 在计划文档里记为「契约缺口:建议后续在 `ProgressRespVO` 补卡片字段(需 specs 负责人改契约)」。
- UI:独立"观看记录"页(入口:我的 → 观看记录)
- 每条:封面、剧名、第 N 集、`positionMs/durationMs` 进度、更新时间;点击进详情并续看;
- 单条删除、清空(二次确认);删除**乐观更新**,失败回滚并提示;
- 未登录 → 复用现有 `AuthGate` 登录引导(接口需要登录,见 `SecurityConfig`);
- 三态:加载中 / 空("还没有观看记录")/ 失败(可重试)。
#### K2-C 收藏列表页(A5 剩余)
- 接口:`GET /app/favorites`(返回 `AppDramaCardVO[]`,**与首页卡片同结构**)、`DELETE /app/favorites/{dramaId}`、`POST /app/favorites/toggle`。
- UI:独立"我的收藏"页(我的 → 收藏),复用首页卡片渲染:
- 取消收藏(按钮或长按/左滑,二选一但要能发现)→ **乐观更新 + 失败回滚**;
- 点击进详情;空/错/载三态;未登录 → `AuthGate`。
- **契约缺口**:TC-RECORD-04 写"分页正确",但 `GET /app/favorites` **没有分页参数**、返回全量数组 → 本批按全量实现,计划文档登记该缺口,**不许自造 `page/size`**。
#### K2-D 下载授权与管理(A5 最后一块,**不做实际落盘**)
- 接口(§1.6,全部需登录;设备号优先请求体、其次 `X-Device-Id`,现有 `ApiClient` 已带):
| 方法 | 路径 | 本批用途 |
|---|---|---|
| GET | `/app/downloads/policy` | 配额/授权天数/wifiOnly/清晰度/已用/剩余/`expirePolicyText` |
| PUT | `/app/downloads/settings` | 仅 Wi-Fi、最高清晰度 |
| POST | `/app/downloads` | 申请授权(body `episodeId`,`quality` 可省=AUTO) |
| GET | `/app/downloads` | 我的下载授权列表 |
| GET | `/app/downloads/{episodeId}/url` | 续传前刷新短时效地址(本批只提供 API 方法) |
| POST | `/app/downloads/{episodeId}/progress` | 进度上报(本批只提供 API 方法) |
| DELETE | `/app/downloads/{episodeId}` | 删除单条 |
| DELETE | `/app/downloads` | 清空,返回条数 |
- UI:
- 详情页/播放页加"下载"入口:调 `POST /app/downloads`;**付费/未解锁/配额满时直接展示后端 `message`**,不要自造判断(判定归服务端);
- 我的 → 下载:策略卡(剩余配额、授权天数、`expirePolicyText`)、授权列表(剧名、集号、`status`、`taskStatus`、`progressPercent`、到期时间)、单条删除、清空、仅 Wi-Fi/清晰度设置;
- 状态枚举全部**后端返回值驱动**(`ACTIVE/EXPIRED/REVOKED`、`QUEUED/DOWNLOADING/PAUSED/COMPLETED/FAILED/CANCELED`),前端不新增枚举;
- 空/错/载三态 + 未登录 `AuthGate`。
- **明确不做**(写进计划文档,避免误判"没做完"):文件分片落盘、断点续传、离线播放、`licenseKey` 本地校验 —— 需要存储/权限/加密播放方案,另立批次。本批只交付"授权与状态管理"。
### 三、UI 与规范约束(严格执行 specs)
- **语言**:所有新增文案必须进 `lib/l10n/app_{en,zh,id}.arb` **三份齐全**并重新生成;禁止硬编码中文/英文。印尼语为首发语言。
- **视觉**:沿用现有深色主题与 `AppColors`/组件风格(specs 里未提供 Stitch 出稿;若用户后续给出稿,再单独做视觉替换,本批不自行改主题)。
- **三态**:每个新页面/列表都要有加载、空、失败(可重试);失败文案走 `errorText` + l10n,禁止白屏/假装成功。
- **分层**:新增 `category_api / download_api`(放 `lib/core/data/remote/`),页面只依赖 repository/controller 抽象;沿用 K1 的分层与依赖注入方式。
- **ID**:19 位雪花 ID 一律按字符串解析(`json_numbers.dart`),**禁止 `as int`**;计数/时长同理按契约类型处理。
- **不要动**:首页/发现已接 feed 逻辑(分类来源替换除外)、登录注册、播放/进度/收藏已有逻辑(除必要接线)、`ReelShortWeb`、`FruitServer` 后端。
- 新文件首行 `// Author: fruit.com.cn`;测试命名 `should_<期望>_when_<条件>`。
### 四、门禁(不达标不提交)
1. `dart analyze lib test` **零告警**;`flutter test` **全过**(存量 33 个测试文件不得跑红)。
2. `flutter build apk --debug` 通过(CODING_STANDARDS §6 门禁总表含构建);若本机缺 Android SDK,回执里贴失败原始输出并标「未验证」,不得静默跳过。
3. 新增测试至少覆盖:分类→section 映射(含无分区空态)、记录页 N+1 详情请求去重/并发上限/单条失败降级、删除与清空回滚、收藏取消回滚、下载策略/授权列表渲染、未登录 AuthGate;核心类覆盖率 ≥80%。
4. **真机/设备侧验证由用户执行**(红线 7、10):回执里给"用户可照做的步骤 + 每步预期结果",**禁止用开发机 `curl` 冒充设备可达**;拿不到设备证据就标「未验证」。
5. Git:`feature/flutter-k2-library` 开发 → `git merge --no-ff` 回 `develop` → `git push origin develop`;**不动 `main`**;合并/推送前再跑一次门禁。
### 五、节奏与留痕
- 先写 `FruitServer/docs/plans/2026-10-17-K2.md`:范围、影响文件清单、上述 3 个契约缺口、风险、待确认。
- **TDD 节奏**:每块先写测试(红灯)→ 实现(绿灯)→ 重构;每完成一个模块跑一次门禁,零告警才继续。
- 每步在公司 `PROGRESS.md` 追加「叙述 + 命令 + 原始输出 + 解读」(PM 验收口径,缺一即打回)。
- 做完**停下写回执**(追加到 `docs/plans/PROGRESS.md` + 本条下),回执必须含:
- 哪些页面已走真数据、哪些仍是本地/未做;
- 用户可照做的验证步骤与每步预期;
- 测试与 analyze 的原始输出;
- 提交号。
- 禅道:开工/完成各同步一次任务状态。
### 六、PM 登记:specs 契约缺口(Codex 不要动,反馈给项目负责人)
1. **搜索**:任务清单 A3 写了"搜索"、TC-CONTENT-05 有搜索用例,但 `api-phase1.md` **没有 `/app/search` 接口** → 本批**不做搜索**。
2. `GET /app/favorites` 无分页参数,与 TC-RECORD-04"分页正确"不一致。
3. `GET /app/watch/recent` 不返回剧名/封面,记录页需 N+1 详情请求(建议后端补卡片字段)。
> **用户裁定(2026-10-17)**:**先按现状契约执行,上述缺口后期再补**。本批不得因缺口停工,也不得临时改后端或自造接口;按第二节的降级方案实现,缺口留到后续批次。
### 七、Codex 回执(2026-10-17)
**状态**:K2-A/B/C/D 代码、自动化测试、Android debug 构建与 develop 推送已完成;
真机/模拟器侧业务验证**未验证**,按红线交用户执行。用户照做清单与测试跳过口径见
`docs/plans/PROGRESS.md`「动作 17:用户真机验收清单」。K3/K4 未开始。
**提交**
```
feature: cc4d7c5a9dc6a4f6f0565a32503cce5973af703c
merge: 45f5f25640a274edb51bd90b3b5e52c43ba9d9cd
branch: feature/flutter-k2-library -> develop(--no-ff,已 push,feature 已删除)
main: 未改动
```
**已接真实契约数据**
- 发现页分类:`GET /app/categories` + `GET /app/discover/feed`;
服务端分类只匹配 `category-{id}`,无分区显示空态。
- 独立观看记录页:`GET /app/watch/recent?limit=20`、按 dramaId 去重并发上限 4 补详情、
`DELETE /app/watch/progress/{dramaId}`、`DELETE /app/watch/progress`。
- 独立我的收藏页:`GET /app/favorites`、`POST /app/favorites/toggle`、
`DELETE /app/favorites/{dramaId}`。
- 我的下载页:§1.6 全部 8 个 `/app/downloads*` 接口;详情页/播放页下载入口调用
`POST /app/downloads`,拒绝时直接展示后端 `message`。
- 三页面均为加载 / 空 / 失败可重试三态;未登录显示登录引导。
**仍本地或未做**
- 搜索未做:契约没有搜索接口。
- 文件分片落盘、断点续传、离线播放、`licenseKey` 本地校验未做;本批只交付授权与状态管理。
- 点赞/评论仍沿用既有本地状态,属 K3,不在本批。
- 发现 feed 当前只下发 `category-{id}`;若后端没有 `hot`/`new` 分区,保留的入口显示空态。
- 收藏接口仍返回全量数组,本批未发送 `page/size`。
**门禁原始输出**
```
$ dart analyze lib test
Analyzing lib, test...
No issues found!
$ flutter test
00:11 +220 ~1: All tests passed!
$ flutter build apk --debug
Running Gradle task 'assembleDebug'... 2,377ms
✓ Built build/app/outputs/flutter-apk/app-debug.apk
```
`~1` 是既有真实后端联调用例因本机 8081 服务未启动而按设计跳过,不是失败。
首次冷构建也成功,耗时 `705.7s`;最终修复后缓存构建 `2,377ms`。
**用户验证步骤与预期**
1. 在 ReelShort 执行 `git checkout develop && git pull origin develop`,确认 HEAD 为
`45f5f25`;后端 8081 已启动,手机与开发机在同一局域网。
2. 取局域网地址并运行:`IP=$(ipconfig getifaddr en0)`,
`flutter run -d <device-id> --dart-define=API_BASE_URL=http://$IP:8081`。
预期:App 启动、首页/发现能加载;地址必须按当前网络重新获取。
3. 打开发现页:分类条应出现 `/app/categories` 返回的分类。选择一个有内容分类,
预期显示 discover feed 对应 `code=category-{id}` 的剧;选择无分区分类显示空态。
4. 播放任意免费集约 10 秒后返回,进入“我的 → 观看记录”。
预期:出现该剧封面/剧名/集号/进度/更新时间;即使该剧不在首页分区也能显示。
单条删除先立即消失;断网重试删除时,记录应恢复并提示失败。清空应先弹二次确认。
5. 在详情或播放页收藏一部剧,进入“我的 → 我的收藏”。
预期:卡片来自服务端;点粉色收藏按钮取消后立即消失;断网取消应恢复卡片并提示失败。
6. 登录后在详情页/播放页点下载入口。
预期:成功时提示“下载授权已就绪”;付费、未解锁或配额满时,直接出现后端 `message`。
进入“我的 → 下载”,预期策略卡显示剩余配额/授权天数/`expirePolicyText`;
列表显示后端 `status`、`taskStatus`、`progressPercent`、到期时间;
仅 Wi-Fi、清晰度、单删、清空可操作。
7. 退出登录后分别进入观看记录、我的收藏、我的下载。
预期:三页都显示登录引导,不显示网络错误或白屏。
**证据口径**
- 以上设备侧步骤尚未执行,因此真机/模拟器验证统一标记「未验证」。
- 没有用开发机 `curl` 冒充设备可达证据。
- 本批不包含实际文件下载,因此不能据此验收“离线播放”。
---
## [已处理] 2026-10-17 | 三库全量拉取 + `main` → `develop` 同步(Trae 执行,无代码改动)
**用户指令(2026-10-17)**:「先拉最新全量代码,三个库全拉,相关规范已经备齐、接口文档完备、INBOX更新、环境地址清单在docs下21-环境地址清单.md 同步main 分支到开发分支」。
### 一、执行结果(实测)
| 仓库 | 分支 | HEAD | 落后 origin/develop | 领先 | `origin/main` 未并入 | 工作区 |
|---|---|---|---|---|---|---|
| FruitServer | develop | `2d70858` | 0 | 0 | 0 | 干净 |
| ReelShort | develop | `00280ca` | 0 | 0 | 0 | 干净 |
| ReelShortWeb | develop | `3ec9ea6` | 0 | 0 | 0 | 干净 |
- 三库 `git fetch --all --prune` 后均无新提交,无需 `pull`。
- **ReelShort** 是唯一有 `main` 未同步的库:`eaf8fb4 style: 全量补充作者标识` 与 develop 改名(`nontonki`→`fruitshorts`)冲突 15 个文件。
解法:保留 develop 口径(`package:fruitshorts`)+ 按文件类型补回署名头(`.dart` → `//`,`.yaml` → `#`,`.xml` → `<!-- -->`);
`main` 版复活了 develop 已删除的 5 个文件(`test/catalog/demo_media_test.dart`、`test/catalog/home_feed_category_test.dart`、
`lib/pages/home/home_feed_sections.dart`、`lib/pages/home/widgets/home_feed_header.dart`、`lib/pages/home/widgets/category_swipe_region.dart`)→ 已 `git rm`;
`lib/l10n/app_localizations*.dart`(生成文件)不加署名头,已还原。
门禁:`dart analyze lib test` → `No issues found!`;提交 `00280ca`(作者 `jianghaifeng <haifeng_hello@163.com>`),已推 `origin/develop`。
### 二、⚠️ 待 PM / 用户确认
1. **`docs/21-环境地址清单.md` 在 specs 仓库中不存在**。已核对:specs `docs/`(仅 01~20 + README + plans + 参考规范材料清单 + 代码现状盘点)、三个代码仓 `git ls-files`、specs 全部远端分支(只有 `origin/main`)—— 均无此文件。请确认是尚未推送,还是在别的路径/仓库。
2. specs 本地仍有 22 笔提交未推送到远端(无写权限,`origin/main` 停在 `c528345`)。若以远端为准,需要决定是否把本地回退到 `origin/main`。
### 三、用户裁定(2026-10-17)
1. **specs 只有 `main`,只需同步** → 已将本地 `specs` 强制对齐到 `origin/main`(`c528345`),本地领先的 22 笔未推送提交已丢弃;此后 specs **只拉取,不提交**。
2. **specs 是唯一权威** → 规范、接口契约、任务清单一律以 specs 为准;与本仓库文档冲突时以 specs 为准。
3. **执行前置动作** → Codex 每次开始任务前**必须先读本文件(INBOX)**,按条目顺序执行;新条目由 PM 追加,不覆盖历史。
---
## [已交接 · 未验证(真机验证改由用户执行)] 2026-10-16 20:30 | K1-verify:用**正确地址**重做真机验证(设备侧取证)
**用户指令(2026-10-16 20:20)**:「让 Codex 用正确地址重做一次真机验证」。
**背景(为什么必须重做)**:你 1-3/1-4 那批的真机验证用的是
`--dart-define=API_BASE_URL=http://10.157.242.1:8081` —— 该地址是**开发机某块 VPN 网卡(utun)的地址**,
现已不存在,**手机侧没有这块网卡**,所以按它构建、安装到手机上的 App **在真机上必然请求失败**
(用户实测提示「**剧目列表加载失败**」,报错里就显示着这个地址)。
更关键的是:当时那条"证据"是**在开发机上 `curl` 本机网卡地址拿到 200** —— 本机 curl 本机必然 200,
**不构成真机可达的证据(假阳性)**。PM 已把 specs 里 4 处记录修正并作废该结论(`481d4bb`)。
### 一、重做验证(**必须设备侧取证**,见红线第 10 条)
1. **先取真实地址,不要硬编码**:`ipconfig getifaddr en0` 拿开发机局域网 IP(PM 实测当前 `192.168.31.187`;**换网络会变**)。
2. **前置条件**:手机与开发机在**同一局域网**;iPhone 已授权「**本地网络**」权限(设置 → 隐私与安全性 → 本地网络)。
3. 真机跑:`flutter run -d 00008101-000875813E42001E --dart-define=API_BASE_URL=http://<步骤1得到的IP>:8081`
4. **证据至少一条来自设备侧**(优先级从高到低):
- **首选:加设备侧集成测试**(`integration_test` 依赖 + `integration_test/` 目录),
用 `flutter test integration_test -d <device> --dart-define=API_BASE_URL=http://<IP>:8081` 在**手机进程里**
真实请求 `/app/home/feed` 与 `/app/dramas/{id}`,断言 `code=0` 且列表非空。
—— 这是可重复、可入库的最硬证据,也顺带把"设备验证"变成可回归的。
- 或:App 运行时抓**从手机 IP 到 8081 的已建立连接**(`lsof -nP -iTCP:8081 | grep <手机IP>`),
并**同时**给出后端侧确实收到该请求的证据。
- 或:手机侧截图(`device` 截图不支持无线 iPhone —— 那就用设备侧测试或连接证据替代)。
5. **若都拿不到:如实标「未验证」**,并说明卡在哪一步(同网段?权限未授权?),**不许含糊**。
6. 顺带确认原故障已消失:首页「剧目列表」能出内容(或由步骤 4 的测试断言覆盖)。
### 二、留痕要求(**只写本仓库,别动 specs**)
- `docs/plans/PROGRESS.md`(本仓库 FruitServer 这份)**追加**逐动作留痕:命令 + 原始输出 + 解读。
- 本条下写回执,含**用户可照做的验证步骤 + 每步预期结果**,并**明确标注"证据来自设备侧"**。
- **不要去改 specs 仓库**(它只拉取;那 4 处 PM 已修正,别重复改)。
### 三、门禁与 git
- `dart analyze lib test` 零告警 + `flutter test` 全过(新增的 integration_test 一起跑;若它依赖真机、无法在纯本机跑,就在回执里说明并给出真机执行命令与输出)。
- 在 `ReelShort` **`develop`** 提交并 `git push origin develop`;**不动 `main`**;新文件首行 `// Author: fruit.com.cn`。
### 四、节奏
做完**停下写回执**,不要连做下一批。
---
## [已处理·待用户验收] 2026-10-16 19:40 | K1 客户端对接:详情 + 播放 + 进度续看 + 收藏(看剧闭环)+ UI 调整
**用户指令(2026-10-16 19:30)**:「现在你要做的事对接客户端接口及 UI 调整,仍然严格按照 specs 执行」→ 转到 **客户端 `ReelShort`(Flutter)**,按 specs 阶段 A 推进。
### 一、PM 已勘察的现状(实测,直接用)
**客户端目前只调了 6 个 C 端接口**(契约里已实现 **44 个**):
```
/app/home/feed /app/discover/feed
/app/auth/register /app/auth/login /app/auth/refresh /app/auth/logout
```
- `lib/core/data/remote/` 下**只有** `api_client / api_exception / api_interceptors / auth_api / catalog_api` —— **没有** playback / watch / library / interaction / me 的 api 类。
- `WatchHistoryRepository` 是 **`SharedPreferencesWatchHistoryRepository`(本地存储)**,`mine_logic` 还在用 **`demoProfileId`** 假数据 —— 观看进度/续看位置**全在本机**,没接 `/app/watch/*`。
- `EngagementRepository` 是 `SharedPreferences` / `InMemory` —— 点赞/收藏状态**也是本地假数据**。
- 已有:`dio` 网络层 + 拦截器(A1 ✅)、改名 + 印尼语(✅)、`json_numbers.dart` 兼容字符串 id(J1 ✅)。
### 二、本批范围(K1:一条完整看剧闭环)
> 目标:**从首页点进一部剧 → 详情正确 → 能起播 → 进度能续看 → 能收藏**,全链路走真接口。
1. **详情(A3 的一块)**:新增 `catalog_api` 方法对接 `GET /app/dramas/{id}`(含剧集列表;**免费集带已签名播放地址**)。
2. **播放(A4)**:新增 `lib/core/data/remote/playback_api.dart`
- `GET /app/episodes/{id}/play` → 票据 + 签名地址。**签名地址原样使用,不要拼接/改写**(文档硬要求)。
- 起播前取票据;票据/签名地址有有效期,**过期要重新取**,不要缓存到失效后还用。
3. **进度与续看(A5 的一块)**:
- `POST /app/watch/progress`(**每 10s 一次**,切集/退出/切后台各补一次)
- `GET /app/watch/progress?dramaId=`(进入详情/播放时定位续看位置)
- `GET /app/watch/recent?limit=20`("我的"页最近观看)
- **把 `WatchHistoryRepository` 的本地实现换成远程实现**(保留抽象接口,测试注入内存假实现);`mine_logic` **去掉 `demoProfileId`**。
4. **收藏(A5 的一块)**:`POST /app/favorites/toggle`、`GET /app/favorites`、`DELETE /app/favorites/{dramaId}`;收藏状态**以服务端为准**(不要再用本地 `EngagementRepository` 存收藏)。
5. **UI 调整**(本批必须一起做):
- 详情页 / 播放页 / "我的"页:**加载中 / 空 / 失败**三态齐备(失败要能重试,不要白屏或假装成功)
- 进度条与**续看定位**、收藏按钮的真实状态与乐观更新回滚
- 断网/超时的提示文案走 l10n(en / zh / id 三份都要有)
### 三、口径与约束(照 specs)
- **ID 是字符串**(J1 刚统一):解析走 `json_numbers.dart`,不要直接 `as int`。**计数类**(`viewCount`/`expiresIn`)也是字符串。
- `accessType` 等枚举**用后端返回值驱动 UI**(免费/付费/试看),**不要在前端写死**;付费集解锁不在本批(属 B2)。
- 接口层与业务层分离:**新增 api 类**、controller/logic 只依赖 repository 抽象(沿用现有 `catalog_api` + `catalog_controller` 的分层做法)。
- **不要动**已对接的首页/发现/登录注册逻辑(除必要的最小接线)。
- 新文件首行 `// Author: fruit.com.cn`;测试命名 `should_<期望>_when_<条件>`。
### 四、门禁
- `dart analyze lib test` **零告警** + `flutter test` **全过**(存量 27 个测试文件不得跑红;新增测试要覆盖:票据过期重取、进度上报节流、续看定位、收藏切换回滚、失败态)
- **真实后端联调**(8081):跑一次端到端并留证据 —— 首页 → 详情 → 播放 → 上报进度 → 重新进入看到续看位置 → 收藏出现在收藏列表
- 在 `ReelShort` **`develop`** 提交并 `git push origin develop`;**不动 `main`**
### 五、节奏
先写 `docs/plans/2026-10-16-K1.md`(计划 + 影响面 + 风险)→ 做 → 停下写回执(含**用户可照做的验证步骤 + 每步预期结果**,以及"哪几个页面现在走真数据、哪几个还是假的"的清单)。**不要连做 K2。**
> **后续批次(预告,不用现在做)**:**K2** = 记录/最近观看页 + 收藏列表页 + 分类/搜索 + 下载授权(A5/A3 剩余);**K3** = 互动(评论/点赞/举报/分享,A6);**K4** = 账号剩余(资料/设备/注销;**Google 登录需 Google OAuth 凭据**,属待你提供的付费/密钥类)。
---
## [已处理 · 待用户验收] 2026-10-16 18:30 | J1 全局 ID 字符串化(Long → String)+ 契约统一
> **PM 独立复核(Trae 2026-10-16 19:05)—— 交付 `FruitServer f35db2c` / `ReelShortWeb 3ec9ea6` / `ReelShort 94ff9f1`(三仓均在 `develop` 且与远端同步),验证通过。**
> - **门禁复跑**:`mvn test` **110 → 115 条全过**(commerce 33 / admin-api **16→21** / app-api 49 / worker 12)+ BUILD SUCCESS ✅
> - **全局配置落位核实**:`fruitshorts-framework/.../web/JacksonLongToStringConfig.java` 存在(一处生效两端,未改 yml)✅
> - **端到端真实验证(PM 亲自跑,非采信回执)**:新注册用户雪花 id `2111018722574602241` →
> ① `/admin/app-users/page` 返回 **`"id":"2111018722574602241"`(字符串)**、**无裸数字大 id**、`"total":2` **仍是数字**(符合约定);
> ② 用该 id 调 `/admin/app-users/{id}` → **`code=0`**(修复前此场景即 `200002`);
> ③ **真 node 逐位比对**:`typeof parsed.id === 'string'`、`String(parsed.id)` 与库值**逐位相等 = true**;测试数据已清理(残留 0)。
> - **按红线第 7 条,最终验收权仍归用户。**
>
> ⚠️ **三处口径待用户点头(PM 意见附后)**:
> 1. **唯一例外 `PageResult.total` 保持数字** —— PM 意见:**接受**(它是条数不是 id,前端分页组件要数字);但需在契约 §0「ID 一律字符串」处**显式写出这个例外**,否则前端会困惑。待 Codex 补一句。
> 2. **客户端改动比"只改 id 解析"略宽(物理上必须)** —— Codex 查明 Flutter 的 id 本来就是 `toString()` 不会崩,真正会崩的是被全局规则扫到的 **Long 计数**,故只改了 `expiresIn`、`viewCount` 等 3 处 + 新增 `json_numbers.dart`。PM 认可这个判断与最小改动口径。⚠️ **另需用户知晓**:`ReelShort` 原先**只有 `main`**,Codex 新建了 `develop` 并推送;若客户端同事仍在 `main` 上开发,这笔需要带过去。
> 3. **Codex 自我纠正了一处统计错误** —— 它第一版清理脚本用 `psql ... returning 1 | wc -l` 数行数,数到的是 psql 命令标签(删 0 行也输出 1),那轮"删除 1 行"是假数字;随后用**全库扫描**复核(uid/邮箱/雪花 id 零命中)确认无残留。PM 认可其如实披露。
>
> 📌 **仍挂着**:禅道里同样**没有 J1 任务**(项目 2 下只有 S0/S1、A1–A6、B1/B2、C1–C3),无可同步状态。
>
> ⚠️ **PM 顺手说明**:为做端到端验证,8080(PID 80375)/ 8081(80383)/ 3005 均为 Codex 重启后的新 jar,接口与页面正常。
**用户裁定(2026-10-16 18:10)**:雪花 id 精度问题 **「按方案 (a) 全局修改,统一契约」**。
**背景(两方独立复现同一根因,故现在铺全局)**:
- 你在 I1 里实测到:UI 点「下架」后端回 `200002 公告不存在` —— 库里 `2111011206851596290`,浏览器 `JSON.parse` 后变 `2111011206851596300`,回查自然找不到。你把修复**限定在本轮两个模块**并留了待裁定。
- PM 独立复核同样确认:新注册用户 id `2111012753262120962` 由 `/admin/app-users/page` **以裸 JSON 数字**下发;用真 JS 引擎验证 `JSON.parse('{"id":2111012753262120962}').id` → `2111012753262121000`(**不相等**)。根因是 JS `Number.MAX_SAFE_INTEGER = 9007199254740991`,19 位雪花 id 必然超出;而服务端**没有全局 `Long→String` 序列化配置**。
- 种子数据都是小 id(4/5/25)所以老页面一直正常 —— 这是**只要"新建后立刻拿 id 操作"就必然踩**的坑。
### 一、后端:全局序列化(放 `fruitshorts-framework`,两个入口都生效)
1. `Long.class` 与 `Long.TYPE` 一律用 `ToStringSerializer`。
2. ⚠️ **不要动 `Integer` / `int`**(分页 `pageIndex`/`pageSize` 等保持数字)。
3. 必须覆盖:普通字段、**嵌套对象**、`List`/`Map` 的值、分页 `list`。
4. **反序列化方向也要验**:请求体 / 路径变量 / 查询参数里 id 传**字符串**要能正确绑定成 `Long`(前端回传的就是字符串)。
### 二、契约统一(`docs/api-phase1.md` §0 通用约定)
- 写明「**所有 ID 字段一律以字符串下发**」(含主键与外键 id),并给示例。
- 把 I1 里逐模块的描述(商品/公告 VO 各自加 `ToStringSerializer`)**收拢到这条总口径**,不要各写一套。
- 顺手把 §2.11/§2.12 里与总口径重复的表述改成引用 §0。
### 三、两个前端适配
1. **ReelShortWeb**:审计现有页面有没有把 id 当**数字**用 —— `parseInt(id)` / `Number(id)` / `id + 1` / 拿 id 当数组下标 / `===` 与数字比较 / `sort((a,b)=>a.id-b.id)`,逐处改成字符串语义。本轮新加的两个页面已按 `string` 写,**别回退**。
2. **ReelShort(Flutter)**:`fromJson` 里 `json['id'] as int` 这类写法在字符串下发时会**直接抛异常**,必须审计并修。策略二选一(推荐前者,改动小且向前兼容):
- **兼容解析**:统一按"接受字符串或数字"的方式解析(如 `int.tryParse(v.toString())`),或把模型里的 id 保留为 `String`;
- 或全量改成 `String`(改动更大,要与客户端同事对齐)。
- ⚠️ 客户端现由另一同事在推进:**只做"ID 解析"这一类最小改动**,不要顺手改别的;有分歧或改不动就写「【待 Trae 协助】」,并在回执里给出**逐文件清单**。
### 四、验证(必须有证据,不接受"应该没问题")
1. 后端契约测试:`Long` 字段确实以**字符串**输出(含嵌套 / 分页 list / Map)。
2. **端到端复现 I1 那个 bug 并证明已消失**:admin 新建一条记录 → 用响应里的 id 调详情 / 编辑 / 上下架 → **成功**(全局范围内,不再只限商品/公告)。
3. **JS 侧一致性证据**:拿真实响应喂 `node -e 'const d=JSON.parse(...); console.log(String(d.id))'`,证明与库里的值**逐位相等**。
4. C 端:注册 / 播放票据 / 订单等返回的 id 也是字符串,且**客户端解析不崩**(Flutter 侧跑测试)。
### 五、门禁与 git
- `mvn test` 存量 **110 条不得跑红** + 新增测试;Web `pnpm ts:check` + eslint 零错误 + `pnpm build:pro` 通过;若改了客户端则 `dart analyze lib test` 零告警 + `flutter test` 全过。
- FruitServer / ReelShortWeb / ReelShort **各自在 `develop` 提交并推送**;**绝不动 `main`**。
- 新写文件首行 `// Author: fruit.com.cn`(Vue/TS 同;Dart 用 `// Author: fruit.com.cn`)。
### 六、节奏
先写 `FruitServer/docs/plans/2026-10-16-J1.md`(方案 + 影响面清单 + 风险)→ 做 → 跑门禁 → **停在干净交接点写回执**,回执必须含:
- 每个仓库改了哪些文件(**客户端逐文件清单**要单独列)
- 「用户可照做的验证步骤 + 每步预期结果」
- 若客户端有未改完/有分歧的部分,**如实列出**(不许含糊)
---
## [已处理·待用户验收] 2026-10-16 17:30 | I1 后台管理端:商品(金币包)配置 + 系统公告管理(后端接口 + Web 页面)
**用户指令(2026-10-16 17:10)**:「现在做后台管理端,规范还是按照 specs」+「ReelShortWeb 就是这个项目」→ **后台管理端 = `ReelShortWeb`(Vue3 + Element Plus),本批后端接口与页面一起做**。
### 一、PM 已勘察的两个缺口(实测证据,直接用)
| # | 缺口 | 证据 |
| --- | --- | --- |
| 1 | **商品/金币包配置:后台完全没有入口** | `product` 表 **16 个字段**已建(`code/name/type/platform/store_product_id/coin_amount/bonus_coin/price_amount/currency/display_price/region_codes/vip_days/sort/status/effective_at/expire_at`)、**已有 3 行种子数据**、C 端 `/app/orders/verify` 依赖 `store_product_id` 对齐商店商品(错误码 `200002 该商品未在后台配置` 就是为它留的);但 admin-api 29 个 Controller 里**没有任何商品 CRUD 接口**(`grep` 只命中 `WalletAdminController /coins` 的发币补偿)→ 运营改商品只能改库。**PRD §09 明确要求「商品配置与价格分层」** |
| 2 | **系统公告:有读没写** | `ops_notice` 表 **0 行**;E1 已交付 C 端 `GET /app/notices`(匿名只读);契约 §2.7 原话「**系统公告 `ops_notice` 目前没有产出方,等有公告功能再接**」→ 后台没有公告管理接口,运营发不了公告 |
> 除这两项外,admin-api 对照契约 §2.1–§2.10 全部已实现,**不要重做**。
### 二、后端(FruitServer / admin-api)
1. **契约先行**(依据 `12-上线检查清单` §一):先在 `docs/api-phase1.md` 写清两个模块的接口、字段、错误码、权限点(放在 §2.6 订单与权益之后,或新增一节),**再动代码**。
2. **商品配置 `/admin/products`**:
- `GET /admin/products/page`(筛选:`type` / `status` / `platform` / `keyword`,分页沿用既有口径)、`GET /admin/products/{id}`
- `POST /admin/products`、`PUT /admin/products/{id}`、`PUT /admin/products/{id}/status`(上下架)、`DELETE /admin/products/{id}`
- 字段按 `product` 表现有结构,**不要改表**
- **必须有校验**:`code` 在全局唯一、`store_product_id` 在**同一 `platform` 下唯一**(否则同一商店商品被重复配置 → 发币口径混乱);`type=COIN` 时 `coin_amount > 0`;`type=VIP` 时 `vip_days > 0`
- ⚠️ **VIP 商品口径**:B1-1 已定「VIP 订阅本轮不做」→ 允许建但**不允许启用**(启用报明确错误)或直接拒绝创建,**二选一由你定,写进契约并在回执里说明**;拿不准就写「【待 Trae 协助】」
- 权限点命名沿用现有习惯(参考 `commerce:*`),并落 `sys_permission` + `sys_menu`
3. **系统公告 `/admin/notices`**:
- `GET /admin/notices/page`、`GET /admin/notices/{id}`、`POST`、`PUT /admin/notices/{id}`、`PUT /admin/notices/{id}/status`(发布/下架)、`DELETE /admin/notices/{id}`
- 字段按 `ops_notice` 表现有结构,**不要改表**
- **与 C 端 `GET /app/notices` 的筛选口径必须严格对齐**(E1 已定:`status=1` 且 `target ∈ {ALL, USER}`、`publish_at <= now`、`expire_at` 为空或未过期、按 `publish_at desc, id desc`)—— 后台发布后 C 端能查到、过期/下架后查不到
- 权限点 + 菜单同上
4. **菜单与权限点落库**:写进 `sql/data-phase1.sql`(**幂等**),注意**沿用既有保留段**、不要与现有 id 冲突(现有菜单到 id 30)。建议:商品配置挂 `订单与权益`(id 22) 下;系统公告可挂新的运营分组,具体由你按现有约定定。**超管必须可见且可授权**。
5. Swagger 中文注解按 S1 规范补齐(`@Tag` / `@Operation`)。
6. **门禁**:`mvn test` 存量 **91 条不得跑红** + 新增契约测试,至少覆盖:`store_product_id` 同平台唯一、`code` 唯一、`type` 校验、公告发布后 C 端可见 / 过期或下架后不可见、越权(无权限点被拒)。
### 三、后台页面(ReelShortWeb)
> **挂载机制(PM 已勘察)**:路由是**静态注册**在 `src/router/index.ts`(`component: () => import('@/views/...')`),而菜单树来自 DB `sys_menu`,其中 `component` 字段必须**与视图路径一致**(例:`views/contentManagement/DramaManagementView`)。两边都要加。
1. 新增两个页面(放在与现有页面同级的目录,命名沿用 `XxxManagementView.vue` 风格):
- **商品配置**:列表(分页/筛选)+ 新建/编辑弹窗 + 上下架 + 删除;表单字段按 `product` 表;金额/金币用数字输入并做前端校验
- **系统公告**:列表 + 新建/编辑(标题/正文/类型/目标/发布时间/失效时间)+ 发布/下架 + 删除
2. **按现有约定写**:API 封装放 `src/api/admin/`(新增如 `product.ts` / `notice.ts`);权限码走现有的 `can(code)`;**表格状态列用接口返回的 `status` / `statusText`,不要在前端写死中文**;大数值用 `formatCount`。
3. `src/router/index.ts` 静态注册路由(`route_name` 与 DB `sys_menu.route_name` 一致,**注意不要与既有 route_name 重名** —— 之前踩过 `CreatorCenter` 重名导致白屏/404 的坑)。
4. **不要改动已对接的既有页面**(避免与其他人正在做的改动冲突)。
5. **门禁**:`pnpm build:pro` 通过 + eslint 零错误。
### 四、git
- **FruitServer**:在 `develop` 提交并推送;**不动 `main`**。
- **ReelShortWeb**:现在是 `main` 分支且**与远端分叉**(本地 `4564aa8` 领先 1、远端 `1fd0b8b 全量补充作者标识` 领先 1)。请先 `git fetch` 并把远端合进来,**然后按 specs 的 `GIT_WORKFLOW` 新建 `develop` 分支,在 `develop` 上提交并 `git push origin develop`**;**不动 `main`**(合并走 PR、由用户操作)。远端是 `jianghaifeng/ReelShortWeb.git`(有写权限)。
- 作者的署名规范:`// Author: fruit.com.cn`(Vue/TS 文件首行)。
### 五、节奏(红线第 7 条)
1. 先写计划文件 `docs/plans/2026-10-16-I1.md`(后端 + 前端两部分、步骤、风险、待确认)。
2. 做,跑门禁。
3. **停在干净交接点写回执**:逐项列出改了什么;**给你和用户可照做的验证步骤 + 每步预期结果**(含「后台登录 → 菜单里能看到这两个新页面 → 建一个商品 → 用该商品在 C 端 verify 能对上」这样的端到端链路)。
4. **不要连着抢做下一批。**
---
### 回执(Codex,2026-10-16):I1 完成,停在干净交接点
**提交**:FruitServer `8d19a0c`(develop,已推) + ReelShortWeb `db313f4`(develop,已推,含 husky 门禁)。
**留痕**:`docs/plans/PROGRESS.md`(本轮新建,逐动作命令 + 原始输出 + 解读);计划 `docs/plans/2026-10-16-I1.md`。
**没碰 `main`**;01-08 冻结模块未改(改动只在 module-commerce / module-system / admin-api / app-api 的 `AppNoticeService`)。
**门禁数字(原始输出见 PROGRESS 动作 3/6/10)**
| 门禁 | 结果 |
| --- | --- |
| `mvn test` | **BUILD SUCCESS**,commerce 33 / admin-api 16 / app-api 49 / worker 12 = **110 条全过**(存量 91 + 新增 19,无跑红) |
| `mvn -pl fruitshorts-app-api,fruitshorts-admin-api -am package -DskipTests` | BUILD SUCCESS |
| `sql/data-phase1.sql` 幂等 | 连跑三遍,`sys_role_menu/permission/menu/role_permission` 行数 54/52/33/79 前后一致 |
| `pnpm ts:check` | 0 错误 |
| `eslint --ext .js,.ts,.vue ./src` | 0 error(首遍 1 个 prettier 折行,改后复跑为空) |
| `pnpm build:pro` | Build successful |
| `:3005` 真浏览器 | 两页渲染正常;1440/1280/1024 三档 `overflowX=false`;截图见下面步骤 |
**交付内容**
1. 后端:`/admin/products` 6 个接口(page/{id}/POST/PUT/{id}/status/DELETE)、`/admin/notices` 6 个接口;字段严格按 `product` / `ops_notice` 表现有结构,**未改表**。
2. 校验:`code` 全局唯一、`storeProductId` **同 platform 唯一**、`COIN_PACK` 需 `coinAmount>0`、`VIP` 需 `vipDays>0`、新建默认下架。
3. 公告可见性与 C 端**共用一份实现**:`OpsNoticeService.pageForApp(...)`,后台与 `/app/notices` 不再各写一套条件。
4. 权限点 `product:list/product:update/notice:list/notice:update` + 菜单 31(商品配置,挂 22「订单与权益」)/ 32(运营管理)/ 33(系统公告),超管已授权,幂等。
5. 前端:`src/api/admin/product.ts`、`src/api/admin/notice.ts`、`ProductManagementView.vue`、`NoticeManagementView.vue`;状态列只用接口 `statusText`,不写死中文。
**两个自己拍的口径(请复核)**
1. **VIP 商品:允许建档、不允许启用** —— `status=1` 时回 `400 会员商品在订阅功能上线前不能启用(可先建档,上线后再上下架)`。理由:上线日可以直接上下架,不必临时补商品数据;已写进契约 §2.11。
2. **⚠️ 雪花 id 以字符串下发(本轮实测抓到的真缺陷)** —— UI 上点「下架」后端回 `200002 公告不存在`,根因是 S0 换雪花主键后 id 超过 JS 安全整数范围:DB 里 `2111011206851596290`,浏览器 `JSON.parse` 后变成 `2111011206851596300`,回查自然找不到。
修复限定在本轮两个模块(`ProductRespVO` / `NoticeRespVO` 的 `id` 加 `ToStringSerializer`,新建接口返回 `String`,前端 `id: string`),**没动全局序列化、没动 app-api**,避免影响 Flutter 客户端;并补了两条防回归测试。
- 遗留:admin-api 其它模块(订单/权益/短剧…)只要"新建后立刻用 id 操作",同样会中招。**是否统一做"admin-api 的 Long → String"、app-api 要不要一起动(Flutter 解析 id 的地方会受影响)——这是平台级改动,请裁定**,我没自行铺开。
3. **菜单挂载机制与条目描述有出入(如实报出)**:实测菜单来自 DB `sys_menu`,前端用 `import.meta.glob` 按 `component` 字符串动态解析;`src/router/index.ts` 的 `asyncRouterMap` 只静态声明 dashboard/content/user/system,commerce/governance/board 都不在里面。所以本轮**没有改 `src/router/index.ts`**,只建页面 + 落菜单(`component` 与视图路径一致)。若你要求"两边都登记",我补一版。
**用户可照做的验证步骤(每步都有预期结果)**
准备(后端):`cd FruitServer && mvn -pl fruitshorts-app-api,fruitshorts-admin-api -am package -DskipTests`,然后
```
java -jar fruitshorts-admin-api/target/fruitshorts-admin-api-1.0.0-SNAPSHOT.jar # 8080
java -jar fruitshorts-app-api/target/fruitshorts-app-api-1.0.0-SNAPSHOT.jar # 8081
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8080/v3/api-docs # 预期 200
```
数据库若在干净机器上:`psql -f sql/data-phase1.sql`(可重复执行)。
1. **菜单**:`pnpm dev` → 打开 `http://127.0.0.1:3005/#/login` → `admin / admin123` 登录。
预期:左侧出现「订单与权益 → 商品配置」与「运营管理 → 系统公告」,其余菜单不变。
2. **商品页**:进 `#/commerce/products`。预期:3 行种子(coin_100/550/1200),列 = 商品编码/名称/类型/平台/商店商品 id/到账金币/价格/排序/状态/创建时间/操作;状态显示「已上架」。
3. **新建商品(正常路径)**:点「新建商品」→ 编码 `uitest_coin_500`、名称随意、平台选 Google Play、商店商品 id `UITEST_COIN_500`、金币 500、赠送 50、价格 9900 → 保存。
预期:toast「商品已创建(默认下架,请确认后再上架)」,列表新增一行且状态「已下架」(**id 是雪花 id,注意这是本轮修过的点**)。
4. **异常路径**:再新建一个同编码商品 → 预期 toast「商品编码已存在:uitest_coin_500」;把类型改成「会员(第二阶段)」、天数 30 → 保存成功(**允许建档**),再点它的状态想上架 → 预期 toast「会员商品在订阅功能上线前不能启用(可先建档,上线后再上下架)」,状态不变。
5. **C 端对齐(命令)**:把第 3 步的商品点上架,然后
```
psql -c "select id, code, platform, store_product_id, status from product where store_product_id='UITEST_COIN_500';"
```
预期:查到 1 行、`status=1` —— 这正是 `/app/orders/verify` 找商品的同一条件。
(`POST /app/orders/verify` 本机默认 `STORE_VERIFIER=disabled`,会先回 `200003 商店校验尚未配置`,这是 B1-1 的既定行为,不是商品没配上。)
6. **公告页**:进 `#/operation/notices` → 「新建公告」填标题/正文(默认「全部用户/系统」、不填时间=立即发布)→ 保存。
预期:toast「公告已发布」,行内状态「已发布」、失效时间显示「-」。
```
curl -s "http://127.0.0.1:8081/app/notices?pageIndex=1&pageSize=5" # 预期 total 1,能看到刚发的标题
```
7. **下架(异常路径的对照组)**:点该行状态「已发布」→ 预期 toast「公告已下架」、行内变「已下架」;再跑上面的 curl → 预期 `total 0`。
8. **越权**:用 `content.editor01 / admin123`(内容运营,无 `product:*`/`notice:*`)登录后直接敲 `#/commerce/products`。
预期:页面 403/无权限(后端 `{"code":403,"message":"无权限"}`);不带 token 请求 `curl -s "http://127.0.0.1:8080/admin/products/page"` → `{"code":401,...}`。
9. **清理**:把第 3/4 步的商品、第 6 步的公告删掉;核对 `select count(*) from product;` = 3、`select count(*) from ops_notice;` = 0。
(本轮自测数据我已清干净:`product` 3 行种子、`ops_notice` 0 行。)
**门禁/流程说明**:本轮所有子步骤都在同一批内完成,未抢做下一批;`mvn test` 与 `package` 都是先停服务再打包、打包后重新起服务、**对着活着的服务**做的 curl 自测(不是对停掉的服务打)。
服务当前状态:8080/8081 我这边是**已起的**(自测完成时仍在监听);若你这边连不上就是被环境回收了,按上面「准备」两条命令起回来即可。
**禅道同步(红线第 6 条,如实说明)**:我把 `水果短剧APP`(项目 2)的 4 个执行下的任务都拉了一遍,
任务只有 S0/S1/S0-16、A1–A6、B1/B2、C1–C3 —— **没有 I1(后台管理端 商品配置 + 系统公告)这一条**,
所以本轮没有可同步的任务号,我没有动任何既有任务的状态(避免误改 B1/C1/A* 这些别的批次)。
下一步请给 I1 建任务并告知任务号,我补一次状态同步(`codex01` 的 token 已可用)。
---
### 回执(Codex,2026-10-16):J1 完成,停在干净交接点
**提交**(三个仓库各自 `develop`,`main` 未动)
- FruitServer `f35db2c`(develop,已推):全局 Long→String + 契约 §0 + 测试
- ReelShortWeb `3ec9ea6`(develop,已推):4 处 id 数字语义适配
- ReelShort `94ff9f1`(**新建 develop**,已推):客户端 Long 计数兼容解析
**留痕**:`docs/plans/PROGRESS.md`(本轮追加 J1 段,含原始输出);计划 `docs/plans/2026-10-16-J1.md`。
**门禁数字**
| 门禁 | 结果 |
| --- | --- |
| `mvn test` | **BUILD SUCCESS** — commerce 33 / admin-api 21 / app-api 49 / worker 12 = **115 条全过**(存量 110 未跑红,新增 5) |
| `mvn … package -DskipTests` | BUILD SUCCESS |
| `pnpm ts:check` / eslint / `pnpm build:pro` | 0 错误 / 0 error / Build successful |
| `dart analyze lib test` / `flutter test` | No issues found! / All tests passed!(+167) |
**每个仓库改了什么**
FruitServer(后端)
- 新增 `fruitshorts-framework/.../web/JacksonLongToStringConfig.java`(`Long`/`long` → 字符串;
`PageResult.total` 用 mixin 保持数字;`PlainLongSerializer` 供 mixin 使用)
- `docs/api-phase1.md`:§0 新增「ID 字段一律以字符串下发」总口径(含示例、边界、`total` 例外),
§2.11/§2.12 改为引用 §0
- I1 的特例回退:`ProductRespVO` / `NoticeRespVO` 去掉 `@JsonSerialize`,
`ProductAdminController` / `NoticeAdminController` 的 `create` 回到 `Result<Long>`
- 测试:新增 `admin-api/.../web/LongToStringSerializationTest.java`(5 条);
`ControllerContractSupport`(admin + app 两处)挂上模块,避免契约测试假绿;
C 端 4 条 id 断言改成 `value("…") + isString()`
ReelShortWeb(4 个文件)
- `src/utils/creatorImpersonation.ts` — 代看创作者 id 全程字符串(原来 `Number(raw)`,19 位会被四舍五入)
- `src/views/contentManagement/EpisodeManagementView.vue` — `Number(route.query.drama)` → 字符串比较
- `src/views/creator-center/composables/useDramaEdit.js` — `Number(route.params.id)` → 字符串比较
- `src/views/permission/composables/useMenuManagement.js` — 顶级菜单哨兵 `0` → `'0'`(与接口的 `"0"` 对齐)
ReelShort(Flutter)逐文件清单(**只有这些,没夹带别的**)
- `lib/core/data/models/json_numbers.dart`(新增)— `jsonToInt`:接受数字或数字字符串
- `lib/core/data/models/auth_session.dart` — `expiresIn`(后端 `long`)改用 `jsonToInt`
- `lib/core/data/models/drama.dart` — `viewCount`(后端 `Long`)两处改用 `jsonToInt`
- `test/core/json_id_parsing_test.dart`(新增)— 8 条回归用例
**三个需要你确认的口径**
1. **`PageResult.total` 保持数字(唯一例外)** —— 它是条数不是 id,前端分页组件(el-pagination / Flutter 分页)
都要数字。实测 `total = 2 int`。若你要求"Long 一律字符串",删掉 `JacksonLongToStringConfig` 里那一行 mixin 即可。
2. **反向(请求方向)未做限制**:请求体 `"id":"…"`、路径变量、查询参数都接受字符串并转 `Long`(有测试)。
`Integer`/`int`(`pageIndex`/`pageSize`/`Result.code`/`totalEpisodes`/`durationSec`…)保持数字 —— 实测已确认。
3. **客户端适配范围比"只改 id 解析"宽了一点点,原因是物理上必须**:客户端 id 本来就是
`(json['id'] ?? '').toString()`,不会崩;真正会崩的是被全局规则扫到的 **Long 计数**,
所以改了 `expiresIn` 与 `viewCount` 共 3 处(`json_numbers.dart` 是新增工具)。
其余 8 处 `as num?` 对应的 Java 字段都是 `Integer`/`int`,**按最小改动原则没动**。
⚠️ **ReelShort 仓库原来只有 `main`**(无 develop),我按条目要求新建了 `develop` 并在上面提交。
客户端由另一位同事推进,若他继续在 `main` 上走,需要把这笔 `94ff9f1` 带过去。
**用户可照做的验证步骤(每步都有预期结果)**
准备:`cd FruitServer && mvn -pl fruitshorts-app-api,fruitshorts-admin-api -am package -DskipTests`,起两个 jar;
`curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8080/v3/api-docs` → 200。
1. **复现 PM 那条(最直观)**:注册一个 C 端账号
```
curl -s -X POST http://127.0.0.1:8081/app/auth/register -H 'Content-Type: application/json' \
-d '{"targetType":"EMAIL","target":"你的邮箱@example.com","password":"Passw0rd!23","nickname":"手测","deviceId":"manual-1"}'
```
预期:`"expiresIn": "7200"`(字符串)。
2. **用响应里的 user id 调后台**:`curl -s "http://127.0.0.1:8080/admin/app-users/page?pageSize=50" -H "Authorization: Bearer <admin token>"`,
拿新用户的 `id`(预期是 `"2111…"` 这种**带引号的字符串**、`total` 是**不带引号的数字**),再
`curl -s "http://127.0.0.1:8080/admin/app-users/<id>" -H "Authorization: Bearer <token>"` → 预期 `code=0`、`data.id` 与列表一致
(**修复前这里返回 `200002 数据不存在`**)。
3. **JS 逐位一致**:把第 2 步的响应存成 `r.json`,然后
```
node -e 'const d=JSON.parse(require("fs").readFileSync("r.json","utf8"));console.log(String(d.data.id))'
```
预期与 `psql -c "select id::text from app_user where uid='<uid>'"` **逐位相同**。
对照:`node -e 'console.log(String(JSON.parse("2111015748376858626")))'` → `2111015748376858620` 附近(会被改写),说明精度问题真实存在。
4. **后台界面(:3005)**:`admin/admin123` 登录 → 商品配置新建一个商品 → **点它那一行的状态/编辑**,
预期正常(toast 成功,不再"商品不存在");菜单管理编辑一个顶级菜单,预期父级下拉显示「顶级菜单」而不是 `0`。
5. **短剧管理 → 剧集管理带参跳转**:从短剧管理点某剧的「剧集」,URL 带 `?drama=<id>`,
预期剧集页自动选中该剧(改成字符串比较前这里会选不中)。
6. **客户端**:`cd ReelShort && flutter test`(预期 All tests passed);有设备时 `flutter run` 打开首页看播放量/详情
不报解析异常(后端 `viewCount` 现在下发字符串)。
7. **异常路径**:请求体里 id 传**数字**(老客户端行为)也应被接受:`PUT /admin/products/<id>/status?status=1` 用数字 id 路径同样 200。
**禅道**:J1 在禅道里同样没有对应任务(项目 2 下仍是 S0/S1/S0-16、A1–A6、B1/B2、C1–C3),未动任何既有任务状态。
**服务现状**:8080/8081 是我刚重建起回的新 jar(自测时在跑);:3005 的 dev server 也在。若被环境回收,按上面「准备」起回来即可。
---
### 回执(Codex,2026-10-16):K1 完成,停在干净交接点
**提交**:`ReelShort d7eff4c`(develop,已推;`main` 未动)。留痕:`docs/plans/PROGRESS.md`;计划:`docs/plans/2026-10-16-K1.md`。
**门禁**:`dart analyze lib test` → No issues found!;`flutter test` → **All tests passed!(188 条,存量 27 个测试文件未跑红)**;真实后端联调(8081)见下方原始输出。
**先纠正条目里的一处勘察**:客户端**详情接口早已接上**(`CatalogApi.dramaDetail` + `CatalogController.requireDetail`,详情页在用),
票据调用 `CatalogApi.playTicket` 也存在但**没有任何页面调用**。所以本轮真正做的是:把票据接进播放链路、进度从本地换服务端、
收藏换服务端、补三态 UI。
**改了什么(ReelShort,只列这一批)**
新增:`remote/playback_api.dart`、`remote/watch_api.dart`、`remote/library_api.dart`、`models/watch_progress.dart`、
`repositories/playback_repository.dart`、`repositories/favorite_repository.dart`、`services/favorite_controller.dart`、
`formatting/error_text.dart`。
改:`app.dart`(注入三个仓库 + 收藏控制器 + 登录/登出联动)、`remote/api_client.dart`(补 `delete`)、
`remote/catalog_api.dart` 与 `repositories/catalog_repository.dart`、`services/catalog_controller.dart`(票据职责搬走)、
`models/play_ticket.dart`(补 dramaId/number/title/previewSeconds)、`repositories/watch_history_repository.dart`(换服务端模型)、
`pages/{discover,drama_detail,mine,player}/*`、`l10n/app_{en,zh,id}.arb` + 4 个生成文件。
测试:新增 5 个(含 live 联调),改 8 个(含 fake 与 widget_test 的离线仓库注入)。
**怎么改的(要点)**
1. **起播前取票**:`PlaybackRepository.ticketFor()` 带 TTL 缓存(默认 10 分钟 < 后端 15 分钟签名),过期或 `forceRefresh` 时重取;
回前台发现票据过期会重新取。**签名地址原样交给播放器**(`AppConfig.mediaUrl` 只补 host,path 与 `?e=&s=` 不动,有测试钉住)。
2. **accessType 驱动 UI**:`COIN_REQUIRED/VIP_REQUIRED` → 锁定提示 + "金币解锁即将开放";`NO_MEDIA` → 暂无片源;
前端不推断能不能播(契约 §1.4 的硬要求)。付费解锁仍属 B2。
3. **进度**:服务端每剧一条记录,`POST /app/watch/progress` 节流从 5 秒改成**10 秒**,切集/退出/切后台各补一次,播完带 `finished`;
续看位置只在"记录里的那一集"生效(其它集从头播);`demoProfileId` 与本地 SharedPreferences 实现**已删除**。
4. **收藏**:`FavoriteController` 以服务端返回为准,乐观更新 + 失败回滚 + 失败提示;退出登录清空(不跨账号串联)。
5. **三态**:详情页(加载 / 失败可重试 / 空剧集);播放页(票据加载中 / 失败可重试 / 后端判定不可播);
我的页(续看读取失败可重试 / 收藏列表加载中·失败·空 / 最近观看加载中·失败·空)。断网与超时文案走 l10n(en/zh/id 各 8 个新键)。
**原始联调输出(8081)**
```
① /app/home/feed → dramaId=5
② /app/dramas/5 → episodes=3;第一集 id='7'
playUrl=/files/episode/202610/cec39d8e….mp4?e=1792144372&s=8791826a…
③ /app/episodes/7/play → episodeId='7' dramaId='5' accessType=FREE accessible=True
④ POST /app/watch/progress (42s) → positionMs="42000" durationMs="228000" playCount=1
⑤ GET /app/watch/progress?dramaId=5 → episodeId='7' positionMs='42000' ← 续看位置读得回来
⑥ GET /app/watch/recent?limit=20 → 条数=1(dramaId='5' episodeId='7')
⑦ POST /app/favorites/toggle → true;GET /app/favorites → 1 条;DELETE → 0 条
$ flutter test test/integration/live_watch_flow_test.dart → All tests passed!
```
自测数据已清干净:`app_user` 只剩 PM 那条(id=28)、`watch_progress` 0、`user_favorite` 0。
**哪些页面现在走真数据、哪些还是假的**
| 页面/能力 | 数据来源 | 状态 |
| --- | --- | --- |
| 首页 / 发现页 | `/app/home/feed`、`/app/discover/feed` | ✅ 真数据(本批未动) |
| 登录 / 注册 / 刷新 / 登出 | `/app/auth/*` | ✅ 真数据(本批未动) |
| **详情页(剧集列表、可播地址)** | `/app/dramas/{id}` | ✅ 真数据 |
| **详情页"继续观看"与剧集进度条** | `/app/watch/progress?dramaId=` | ✅ 本批接上(原来读本机) |
| **播放页(票据/签名地址/起播判定)** | `/app/episodes/{id}/play` + accessType | ✅ 本批接上 |
| **播放页进度上报(10s 节流)** | `POST /app/watch/progress` | ✅ 本批接上(原来存本机) |
| **我的页:收藏列表 + 收藏按钮** | `/app/favorites*` | ✅ 本批接上(原来存本机) |
| **我的页:最近观看** | `/app/watch/recent` | ✅ 接上;**只渲染能在已加载目录里按 id 匹配到的剧**,匹配不到先不显示(见待确认 2) |
| 我的页:点赞 | 本地 `EngagementController` | ❌ 仍是本地假状态(属 **K3** 互动) |
| 播放页:评论 | 本地 `DramaCommentsSheet` | ❌ 仍是本地(属 **K3**) |
| 播放页:解锁付费集 | — | ❌ 本批不做(属 **B2**,票据已按 `accessType` 提示) |
| 记录页 / 收藏列表页 / 分类搜索 / 下载 | — | ❌ 属 **K2** |
**用户可照做的验证步骤(每步预期结果)**
准备:后端两个 jar 起着(`admin=200`、`app=200`);`cd ReelShort && flutter run`(模拟器用
`--dart-define=API_BASE_URL=http://10.0.2.2:8081`;iOS/桌面用默认 127.0.0.1)。
1. **匿名可看**:不登录进首页 → 点任一剧 → 详情正常出剧集列表;点某集能起播(免费集)。
预期:起播前有一瞬"加载票据",无白屏;付费集显示"第 N 集已锁定 + 金币解锁即将开放"。
2. **登录后进度**:用「我的 → 登录」注册/登录一个邮箱账号 → 进一部剧 → 看 20~30 秒 → 返回详情。
预期:详情页的"继续观看"按钮下方出现进度条与"续看至 mm:ss / mm:ss",剧集行上也有一条进度条。
3. **跨进程续看(真数据验证)**:杀进程重开 → 进同一部剧 → 直接点"继续观看"。
预期:从上次的位置继续播(不是从头)。也可以直接查库:
`psql -c "select drama_id, episode_id, position_ms from watch_progress order by updated_at desc limit 3;"` 预期有该剧记录。
4. **收藏**:播放页右侧点收藏(或详情页右上角)→ 打开「我的 → Watchlist」。
预期:该剧出现在收藏列表;再点一次取消 → 从列表消失。断网时点收藏 → 提示"收藏操作失败,请重试"且图标回到原状态。
5. **异常路径**:把后端停掉(`lsof -tiTCP:8081 -sTCP:LSLISTEN | xargs kill`)后进详情/播放。
预期:详情页给"网络异常,请检查网络后重试"+ 重试按钮;播放页给"此视频暂时无法播放"+ 重试;
我的页收藏区给失败文案 + 重试;**都不白屏、也不假装成功**。恢复后端后点重试即可继续。
6. **一套命令验证(不用模拟器)**:
`cd ReelShort && flutter test test/integration/live_watch_flow_test.dart` → 预期 All tests passed(服务没起会 skip)。
**待确认 / 如实说明**
1. **票据过期用的是客户端 TTL(10 分钟)**,因为契约里 `play` 响应没有 `expiresAt` 字段。后端若补上过期时间,改成按字段判断即可(一行)。
2. **`/app/watch/recent` 只返回 dramaId/episodeId/位置,没有剧名封面**,所以"我的页最近观看"目前按 id 去已加载的目录卡片里匹配;
匹配不到的条目先不显示。**K2 的记录页**建议要么后端补卡片字段,要么做成独立请求(已在计划里记为风险 4)。
3. **`WatchHistoryRepository` 的接口签名被改了**(服务端是"每剧一条续看记录",没有 profile 概念),连带改了 4 个 logic 与 6 个测试文件——
这是接口模型决定的,不是顺手重构;`clampResumePosition` 等本地工具函数保留。
4. **未登录时**进度与收藏接口一律不发(匿名浏览/试看不受影响),收藏按钮沿用"先引导登录"。
5. 禅道里仍**没有 K1 任务**(项目 2 下只有 S0/S1、A1–A6、B1/B2、C1–C3),未动任何既有任务状态。
---
### 回执(Codex,2026-10-16):K1-verify —— **未验证(真机验证由用户执行,Codex 未执行)**
**结论先说**:按用户 20:45 指令「codex 不用跑测试 我来执行」,本批的**真机验证状态 = 未验证**,
由用户执行后以其结果为准。我**没有**执行任何最终真机验证,也**没有**用任何"开发机 curl"当作设备可达证据。
**交付物(ReelShort `develop`,已推)**
- `pubspec.yaml`:dev_dependencies 加 `integration_test`(sdk)—— 唯一的新依赖
- `integration_test/app_backend_test.dart`(新增,首行 `// Author: fruit.com.cn`):两个设备侧用例
1. 在手机进程里请求 `/app/home/feed` 与 `/app/dramas/{id}`,断言列表非空、详情 id 与卡片一致;
2. 用**真 App**(真网络、真目录控制器)启动,轮询到首页底部标签栏出现 —— 即"剧目列表加载成功",
也就是用户在真机上遇到的「剧目列表加载失败」是否消失。
- 该文件**按用户要求保持原样,不再改动**。
**当前机器状态(交接前已清理)**
- 我起的 `flutter run` / `xcodebuild` / lsof 轮询进程**已全部终止**(`pgrep -fl "flutter_tools|xcodebuild"` 只剩 IDE 常驻 daemon,非构建)。
- `build/` 里留着我被叫停那次构建的半成品 → **用户从 `flutter clean && flutter pub get` 起跑即可清掉**。
- 后端 8080 / 8081 仍在跑(`admin=200` / `app=200`)。
**用户可直接复制执行的命令(⚠️ 本项目 iOS 用 SPM,没有 Podfile,不要跑 `pod install`)**
```bash
# 0) 起点:清掉残留构建产物(我被打断的那次留下的)
cd /Users/jianghaifeng/Desktop/ReelShort/ReelShort
flutter clean && flutter pub get
# 1) 取开发机真实局域网地址(不要硬编码;换网络会变)
IP=$(ipconfig getifaddr en0) && echo "API_BASE_URL=http://$IP:8081"
# 2) 设备侧集成测试(最硬的证据,跑在手机进程里)
flutter test integration_test -d 00008101-000875813E42001E --dart-define=API_BASE_URL=http://$IP:8081
# 2b) 只想跑网络那一条
flutter test integration_test/app_backend_test.dart -d 00008101-000875813E42001E \
--plain-name "should_fetch_home_feed_and_drama_detail_when_device_reaches_backend" \
--dart-define=API_BASE_URL=http://$IP:8081
# 3) 真机手动看 App(可选,验证首页列表与交互)
flutter run -d 00008101-000875813E42001E --dart-define=API_BASE_URL=http://$IP:8081
```
(本次实测 `ipconfig getifaddr en0` = **192.168.31.187**;若换网络请以命令输出为准。)
**前置条件**
1. 手机与开发机在**同一局域网**(例如都在 192.168.31.x)。
2. iPhone 已授权「**本地网络**」权限:设置 → 隐私与安全性 → 本地网络 → 打开本项目(App 名 **FruitShorts**)。
没授权时 App 请求局域网会失败;`Info.plist` 里 `NSLocalNetworkUsageDescription` 与
`NSAllowsLocalNetworking` 都已配好(上一批加的),所以系统应当会弹权限询问。
3. 同一时刻**只跑一个** flutter 构建(并发写同一个 `build/` 会互相破坏产物 —— 这正是本次
`video_player_avfoundation…bundle: No such file or directory` 的疑似成因)。
**设备侧证据看什么(红线第 10 条)**
- 测试输出里必须出现**由手机进程打印**的:
- `[device] API_BASE_URL=http://192.168.31.187:8081`(证明手机上用的是局域网地址,不是开发机环回/VPN 地址)
- `[device] 首页分区=4 剧目=3` / `[device] 详情 id=5 剧集=3`(证明设备真的拿到了数据)
- `[device] 首页剧目列表已渲染:API_BASE_URL=http://192.168.31.187:8081`(证明真 App 首页列表出内容)
- 最后一行 `All tests passed!`
- 若失败:`reason` 里会带上 `API_BASE_URL=…`,据此区分"地址不对"还是"设备连不上/权限未授权"。
**如实说明(失败与历史记录,详见 PROGRESS 的 K1-verify 段)**
- 我在 17:56 跑过一次,**那次是设备侧输出且全过**(上面那几行 `[device]` 就是它的原始输出)。
- 随后两次执行失败:17:59 那次 `The Dart VM Service was not discovered after 60 seconds`(app 没 attach,我也没抓到任何手机→8081 的连接);
18:03 那次 `flutter run` 构建完成后 PM 报产物缺失(`video_player_avfoundation…bundle: No such file or directory`)并叫停。
- 因此**在用户重跑之前,本批一律按「未验证」计**;上面那次的历史输出只作参考,不作为结论。