docs: G1 gate closure recorded in master plan and P5 draft

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-21 10:17:16 +08:00
parent eb8983a06c
commit 5cdf828ff5
5 changed files with 641 additions and 1 deletions
@@ -13,6 +13,12 @@
android:label="photo_live"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<!-- MuMu 等模拟器的 Vulkan 模拟不完整,Impeller 硬件渲染首帧无法呈现
(正常模式启动卡启动页;flutter test 因软件渲染不受影响)。
G2 真机验证时评估恢复 Impeller。 -->
<meta-data
android:name="io.flutter.embedding.android.EnableImpeller"
android:value="false" />
<activity
android:name=".MainActivity"
android:exported="true"
+1 -1
View File
@@ -72,7 +72,7 @@ Schema 所有权:国内库表(users、albums、photos、upload_sessions、ou
|---|---|---|---|---|---|
| P1 底座 + 国内核心链路 | compose 一键起环境;密码登录→建相册→上传会话→核验→处理→自动发布→观众页瀑布流/大图/短期签名 URL 刷新(读 cn 公开 API);可见性状态机与单张隐藏/软删除;seed 与脚本化 E2E;admin 最小页(后段) | T02 T05(后端部分)T06 T07,T11 部分 | A01(API 与后台级,App 端随 P3)A02 A03 A06 A13(凭证设计级,OSS 生命周期全量核验随 G1/P5);A07/A08 基础级 | 回归全绿 + 验收记录;可选 G1 | G1(可选) |
> **进度:P1 整体完成(P1a + P1b),2026-09-18,tag `p1`**。P1a 验收记录见 docs/plans/progress/P1a-验收记录.md(195 测试全绿,A01-A03/A06/A07/A13 证据落档);P1b 验收记录见 docs/plans/progress/P1b-验收记录.md(A08 基础级 / A12 部分 / R09 / P1a 终审修缮补登,全仓 324 测试绿,含跨端 Playwright 联调冒烟)。**G1(可选关口)正式登记跳过**:真实 OSS 凭证未提供,按出口约定「无凭证显式跳过并登记」处理(本地 MinIO 已覆盖 presign/head/禁止覆盖/受控复制全语义,凭证到位后可补跑)。
> **进度:P1 整体完成(P1a + P1b),2026-09-18,tag `p1`**。P1a 验收记录见 docs/plans/progress/P1a-验收记录.md(195 测试全绿,A01-A03/A06/A07/A13 证据落档);P1b 验收记录见 docs/plans/progress/P1b-验收记录.md(A08 基础级 / A12 部分 / R09 / P1a 终审修缮补登,全仓 324 测试绿,含跨端 Playwright 联调冒烟)。**G1 补跑完成(2026-09-21,eb8983a)**:P1 时因凭证未提供登记跳过;2026-09-21 用户交付真实 OSS 凭证(live-album @ oss-cn-beijing),裸 SDK + 生产适配器两级冒烟全部通过(presign 直传/head 核验/签名 GET 往返/受控复制幂等/禁止覆盖守护),寻址形态定案 virtual-host——唯一剩余子项:控制台人工确认 Bucket 生命周期规则不影响 `photo-live/` 前缀(需求 §5 红线)
| P2 双地域仿真 + 互动 | ov-app + outbox/inbox 同步(版本、墓碑、高优先级下线、补漏对账);点赞批次幂等;WebSocket 气泡(自身批次过滤、后台暂停);浏览统计窗口去重;统计快照回传;观众页切换至 ov-api 并新增最新/热门排序 | T25(仿真部分)T26 T13 T19 T27 T24,T11 完成 | A09 A10 A14 A21 A26 A27(仿真级) | 回归全绿 + 验收记录;D07/D11 初值确认 | 无 |
> **进度:P2 完成,2026-09-18,tag `p2`**。验收记录见 docs/plans/progress/P2-验收记录.md(A09/A10/A14/A21 部分/A26/A27 仿真级 + A25 部分(本地直读)证据落档,全仓 448 测试绿,含跨地域六幕集成套件);**D07/D11 已给初值**(30min 浏览窗口;2s 推送 / 5min 对账 / 60s 统计回传),建议维持、P5 实测冻结。
@@ -1,5 +1,7 @@
# P5 真实环境接入 实施计划
> **T1(G1 关口)已于 2026-09-21 完成**(eb8983a):真实 OSS(live-album @ oss-cn-beijing)裸 SDK + 生产适配器两级冒烟全部通过;寻址形态定案 virtual-host(详见 .env.production.example-cn 注释)。**G1 唯一剩余子项**:控制台人工确认该 Bucket 无影响 `photo-live/` 前缀的生命周期规则(需求 §5 红线)。凭证已由用户提供(建议 P5 稳定后轮换,因经过聊天渠道)。
> **状态:草案——待 G5 凭证到位后评审转正。** 本草案由 P5-prep(2026-09-21,
> tag `p5-prep`)产出:全部准备件(镜像/编排/RUNBOOK/监控/CDN 清单/压测框架)
> 已就位,凭证到位后按序执行并按实际环境修正本计划再转正。
@@ -0,0 +1,482 @@
# UI-01:App 与 JS 悬浮组件体验优化实施方案
版本:1.0
日期:2026-09-21
状态:待执行;本文件中的复选框均表示待完成工作,不代表已经实施。
目标执行者:Claude 或其他接手本仓库的开发者。
关联:[实施总纲](2026-09-17-实施总纲.md)、[需求说明书](../01-需求说明书.md)、[技术实施方案](../03-技术实施方案.md)、[P3 验收记录](progress/P3-验收记录.md)、[P4b 验收记录](progress/P4b-验收记录.md)。
## 1. 执行目标与使用方法
在现有功能基础上完成 App 与嵌入组件的产品化体验升级:工作人员能凭图片识别相册与照片,快速拍摄上传、定位异常;网站观众能在不被遮挡的情况下浏览轮播、打开相册。保留现有权限、上传恢复、多语言、海外读取和版本同步机制。
执行顺序:先阅读本文件与当前仓库说明,执行 U00 基线核对,再依次完成 U01–U09。每个任务交付可运行增量,记录改动、验证和剩余问题。不要只修改颜色后宣布完成,也不要把本方案写成另一份计划代替实现。
本文件给出了无需再次询问的设计默认值。常规组件划分、命名和排版由执行者决定。只有缺少真实资源、现有权限语义发生实质冲突或必须突破本文件范围时,列出具体依赖;继续完成不依赖该条件的工作。不得虚构接口、数据、图片或测试结果。
本文是范围明确的执行计划,不要求安装指定插件或使用特定子代理。实际执行仍遵循用户当次授权和适用的仓库规则。
### 1.1 当前基线
- 总纲登记 P1–P4b 本地阶段完成,P5 准备件完成;这不表示真实 OSS/CDN、用户源及双平台真机验收已完成。
- Flutter 已有登录、相册、拍摄、上传队列、上传历史、语言设置及原生上传桥接。
- App 主题仅定义 Material 种子色;相册卡片以字段和开关为主;管理照片列表使用占位图标、照片编号和文件大小。
- 嵌入组件已有 loader、iframe 轮播、收起、语言、轮询退避和版本判断。
- 管理后台已有展示位设置表单,但没有本方案要求的完整视觉预览。
- 本方案基于代码与文档静态核对。运行时截图、真机表现和性能数据必须在实施时补证据。
### 1.2 范围
| 必须完成 | 本阶段不包含 |
|---|---|
| App 统一主题、相册列表/详情、图片管理、上传中心、拍摄/表单/分享/设置页整理 | 重写上传引擎、原生传输和国内海外架构 |
| 受权限保护的管理照片预览、相册封面摘要等必要契约补齐 | AI 修图、自动翻译、视频、公众账号体系 |
| 悬浮组件布局、交互、状态、可访问性、宿主适配 | 全站观众页和管理后台全面重新设计 |
| 展示位视觉配置与本地预览 | 真实部署、域名变更、云资源操作 |
| 中英文、截图检查、针对性回归 | 全局品牌重命名、替换应用包名与桥接通道 |
保留当前英文品牌显示,若仓库已有正式品牌资产则复用。没有确定资产时使用现有英文名称和通用图片图标,不自行把 Framevia 等候选名称批量写入产品。界面仍支持中英文;“英文品牌”不等于强制全部界面使用英文。
## 2. 实施保护规则
1. 开始前检查 Git 状态并记录已有改动。编制时已有 AndroidManifest.xml 修改以及 demo-seed.mjs、g1-oss-smoke.mjs 未跟踪文件;实施时重新核对,不覆盖、清理或提交他人改动。
2. 保留账号隔离、权限校验、上传幂等标识、数据库持久队列、凭证续签、原生事件确认和恢复逻辑。
3. 不把“上传完成”改成“已公开”。传输、核验、处理、公开是不同事实。
4. 不通过观众公开 API 获取管理端隐藏照片,不把永久 OSS 地址放进页面。
5. 不新增重型 UI 框架;Flutter 复用 Material 与 Riverpod,Web 复用 Vue;loader 保持零运行时依赖。
6. 兼容已有 API 消费者、已有展示位与脚本。新增字段采用兼容扩展,更新生成类型,不直接编辑生成文件。
7. 所有用户文案进入现有语言资源。日志、代码变量、数据库状态不能直接作为用户提示。
8. 测试使用隔离环境;运行前检查脚本是否清库、seed 或覆盖配置。禁止对真实业务数据库执行测试初始化。
9. 此阶段不自动发布、推送、修改云配置或创建正式版本标签。提交行为按用户授权执行。
10. 旧的后端正确性问题单独登记:补漏分页未推进、发布覆盖删除状态、同步签名防重放等。本阶段不把它们默认视为已解决,也不顺手扩大成后端全面重构。
## 3. 视觉规范(默认执行值)
### 3.1 基础样式
| 元素 | 浅色默认值 | 使用要求 |
|---|---|---|
| 品牌/主要操作 | #1A73E8 | 主要 CTA、选中态;每个操作区一个主要动作 |
| 页面背景 | #F6F8FB | 列表与工作区背景 |
| 内容表面 | #FFFFFF | 卡片、表单、底部操作区 |
| 主文字 | #172033 | 标题、关键数字 |
| 次文字 | #5E6878 | 时间、地点、说明 |
| 分隔线 | #E5EAF1 | 轻分隔,不用粗边框 |
| 成功 | #16794B | 已上传等完成事实 |
| 等待 | #996300 | 等待网络、等待调度 |
| 错误 | #B42318 | 需要用户处理的失败 |
- 间距:4/8/12/16/24/32;页面左右默认 16。
- 卡片圆角 16,输入框与按钮 12,小标签 6–8;避免所有元素都做成胶囊。
- 页面标题 22–24,分组标题 17–18,正文 14–16,辅助文字 12–13。字号必须随系统缩放。
- App 主要按钮高 48–52;图标按钮有效触控区至少 44×44;图标本身可为 20–24。
- 状态同时用文案和图标表达,不能仅靠颜色。
- 深色主题使用对应语义色,不直接反转图片;图片不加彩色滤镜。沿用已有跟随系统逻辑。
- App 轻量状态动效约 150–220ms;轮播约 250–350ms;减少动态效果开启时禁用非必要动效。
- 正文对比度目标 4.5:1,重要控件和焦点清晰可见;实施时实际验证。
### 3.2 组件组织
修改 `apps/mobile/lib/app/theme.dart`,建立统一 ColorScheme、TextTheme、按钮、输入框、AppBar、NavigationBar、Dialog、BottomSheet 主题。可在 `apps/mobile/lib/ui/` 新增 tokens 与公共组件,例如 StateBadge、EmptyState、ErrorState、PhotoThumbnail、SectionHeader。
只抽取真正跨页面复用的组件,避免为每个 SizedBox 建一层封装。Web 在 viewer 样式目录建立 embed 专用变量,loader 外层通过受控参数同步;禁止污染宿主全局 CSS。
## 4. App 页面规格
### 4.1 导航与相册首页
保留三个页签,显示为“相册 / 上传 / 设置”,英文为 Albums / Uploads / Settings。沿用 IndexedStack,保持页面状态。
相册卡片顺序:封面 → 活动标题 → 时间/地点 → 公开与上传状态。标题最多两行,正文完整标题在详情可见。移除主卡片的 publicToken 以及下载/原图四开关堆叠,完整配置仍可在详情设置访问。
```text
Albums +
┌───────────────────────────────────────┐
│ 活动封面 │
├───────────────────────────────────────┤
│ Canton Fair · Autumn │
│ Sep 21–25 · Guangzhou │
│ Public Uploads on │
└───────────────────────────────────────┘
Albums Uploads · 3 Settings
```
- 手机单列卡片;平板按可用宽度使用两列,每张卡片不得挤压英文标题。
- 无封面显示固定比例中性占位,网络失败显示替代图与可重试状态。
- 不为每张卡片单独请求相册详情。摘要响应批量提供封面信息。
- 标题按当前界面语言 → 相册默认语言 → 首个有效翻译回退。
- 活动时间使用相册时区,不用设备时区悄悄改变日期;详情明确时区。
- 上传页签角标表示当前账号尚未结束的任务数,明确统计范围;不跨账号显示。
- 不添加没有后端支持的全量搜索或“我的/协作”筛选。本轮保留现有分页,若增加筛选必须另列兼容 API 任务,禁止只筛已加载页却声称全量。
### 4.2 相册详情
布局:紧凑封面与活动信息 → 上传/公开状态 → 照片入口或照片预览 → 次级管理入口;底部固定“拍摄上传 / 从相册选择”,避让系统安全区。
- 拍摄是主要按钮,选择照片为次按钮;目标相册与分类清晰可见。
- 编辑、成员、分类、分享进入工具菜单或次级区域,不再全部用同等强调的按钮。
- 公开关闭不等于上传关闭,分别显示;上传关闭时说明原因并禁用上传操作。
- 创建者和管理员可管理相册;协作者仅显示自己被授权的内容与动作。
- App 列表统计如果使用海外回传值,附最近更新时间,不声称实时在线人数。
- 无照片时显示“拍摄第一张照片”入口;正在处理不显示为空且无解释。
### 4.3 照片管理与大图
将 `album_photos_page.dart` 文本列表替换为默认三列网格;极窄屏两列,平板按最小格宽扩列。
- 网格使用缩略图,允许 cover 裁切;大图使用 contain 完整展示。网格角标仅提示处理中、失败、隐藏、删除等必要状态。
- 点开大图,支持已加载照片间左右切换,临近边缘按现有游标加载更多;关闭后回到原网格位置。
- 大图详情显示上传者、活动时区时间、尺寸/大小,ID 放诊断区域。
- 隐藏、恢复、删除操作沿用权限;删除确认明确影响云端照片,与本机任务记录清理区分。
- 本轮单张操作必做。批量照片管理不新增到 App,以免牵连现有批量权限与部分成功语义;后续单独排期。
- 隐藏照片允许有权限的管理者预览。已删除照片默认不签发预览,显示删除占位;后续若需要回收站预览,独立定义权限。
- 未准备好缩略图时显示处理状态。签名 URL 过期允许一次受控刷新,不无限重试。
- 协作者“自己的照片”通过受权限控制的数据接口实现;不能先获取他人数据再前端过滤。现有列表若只允许创建者/管理员,按第 6 节增加明确 own 范围支持。
### 4.4 上传中心
保留当前相册/时间/状态筛选、缩略图、重试、跨设备历史。顶部增加当前筛选范围的任务摘要,并标注范围。
```text
Uploads History
Today · This device
82 completed 3 uploading 13 waiting 2 need attention
All / In progress / Needs attention / Completed
[图] IMG_001.jpg Uploading 68%
━━━━━━━━━━━
[图] IMG_002.jpg Waiting for network
[图] IMG_003.jpg File unavailable Select again
```
| 内部状态 | 界面含义 | 行为 |
|---|---|---|
| preparing | 正在准备照片 | 保留已有取消能力 |
| queued | 等待上传 | 保留取消 |
| uploading | 正在上传,传输进度 | 不伪造暂停能力 |
| uploadedConfirming | 正在确认上传 | 不确定进度,不显示已公开 |
| completed | 上传完成 | 可移除本机记录;不表示海外已可见 |
| waitingNetwork | 等待网络 | 橙色说明,自动恢复,保留已有重试 |
| preparationFailed | 无法准备文件 | 重新选择,不能普通重试冒充恢复 |
| retryableFailed | 上传失败 | 本地化原因与重试 |
| cancelled | 已取消 | 保留清理记录 |
- 摘要按任务数统计,单任务百分比按字节进度;不把两者混成不明口径的总百分比。
- “一键重试”仅作用于已有可重试任务,文案说明范围。
- 已完成任务清理按钮改为“移除记录”,明确不会删除 OSS 照片。
- 不把网络等待和上传成功使用相同状态颜色。
- 摘要使用现有聚合能力或本地数据库聚合,不把全部任务加载到内存计数。
- 进度保持行级订阅,不让整个列表随每个字节回调重建。
- 本机队列与服务端历史采用不同标题和说明;不要将两者相加成去重后的全局数量。
### 4.5 拍摄、表单、分享、设置
- 拍摄:深色取景,顶部相册/分类,底部清晰快门与入队计数。入队成功用短暂反馈,不每张弹确认。保留现有权限拒绝和相机失败恢复。
- 创建/编辑:分组为活动信息、多语言内容、访问设置;默认语言必填,另一语言页签按需填写。沿用现有校验,不把未填写文案自动翻译。保存按钮在键盘打开时可达。
- 分享:名称、二维码、链接操作清晰分层;无现成封面也能使用。复制链接必做;系统分享仅在现有依赖可用或新增依赖经过实际兼容验证时实施,否则明确登记,不放无效按钮。
- 设置:账号信息、语言、密码、上传帮助、版本分组;退出登录保留任务数据与账号隔离语义。不要在退出弹窗承诺后台会继续上传。
- 后台帮助说明区分系统限制、用户强制退出和网络等待;根据真实权限状态提示,不固定显示“已开启后台上传”。
## 5. 悬浮组件规格
### 5.1 两种形态
展开卡片:标题栏 → 4:3 图片舞台 → 活动名称与页码/入口。桌面默认宽 320px,小档 280px。旧配置 sm/md 保留兼容语义;本轮 md 统一为 320px,列入视觉变更说明。
```text
┌────────────────────────────┐
│ Event photos − │
├────────────────────────────┤
│ │
│ 活动图片 │
│ │
├────────────────────────────┤
│ Canton Fair · Autumn │
│ 03 / 12 Pause View album│
└────────────────────────────┘
```
收起入口:图片图标 + Photos/照片;有效高度至少 44px。窄屏默认收起,用户展开后不因轮询再次收起。记住会话内偏好,配置默认值只在没有用户选择时生效。
照片点击打开当前单图深链;View album 打开相册首页。继续保持用户手势触发新页、noopener/noreferrer 和相同语言参数。不要自动跳转宿主。
### 5.2 图片与动效
- 默认 object-fit: contain,中性深色舞台,完整显示合影和海报;cover 作为配置可选项。
- 舞台比例固定,高度不随图片横竖改变。标题允许两行,入口文本不能被裁掉。
- 轮播默认 5000ms,合法范围保持 3000–30000ms;淡入默认 300ms。
- 用“当前序号 / 当前轮播队列数量”替代无限圆点;不得把队列数量标为相册照片总数。
- 仅当前图和下一张预加载;旧图最多保留一个过渡层,避免同时请求整队列图片。
- 新图 decode 成功再切换;失败保留上一张并尝试后续图片,循环失败有上限和退避。
- 用户暂停、指针悬停、键盘焦点在组件内时停止自动切换;离开后仅在用户未手动暂停时恢复。
- 减少动态效果时取消过渡,默认暂停自动切换并提供手动上一张/下一张。
- 页面隐藏、组件收起/不可见停止轮询和切换;恢复立即校准一次,避免重复启动计时器。
### 5.3 状态与可访问性
| 状态 | 展示要求 |
|---|---|
| 初次加载 | 等比例静态骨架,收起入口可用 |
| 成功 | 图片、活动名、序号、暂停和相册入口 |
| 空相册 | 照片即将上线/Photos coming soon,精简占位 |
| 临时断网,有缓存 | 保留可用图片、安静退避,不显示技术错误 |
| 临时断网,无缓存 | 紧凑暂不可用状态,可收起,不永久骨架 |
| 签名失效 | 刷新元数据后有限重试,避免破图闪烁 |
| 停用/解绑/相册不可公开 | 移除卡片和收起入口,不留无效按钮 |
- 将“用户收起”与“服务端终止展示”区分为不同消息语义;新增 destroy 消息时双向严格检查 origin/source/type。
- 隐藏轮播帧不能进入 Tab 顺序或读屏内容;优先只渲染当前可交互帧,过渡层设置不可交互及辅助技术隐藏。
- 所有操作使用 button/a 语义,提供中英文标签,保留可见焦点,不用轮播消息持续打断读屏。
- 图片没有真实描述时用本地化的“活动照片,第 N 张”等上下文标签,不编造画面描述。
- 修复 loader 内联 SVG 的 fill="%23ffffff" 为有效颜色;关闭按钮视觉可小,有效点击区至少 44×44。
### 5.4 宿主适配与配置
配置优先级:系统默认 < 展示位配置 < script 显式 data-* 覆盖;用户会话收起/暂停选择高于默认值。禁止解析器把“参数缺省”立即变为显式覆盖,导致后台设置永远不生效。
| 配置 | 默认值 | 生效范围 |
|---|---|---|
| position | right | left/right,兼容现有 |
| size | md | sm=280、md=320 |
| intervalMs | 5000 | 图片切换,兼容现有 data-interval |
| lang | auto | 保留现有相册默认语言回退 |
| collapsedDefault | false | 手机小于 480px 默认 true;用户选择优先 |
| imageFit(新增) | contain | contain/cover |
| offsetX(新增) | 16 | 0–120px |
| offsetBottom(新增) | 24 | 0–240px,叠加设备安全区 |
| zIndex(新增) | 1000 | 1–2147483000;用于宿主协调 |
- CSS 宽度取配置宽与视口剩余宽度的较小值,保证 320px 宿主无溢出;窗口变化和横竖屏切换时重新布局。
- zIndex 无法自动解决所有第三方弹窗冲突,文档提供宿主配置方法;禁止承诺自动识别所有客服或 Cookie 横幅。
- 多 slot 同侧时纵向排列并保留间隔;可用高度不足时收起后续实例,不让多张卡片重叠。维持独立用户偏好。
- loader 不向宿主注入全局字体和标签选择器,使用独立容器内联样式;可聚焦控件显式设置必要基础样式,测试宿主 button 全局 CSS 干扰。
- 外层位置/尺寸等后台配置经 iframe 校验后通过白名单 config 消息交给 loader;只允许上述字段和范围,不传任意 CSS/HTML。loader 再验证一次。
- 明确 configVersion 变化后的动态应用规则,旧请求不能覆盖新配置。脚本显式覆盖的字段不会被后端刷新冲掉。
### 5.5 轮询与版本兼容
- 轮播与取数彻底解耦:生产轮询基准 30000ms,沿用现有退避;轮播 3000ms 不能导致每 3 秒取数。
- 保留 ETag、configVersion、旧响应丢弃以及相册换绑清队列语义。
- 304 路径不能导致签名图片永久失效:核对签名有效期与 ETag 策略,过期刷新必须能取得新 URL;必要时提供明确的强制刷新路径及请求上限。
- 新增配置 JSON 字段使用默认回退,不要求全量改写旧记录。更新契约、验证、国内管理与海外投影消费者。
- 展示位未获授权的用户不能通过预览或配置接口读取内部数据。保持当前 admin-only 管理范围,不在本轮悄悄扩展展示位授权体系。
- 老 loader、老 iframe 与新服务过渡期仍可工作;新增消息被旧端忽略时基础展示不应崩溃。
- loader 构建并 sync 到 viewer,不手改 dist。检查现有 Nginx 缓存策略:旧 v1 URL 若长期缓存,不直接假定部署后全部刷新。采用带内容哈希的发布文件及生成代码引用,保留旧 v1 兼容入口;正式 CDN 刷新由 P5 执行,不在本轮操作云资源。
## 6. 必要接口与数据调整
先检查 `packages/contracts/openapi.yaml`,优先复用已有能力。以下是目标语义;只有不存在等价字段时才新增。新接口最终路径与 operationId 在 U01 完成后登记到验收记录。
### 6.1 相册封面摘要
- 在 AlbumSummary 兼容增加可选的 cover 字段(推荐 `{url, expiresAt, width, height}` 或 null,与已有 SignedUrl 结构复用)。
- 数据源优先已配置的有效封面;缺失时返回 null,本轮不对每张卡片扫描最新照片自动生成封面。
- 时间地点使用现有翻译字段,未填不占一整行空白。
- 列表按页批量查询资产,不引入每相册一次数据库/HTTP 查询。
- Flutter 模型允许旧响应无 cover;旧客户端忽略新字段不受影响。
### 6.2 管理照片预览
- 管理列表条目兼容增加 nullable preview,包含 thumb 与 browse 的短期签名 URL、过期时间;复用现有照片宽高。
- 权限:管理员全部、创建者本相册、协作者仅自己上传。若现有列表限定管理者,新增 `scope=own` 的显式查询语义;旧请求权限保持不变。
- scope=own 过滤在服务端完成,相册成员资格每次检查;无权查看的照片不签发 URL。
- 增加或复用有鉴权的单图预览刷新端点,大图分页仍使用管理列表,不改走观众公开接口。
- 没有有效变体、已删除照片返回 null 与真实状态。隐藏照片只给有权限的管理者签名地址。
- 日志不记录完整签名 URL。签名有效期遵循现有策略;取消权限后停止新签发,旧地址有效期是明确的有限撤销窗口。
- 不新增 OSS 公共读权限,不把原图下载能力混入浏览预览;水印等策略沿用现有授权和变体约定。
### 6.3 不在本轮虚构的数据
- 没有照片数/浏览数摘要接口时,不在卡片上放假数字,也不以已加载条数冒充总数。
- 没有海外逐照片确认状态时,不显示“海外已同步”。
- 没有可靠活动运行状态时,用 Event photos,不自动显示 LIVE。
- 没有新的业务批次标识时,上传摘要标注“今天/当前筛选”,不称为“本次活动全部上传”。
## 7. 文件改动地图
| 区域 | 主要文件/目录 | 目标 |
|---|---|---|
| 契约 | packages/contracts/openapi.yaml 及生成脚本/类型 | 预览、封面、组件配置兼容扩展 |
| 国内相册 | services/api/src/albums/albums.service.ts、assets.service.ts、dto.ts | 封面摘要、批量资产组装 |
| 国内照片 | services/api/src/photos/photos.service.ts、album-photos.controller.ts、photos.controller.ts、dto.ts | own 范围和受控预览 |
| 组件服务 | services/api/src/embed/、services/api/src/embed-ov/ | 新配置校验及公开响应 |
| 同步 | services/api/src/sync/、sync-inbox/ 中相关展示位投影 | 新配置完整传递、旧事件兼容 |
| App 主题 | apps/mobile/lib/app/theme.dart、app.dart、新增 ui/ | 主题与公共组件 |
| App 相册 | lib/pages/albums_tab.dart、album_detail_page.dart、album_photos_page.dart | 图片主导布局、大图、权限操作 |
| App 上传 | lib/pages/home_page.dart、queue_tab.dart、server_history_page.dart、capture_page.dart | 导航、汇总、反馈 |
| App 辅助页 | lib/pages/album_form_page.dart、share_qr_dialog.dart、login_page.dart、password_change_page.dart | 表单及样式统一 |
| App 数据 | lib/core/api_client.dart、features/albums/models.dart、features/photos/models.dart | 新字段与刷新调用 |
| App 语言 | apps/mobile/lib/l10n/app_zh.arb、app_en.arb | 双语文案,按工具链生成 |
| loader | packages/embed-loader/src/params.js、loader.js、README-embed.md | 配置、响应式、生命周期 |
| iframe | apps/viewer/src/pages/EmbedPage.vue、embed/useEmbedFeed.ts、feed.ts、postMessage.ts | 轮播、焦点、取数与展示解耦 |
| 后台预览 | apps/admin/src/views/system/embed-slots/SlotFormDialog.vue、EmbedCodeDialog.vue,相关 types/api/utils | 配置、预览、代码生成 |
表中 App 的 lib 相对路径均以 apps/mobile 为根。文件如果在实施前已移动,追踪实际调用链,不复制一套旧实现。
## 8. 可执行任务清单
### U00:基线与现状截图
- [ ] 读取总纲、P3/P4b 验收、当前 scripts/package.json;记录 Git 状态。
- [ ] 运行现有本地环境,采集相册首页、详情、照片页、上传、拍摄、表单、分享、设置、组件展开/收起、后台配置截图。
- [ ] 建立验收记录 `docs/plans/progress/UI01-验收记录.md`,写入实际基线 commit、环境与外部关口。
- [ ] 如果模拟器/服务不可用,说明原因并继续契约与静态实现,不伪造截图。
出口:当前行为、已有改动、可用环境明确。
### U01:契约与数据配合
- [ ] 列出复用字段与必须新增字段,落实第 6 节及配置优先级。
- [ ] 修改 OpenAPI、校验、实现、生成类型、Flutter 数据模型。
- [ ] 完成权限测试:管理员/创建者/协作者/移除成员/跨相册访问/隐藏/删除。
- [ ] 检查封面和预览批量组装、URL 刷新及旧字段缺省兼容。
- [ ] 对组件新配置完成国内→海外投影验证。
出口:有真实数据支撑后续界面,旧消费端可用;不得先用假图片掩盖缺失接口。
### U02:设计基础与导航
- [ ] 实现主题、公共状态/空态/缩略图组件、中英文基础文案。
- [ ] 调整底部导航与任务角标,保留页面/账号状态。
- [ ] 相册列表完成一页可运行样板,同时检查浅色/深色、中英文。
出口:视觉规范已在真实页面体现;后续页面复用同一套组件。
### U03:相册与照片工作流
- [ ] 完成封面卡片、详情主次操作、底部上传入口。
- [ ] 完成照片网格、大图切换、签名刷新与返回原位置。
- [ ] 完成有权限的隐藏/恢复/删除,协作者 own 列表。
- [ ] 校验分页、图片缺失、长标题、处理失败、上传关闭。
出口:工作人员可凭图找照片,上传入口清楚,权限不退化。
### U04:上传与拍摄
- [ ] 完成当前筛选摘要、状态分组、明确错误操作、移除记录文案。
- [ ] 优化拍摄界面和入队反馈;保留原生通道与任务状态机。
- [ ] 验证准备失败、断网、重试、确认中、退出切账号、历史记录。
- [ ] 检查列表滚动与上传并行时,不发生整页高频重建。
出口:异常可理解且可处理,没有“上传完成=公开”的误导。
### U05:辅助页面统一
- [ ] 整理登录、密码、创建/编辑、分享、成员/分类、设置及服务端历史页样式。
- [ ] 表单键盘、双语切换、默认语言回退、保存失败均有可用反馈。
- [ ] 深色、大字体和窄屏无溢出;系统分享按第 4.5 节处理。
出口:App 所有主要路径风格统一,不只优化首页。
### U06:嵌入轮播与 loader
- [ ] 完成两形态、完整图片模式、页码、暂停/手动切换、加载和失败状态。
- [ ] 完成配置合并、动态 config 消息、收起偏好和 destroy 生命周期。
- [ ] 解耦轮播与轮询;处理焦点、减少动态效果、预加载和过期 URL。
- [ ] 实现窄屏/安全区/窗口变化/多实例布局;修复图标颜色。
- [ ] 保留并测试消息白名单、iframe sandbox、外链安全。
- [ ] build + sync loader,落实带哈希产物与旧入口兼容,更新接入说明。
出口:第三方页面可接入,不遮挡关键内容、不泄漏焦点到隐藏帧。
### U07:后台配置与预览
- [ ] 编辑表单提供左右设置、尺寸、轮播间隔、语言、收起默认、图片适配、偏移和层级。
- [ ] 表单旁或下方提供桌面/手机预览,未保存修改即时更新本地预览。
- [ ] 预览使用本地示例数据或授权管理数据,不写展示位、不上报浏览次数、不跳转真实观众页。
- [ ] 预览与生产组件共享展示结构,避免复制两套不同样式;数据获取与展示拆分。
- [ ] 明确“保存”才更新线上配置;接入代码只包含用户选择的显式覆盖,默认让后台配置生效。
出口:后台预览与实际效果一致,配置修改确实传到现有宿主页。
### U08:回归与视觉验收
- [ ] 完成第 9 节矩阵,修复实现造成的回归。
- [ ] 对照基线采集优化后截图;记录设备/视口/语言/主题。
- [ ] Android 可用环境执行真实链路冒烟;iOS 没有 macOS/设备时明确待验收,不能以 Dart 测试代替。
- [ ] 记录结果、未执行项、原因、影响与下一步;不引用旧阶段测试数量作为本阶段结果。
### U09:文档与交接
- [ ] 更新总纲增加 UI-01 状态,不将 P5 标记完成。
- [ ] 修正直接涉及的旧状态描述;D06/D07/D11 等分别标注“已有工作默认”与“待正式确认”,不擅自替用户确认。
- [ ] 完成验收记录、截图索引、API 变更、脚本兼容和已知问题列表。
- [ ] 总结文件、运行方式、测试、剩余真机关口。无需依赖对话上下文即可接手。
任务依赖:U00 → U01 → U02 → U03/U04/U05;U06 依赖 U01 配置契约;U07 依赖 U06;U08 汇总全部;U09 收尾。这里表达工作依赖,不要求并行代理。
## 9. 验收矩阵与命令
### 9.1 验收编号
| 编号 | 必须验证的结果 |
|---|---|
| UI-A01 | App 浅色/深色一致,无硬编码浅色导致不可读 |
| UI-A02 | 相册有/无封面、长标题、中英文回退、时间地点正确 |
| UI-A03 | 图片网格与大图真实可用,分页及返回位置正确 |
| UI-A04 | 隐藏照片权限受控,删除照片不继续签预览,协作者无法看他人照片 |
| UI-A05 | 上传状态、筛选汇总、重试、清理记录不改变队列语义 |
| UI-A06 | 连拍与上传同时进行,成功提示不打断拍摄 |
| UI-A07 | 320/390/430 逻辑宽度与平板、文字 1.0/1.3/2.0 倍主要操作可达 |
| UI-A08 | 组件 320/390/768/1440px 宿主无横向溢出,安全区与偏移生效 |
| UI-A09 | 横图/竖图/全景/文字海报可看完整,换图不导致卡片跳高 |
| UI-A10 | 收起/展开/暂停、后台、键盘焦点、减少动态效果行为正确 |
| UI-A11 | 相册换绑、配置修改、旧响应、304、URL 过期、404/410 正确 |
| UI-A12 | 双来源真实跨源测试通过,非法 origin/source/config 消息被拒绝 |
| UI-A13 | 后台未保存预览不改变线上数据,不增加浏览统计 |
| UI-A14 | 老脚本与旧记录缺字段不崩溃,显式覆盖与后台配置优先级正确 |
| UI-A15 | 各端中英文键与占位符一致,运行时长文案无裁切 |
| UI-A16 | 未运行的真机/云资源项明确列为待验证,不宣称生产验收完成 |
### 9.2 有意义的自动化
- API 集成:管理预览权限矩阵、own 过滤、签名刷新、旧响应兼容、无 N+1 查询。
- Flutter widget:主要布局和关键状态;有稳定渲染环境后给核心页面增加少量 golden,不为每个间距写脆弱测试。
- Flutter 现有集成:登录→相册→拍摄/选择→队列→历史;修改视觉后维护语义定位,不靠坐标点击。
- loader 单测:缺省/显式参数区分、上下界、合并优先级、配置消息校验。
- viewer 单测:暂停和恢复、请求计时器独立、失败图片、签名刷新、非当前帧不可聚焦。
- Playwright:真实不同 origin 的宿主与 iframe(两个端口即可),至少一次模拟全局 button CSS、客服浮层和多 slot;不能继续只用同源测试替代跨源验证。
- 性能记录:保存改造前后首屏请求数、组件预加载数、loader gzip 大小;上传中列表滚动使用 profile 观察。不得捏造“提升百分比”。
### 9.3 执行入口
在仓库根运行现有 Web 检查;先定向再全量,环境缺失与真实失败分开记录:
```text
pnpm lint
pnpm typecheck
pnpm test
pnpm --filter @photo/embed-loader build
pnpm --filter @photo/embed-loader sync
pnpm --filter @photo/viewer build
pnpm --filter @photo/viewer test:e2e
```
Flutter 在 `apps/mobile`,使用仓库固定的 Flutter 工具链:
```text
flutter gen-l10n
flutter analyze
flutter test
```
OpenAPI 生成、后端集成、admin E2E、mobile IT 的命令从对应 package.json 与现有 scripts 核对后运行;不猜不存在的脚本。全量测试可能初始化数据库,必须先确认指向专用测试环境。记录实际命令、退出码、结果和跳过原因。
## 10. 验收记录模板
在 `docs/plans/progress/UI01-验收记录.md` 填写:
1. 基线与完成 commit(未提交则记录工作树状态),日期与执行环境。
2. U00–U09 完成表,未完成项原因。
3. UI-A01–UI-A16:通过/失败/待验证、证据位置。
4. API 新字段与权限变化、旧客户端兼容结果。
5. App 各页、组件两形态、后台预览的前后截图索引。
6. 测试命令与真实结果,性能观察而非推测。
7. 既有改动保护情况、未触碰的外部环境。
8. 方案偏差及理由;仍需真机、OSS/CDN、用户接口的事项。
完成定义:规定页面和组件实现可运行、必要接口已落地、针对性回归通过、视觉证据可核查、剩余外部关口清晰。不以“代码生成完”“本地构建通过”单独替代完成定义。
## 11. 可直接交给 Claude 的执行指令
> 请执行 `docs/plans/2026-09-21-UI01-App与悬浮组件体验优化实施方案.md`,从 U00 开始核对当前仓库,按 U01–U09 实际完成 App 与 JS 悬浮组件的界面和体验调整。沿用文档默认设计,不重复询问常规样式选择。保护当前未提交改动,不重写上传引擎,不绕过管理预览权限,不虚构数据或测试结论。接口缺口按文档兼容扩展;每步执行相应验证。遇到真机或外部环境依赖,明确登记并继续不受影响的工作。最终提供可运行实现、UI01 验收记录、截图证据和剩余事项,不只返回计划。未经另外授权不要发布、推送、修改云资源或创建正式版本标签。
+150
View File
@@ -0,0 +1,150 @@
// 演示数据种子:登录 admin → 建双语演示相册 + 分类 → sharp 生成 12 张真实照片 →
// 全链上传(presign PUT → complete)→ 等 worker 发布 → 建 embed slot 绑相册。
// 产出:观众页 URL + 嵌入宿主页 URL。可重复运行(相册名带时间戳)。
import { execSync } from 'node:child_process';
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const sharp = require(
require('path').join(process.cwd(), 'services/api/node_modules/sharp'),
);
const CN = 'http://127.0.0.1:8080';
const stamp = new Date().toISOString().slice(11, 19).replace(/:/g, '');
const j = (r) => r.json();
const api = async (path, opts = {}) => {
const r = await fetch(CN + path, {
...opts,
headers: { 'content-type': 'application/json', ...(opts.headers || {}) },
});
if (!r.ok && r.status !== 201 && r.status !== 202 && r.status !== 204)
throw new Error(`${path} → ${r.status}: ${await r.text()}`);
return r.status === 204 ? null : j(r);
};
// 1. 登录
const login = await api('/v1/auth/password', {
method: 'POST',
body: JSON.stringify({
account: process.env.ADMIN_ACCOUNT || 'admin',
password: process.env.ADMIN_PASSWORD || 'admin-secret-change-me',
}),
});
const auth = { Authorization: `Bearer ${login.sessionToken}` };
console.log('✓ admin 登录');
// 2. 双语相册 + 分类
const album = await api('/v1/albums', {
method: 'POST',
headers: auth,
body: JSON.stringify({
translations: [
{ locale: 'zh-CN', title: '演示相册 · 现场直击', description: '这是本地演示环境生成的相册,照片由脚本真实上传并经 worker 处理发布。', venue: '广州·广交会展馆' },
{ locale: 'en', title: 'Demo Album · Live', description: 'Generated by the local demo seed; photos really uploaded and processed.', venue: 'Canton Fair Complex' },
],
defaultLocale: 'zh-CN',
startAt: new Date().toISOString(),
}),
});
const cat = await api(`/v1/albums/${album.id}/categories`, {
method: 'POST',
headers: auth,
body: JSON.stringify({
sortOrder: 1,
translations: [
{ locale: 'zh-CN', name: '开幕现场' },
{ locale: 'en', name: 'Opening' },
],
}),
});
console.log(`✓ 相册 ${album.publicToken} + 分类`);
// 3. 12 张真实照片(不同色块/比例,横竖混排)
const palettes = [
['#e74c3c', '#c0392b'], ['#3498db', '#2980b9'], ['#2ecc71', '#27ae60'],
['#f39c12', '#e67e22'], ['#9b59b6', '#8e44ad'], ['#1abc9c', '#16a085'],
['#e91e63', '#ad1457'], ['#3f51b5', '#303f9f'], ['#00bcd4', '#0097a7'],
['#ff5722', '#e64a19'], ['#607d8b', '#455a64'], ['#8bc34a', '#689f38'],
];
const dims = [[1600, 1067], [1200, 1600], [1600, 900], [1080, 1350]];
const ids = [];
for (let i = 0; i < palettes.length; i++) {
const [w, h] = dims[i % dims.length];
const [a, b] = palettes[i];
const svg = `<svg width="${w}" height="${h}"><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="${a}"/><stop offset="1" stop-color="${b}"/></linearGradient></defs><rect width="${w}" height="${h}" fill="url(#g)"/><text x="50%" y="50%" font-family="sans-serif" font-size="${Math.round(Math.min(w, h) / 6)}" fill="rgba(255,255,255,.85)" text-anchor="middle" dominant-baseline="middle">IMG ${String(i + 1).padStart(2, '0')}</text></svg>`;
const buf = await sharp(Buffer.from(svg)).jpeg({ quality: 88 }).toBuffer();
const up = await api('/v1/uploads', {
method: 'POST',
headers: auth,
body: JSON.stringify({
albumId: album.id,
categoryId: i < 6 ? cat.id : undefined,
clientTaskId: `demo-${stamp}-${i}`,
filename: `demo-${i + 1}.jpg`,
bytes: buf.length,
contentType: 'image/jpeg',
sourceDeviceId: 'demo-seed-000000000000001',
}),
});
const put = await fetch(up.credential.url, {
method: 'PUT',
headers: up.credential.headers,
body: buf,
});
if (!put.ok) throw new Error(`PUT ${i}: ${put.status}`);
await api(`/v1/uploads/${up.uploadId}/complete`, { method: 'POST', headers: auth });
ids.push(up.photoId);
}
console.log(`✓ ${ids.length} 张照片上传完成,等待 worker 发布...`);
// 4. 等 worker 发布(poll 公开列表)
let seen = 0;
for (let t = 0; t < 40 && seen < ids.length; t++) {
await new Promise((r) => setTimeout(r, 1000));
const list = await fetch(
`${CN}/v1/public/albums/${album.publicToken}/photos?limit=60`,
).then(j);
seen = list.items.length;
}
console.log(`✓ 公开列表可见 ${seen}/${ids.length}(worker+sync 已处理)`);
// 5. embed slot
const slot = await api('/v1/admin/embed-slots', {
method: 'POST',
headers: auth,
body: JSON.stringify({ name: `官网演示位 ${stamp}` }),
});
await api(`/v1/admin/embed-slots/${slot.id}`, {
method: 'PATCH',
headers: auth,
body: JSON.stringify({ albumId: album.id }),
});
console.log(`✓ 展示位 ${slot.publicToken} 已绑定相册`);
// 6. 点几个赞(让热门排序/气泡有内容)
const ov = 'http://127.0.0.1:8081';
for (const [idx, photoId] of ids.slice(0, 4).entries()) {
for (let k = 0; k < (5 - idx); k++) {
await fetch(`${ov}/v1/public/albums/${album.publicToken}/photos/${photoId}/likes`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ sessionId: `demo-user-${idx}`, batchId: `b${idx}-${k}`, delta: 1 }),
});
}
}
// 浏览上报
for (let v = 0; v < 5; v++) {
await fetch(`${ov}/v1/public/albums/${album.publicToken}/visits`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ visitorId: `demo-visitor-${v}${v}${v}${v}`, requestId: `demo-req-${v}-${stamp}` }),
});
}
console.log('✓ 点赞 + 浏览上报完成');
console.log('\n════════ 演示入口 ════════');
console.log(`观众页(中文): http://localhost:5175/a/${album.publicToken}`);
console.log(`观众页(英文): http://localhost:5175/a/${album.publicToken}?lang=en`);
console.log(`嵌入组件宿主页: http://localhost:5175/__test__/embed-host.html?slot=${slot.publicToken}`);
console.log(`管理后台: http://localhost:5176 (admin / ${process.env.ADMIN_PASSWORD || 'admin-secret-change-me'},相册列表第一个即本相册)`);