Files
photo-live/docs/plans/2026-09-21-UI01-实施计划.md
T
2026-09-21 20:15:36 +08:00

17 KiB
Raw Blame History

UI-01 App 与悬浮组件体验优化 实施计划

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 按 UI01 方案完成 App 与 JS 悬浮组件的产品化体验升级:统一主题、图片主导布局、上传状态清晰化、组件完整图片模式与可访问性——同时补齐封面摘要、受权限管理照片预览等必要契约。

Architecture: 纯 UI/UX 层改造 + 少量兼容契约扩展。不重写上传引擎/原生桥/同步架构;不改数据库 schema(仅 openapi.yaml 可选字段 + 服务端组装逻辑);Flutter 复用 Material + Riverpod;embed loader 保持零依赖手写。

Tech Stack: 既有全栈(Flutter 3.44 / NestJS / Vue3 / 手写 loader),零新框架依赖。

对应验收: UI-A01 至 UI-A16(方案 §9.1)。

依赖基线: 当前 HEAD(含 P5 准备件 + G1 + 测试服务器部署中产生的修复)。


方案分析与关键决策

分析结论

维度 评估
完整性 方案文档极完整:设计值精确到 px、状态机表、ASCII 布局图、验收矩阵——不需要再问用户常规样式问题
复杂度 中高:Flutter 端 ~10 个页面重构 + 主题系统 + 契约扩展;embed 端 loader+iframe+admin 预览全链路
风险点 ① 相册卡片封面→需要契约扩展 cover 字段 ② 协作者 own 照片列表→需要 scope=own 查询语义 ③ embed 新配置字段→需要 cn→ov 投影验证 ④ 跨源 Playwright 宿主测试需要双端口环境
与既有代码的关系 零破坏性:新字段全部可选/nullable;旧客户端不 break;组件 sm/md 保留兼容

实施切分策略

方案 §8 定义了 U00-U09 十个任务,依赖链 U00→U01→U02→U03/U04/U05→U06→U07→U08→U09。为子代理执行效率优化为:

  • M0 基线:U00 截图 + 契约预研
  • M1 契约与数据:U01 全量(cover + preview + scope=own + embed config)
  • M2 App 主题与导航:U02(设计系统 + 底部导航 + 相册卡片样板)
  • M3 App 相册与照片:U03(封面卡片 + 详情 + 照片网格 + 大图 + 权限操作)
  • M4 App 上传与拍摄:U04 + U05(上传中心 + 拍摄 + 表单/分享/设置统一)
  • M5 嵌入组件:U06(loader + iframe 轮播 + 状态 + 可访问性)
  • M6 后台预览:U07(展示位配置 + 本地预览)
  • M7 回归与验收:U08 + U09(全量矩阵 + 截图 + 记录 + 交接)

Task 1: U00 基线与现状截图

Files:

  • Create: docs/plans/progress/UI01-验收记录.md(骨架)

  • 采集截图目录:docs/plans/progress/ui01-screenshots/baseline/

  • Git 状态核对(未提交改动记录,不清理不覆盖);记录当前 HEAD

  • 启动本地全栈环境(compose + dev services);不可用的页面标注原因并跳过截图

  • Flutter App 截图:相册首页、详情、照片管理、上传队列、拍摄、创建/编辑、分享、设置(8 张,MuMu 模拟器)

  • Embed 组件截图:展开态、收起态、空态、宿主页(4 张,viewer e2e 宿主页面)

  • Admin 后台截图:展示位配置当前表单(1 张)

  • 初始化验收记录骨架(基线 commit、环境信息、截图索引)

  • Commit docs(ui01): baseline screenshots and acceptance record skeleton


Task 2: U01 契约与数据配合

Files:

  • Modify: packages/contracts/openapi.yaml(+gen)

  • Modify: services/api/src/albums/(cover 摘要批量组装)

  • Modify: services/api/src/photos/(preview + scope=own)

  • Modify: services/api/src/embed/(新配置字段校验)

  • Modify: ov 侧展示位公开响应投影

  • Modify: apps/mobile/lib/features/(Flutter 模型新字段)

  • Test: e2e 权限矩阵

  • 契约扩展(全部可选/nullable,旧客户端零影响):

    • AlbumSummary += cover: {url, expiresAt, width, height} | null(批量查 album_assets kind=cover,无封面返回 null)
    • AdminPhoto += preview: {thumb: SignedUrl, browse: SignedUrl} | null(有变体才签发;已删除返回 null;隐藏给有权限管理者)
    • listAlbumPhotos query += scope: 'all' | 'own'(默认 all=现有行为不变;own=协作者仅自己上传,服务端过滤)
    • EmbedSlotSettings += imageFit: 'contain'|'cover'、offsetX: 0-120、offsetBottom: 0-240、zIndex: 1-2147483000(全部带默认值,旧 JSON 兼容)
  • gen + lint + 全 web 回归绿(旧字段缺省兼容确认)

  • 服务端实现:

    • cover:albums 列表按页批量查 assets(不 N+1),签发短期 URL
    • preview:photos 管理列表按权限逐张判断(admin 全部/owner 本相册/collab 仅自己),scope=own 服务端过滤+成员资格逐次检查
    • embed config:新字段在 cn 管理 PATCH 校验 + slot-state payload 携带 + ov 公开响应返回
  • e2e 权限矩阵(~12 用例):admin/owner/collab-own/collab-other/cross-album × cover/preview/scope;隐藏照片有权限可见无权限 404;删除照片 preview=null;旧请求无新字段正常

  • 同步验证:新配置字段经 slot-state 到 ov 投影完整传递

  • Flutter 模型:Dart 模型增加 cover/preview/scope 字段(nullable,旧响应不 break)

  • Commit feat(contracts,api): UI-01 cover, preview, scope=own and embed config extensions


Task 3: U02 设计基础与导航

Files:

  • Create: apps/mobile/lib/ui/tokens.dart(色彩/间距/圆角/字号常量)

  • Create: apps/mobile/lib/ui/components/(StateBadge/EmptyState/ErrorState/PhotoThumbnail/SectionHeader)

  • Modify: apps/mobile/lib/app/theme.dart(完整 ColorScheme+TextTheme+组件主题)

  • Modify: apps/mobile/lib/app/app.dart(导航标签改名)

  • Modify: apps/mobile/lib/pages/home_page.dart(底部导航三 tab + 角标)

  • Modify: apps/mobile/lib/l10n/app_zh.arc / app_en.arb

  • 主题系统:

    • 浅色 ColorScheme(方案 §3.1 色值精确落地:primary=#1A73E8, surface=#FFFFFF, background=#F6F8FB 等)
    • 深色 ColorScheme(对应语义色,不反转图片)
    • TextTheme(22-24 标题 / 17-18 分组 / 14-16 正文 / 12-13 辅助)
    • 按钮(主操作高 48-52)、输入框(圆角 12)、AppBar、NavigationBar、Dialog、BottomSheet 主题
    • 动效时长常量:轻量 150-220ms / 轮播 250-350ms / reducedMotion 时禁用非必要动效
  • 公共组件(仅跨页面复用的,不为每个 SizedBox 封装):

    • StateBadge:成功/等待/错误/中性 + 图标 + 文案(不只靠颜色)
    • EmptyState:图标+标题+说明+可选操作按钮
    • ErrorState:错误说明+重试按钮
    • PhotoThumbnail:网络图片+占位+失败替代图+宽高比预留
    • SectionHeader:分组标题
  • 底部导航:三 tab 改名"相册/上传/设置"(Albums/Uploads/Settings);上传 tab 角标=当前账号未完成任务数(drift countsByState 聚合,不跨账号);IndexedStack 保持页面状态

  • 相册卡片样板页:完成一张可运行的封面卡片(封面图+标题+时间地点+公开/上传状态),同时验证浅色/深色/中英文

  • flutter analyze 0 + 新增 widget 测试(~10:主题一致性、StateBadge 各状态、PhotoThumbnail 加载/失败/占位、EmptyState、角标绑定)

  • Commit feat(mobile): UI-01 theme system, shared components and navigation


Task 4: U03 相册与照片工作流

Files:

  • Modify: apps/mobile/lib/pages/albums_tab.dart(封面卡片列表)

  • Modify: apps/mobile/lib/pages/album_detail_page.dart(紧凑信息+底部上传)

  • Rewrite: apps/mobile/lib/pages/album_photos_page.dart(网格+大图)

  • Modify: apps/mobile/lib/core/api_client.dart(scope=own 参数 + preview 刷新调用)

  • Modify: apps/mobile/lib/features/photos/models.dart

  • 相册列表:

    • 卡片顺序:封面 → 标题(≤2 行)→ 时间/地点 → 公开/上传状态 badge
    • 移除 publicToken 和四开关堆叠(完整配置在详情设置)
    • 手机单列 / 平板两列(按可用宽度,不挤压英文标题)
    • 无封面固定比例中性占位;网络失败替代图+重试
    • 标题回退:界面语言 → 相册默认语言 → 首个有效翻译
    • 时间用相册时区(不用设备时区)
  • 相册详情:

    • 布局:紧凑封面+信息 → 开关状态 → 照片入口/预览 → 次级管理入口(编辑/成员/分类/分享进工具菜单)
    • 底部固定"拍摄上传"(主按钮)+ "从相册选择"(次按钮),避让安全区
    • 公开/上传分开显示;上传关闭时说明原因+禁用
    • 无照片→"拍摄第一张照片"引导入口
  • 照片管理网格 + 大图:

    • 三列网格(极窄两列,平板按最小格宽扩列);缩略图 cover 裁切
    • 角标:处理中/失败/隐藏/删除
    • 点开大图:contain 展示、左右切换、按需加载更多、关闭回原位置
    • 大图详情:上传者/时区时间/尺寸大小(ID 放诊断区域)
    • 隐藏/恢复/删除单张操作(权限校验+确认弹窗)
    • 签名 URL 过期→一次受控刷新
    • 协作者 own 列表(scope=own,服务端过滤)
  • widget 测试(~15:封面有无/标题回退/网格列数/大图切换返回/角标状态/权限操作/URL 刷新)

  • Commit feat(mobile): UI-01 album cards, photo grid and full viewer


Task 5: U04+U05 上传中心 + 辅助页面统一

Files:

  • Modify: apps/mobile/lib/pages/queue_tab.dart(摘要+状态分组)

  • Modify: apps/mobile/lib/pages/capture_page.dart(深色取景+反馈)

  • Modify: apps/mobile/lib/pages/album_form_page.dart(分组表单)

  • Modify: apps/mobile/lib/pages/share_qr_dialog.dart(分层布局)

  • Modify: apps/mobile/lib/pages/login_page.dart / password_change_page.dart / settings section

  • Modify: apps/mobile/lib/pages/server_history_page.dart

  • 上传中心:

    • 顶部摘要条(筛选范围标注:"今天·本设备"):82 完成 3 上传中 13 等待 2 需关注
    • 状态 chips:全部/进行中/需关注/已完成
    • 状态行映射(方案 §4.4 表格完整落地,每种状态独立文案+颜色+图标+操作)
    • "一键重试"仅作用于可重试任务,文案说明范围
    • "移除记录"替代"清理"(明确不删 OSS 照片)
    • 进度行级订阅(UploadProgressHub 已有,避免整列表重建)
    • 本机队列与服务端历史分开标题+说明
  • 拍摄页:

    • 深色取景器;顶部相册/分类常驻;底部快门+入队计数
    • 入队成功短暂反馈(不逐张弹窗);连拍模式切换只影响新照片提示
  • 创建/编辑表单:

    • 分组:活动信息 / 多语言内容 / 访问设置
    • 默认语言必填;另一语言按需填写(tab 切换)
    • 保存按钮键盘打开时可达(Scrollable + bottomNavigationBar)
  • 分享:名称/二维码/链接清晰分层;复制链接必做

  • 设置:账号信息/语言/密码/上传帮助/版本分组;退出不承诺后台继续上传

  • 登录/改密:样式统一(主题组件应用)

  • widget 测试(~12:摘要计算/状态映射/分组表单校验/分享布局/设置分组)

  • Commit feat(mobile): UI-01 upload center, capture and auxiliary pages


Task 6: U06 嵌入轮播与 loader

Files:

  • Modify: packages/embed-loader/src/loader.js / params.js

  • Modify: packages/embed-loader/src/loader.css(新增:舞台样式/收起入口/响应式)

  • Modify: apps/viewer/src/pages/EmbedPage.vue

  • Modify: apps/viewer/src/embed/useEmbedFeed.ts / feed.ts / postMessage.ts

  • Create: apps/viewer/src/pages/embed/ 组件拆分(Carousel/Stage/Footer/CollapsedHandle)

  • Modify: packages/embed-loader/README-embed.md

  • loader 升级:

    • 新配置字段支持:imageFit/offsetX/offsetBottom/zIndex(校验+合并,优先级 data-* > config消息 > 后台设置 > 默认)
    • 收起入口重新设计:图片图标+Photos/照片,高≥44px,把手有效点击区≥44×44
    • 修复内联 SVG fill="%23ffffff" 为有效颜色
    • 多 slot 同侧纵向排列+间距;高度不足时后续收起
    • 窄屏(<480px)默认收起+sm 尺寸
    • destroy 消息接收(白名单 origin+source+type 严格校验)→ 移除卡片+把手+监听器
    • config 动态消息接收(configVersion 校验+旧请求丢弃+loader 二次验证)
    • 窗口 resize/横竖屏切换重新布局
  • iframe 轮播页重构:

    • 组件拆分:Carousel(切换逻辑)/ Stage(4:3 图片舞台)/ Footer(名称+页码+入口)/ CollapsedHandle
    • object-fit: contain 默认(中性深色舞台);cover 可配置
    • 页码"当前序号/队列数量"替代圆点
    • 预加载:仅当前+下一张;decode 成功再切换;失败保留上一张+尝试后续
    • 暂停/恢复:用户暂停/指针悬停/键盘焦点/页面隐藏/组件收起 全部停止;恢复后校准一次
    • reducedMotion 时取消过渡+默认暂停+手动上/下一张按钮
    • 轮播与取数彻底解耦:轮播 intervalMs 不影响取数轮询周期(30000ms 基准)
    • ETag 304 过期 URL 处理:304 后签名仍可能过期→强制刷新路径(有限重试)
    • 停用/解绑/404/410 → postMessage destroy(loader 移除自身)
  • 可访问性:

    • 隐藏轮播帧不可 Tab/读屏(aria-hidden + 仅渲染当前可交互帧)
    • 所有操作 button/a 语义+中英文标签+可见焦点
    • 图片 aria-label="活动照片,第 N 张"(不编造描述)
  • 宿主测试环境:

    • 新增跨源 Playwright 宿主:两个端口(如 5175+5176 模拟不同 origin),测试白名单拒绝非法 origin/source/config
    • 模拟宿主全局 button CSS 干扰、多 slot 同侧
  • 单测(loader params ~15:缺省/显式/config 消息合并+校验+边界) + vitest(viewer ~12:暂停恢复/计时器独立/失败图片/签名刷新/非当前帧不可聚焦)

  • Playwright(~8:跨源接入/收起展开暂停/键盘焦点/多 slot/全局 CSS 干扰/destroy/304+URL 过期/窄屏)

  • build + sync loader(带内容哈希产物 + 旧 v1 兼容入口保留)

  • Commit feat(embed,viewer): UI-01 complete carousel, loader config and accessibility


Task 7: U07 后台配置与预览

Files:

  • Modify: apps/admin/src/views/system/embed-slots/(表单+预览)

  • Modify: apps/admin/src/api/embedSlots.ts / types/api/embedSlot.d.ts

  • 配置表单增强:

    • 新增:左右位置选择、尺寸(sm=280/md=320)、轮播间隔滑块、语言选择、收起默认开关、图片适配(contain/cover)、水平偏移(0-120px)、底部偏移(0-240px)、层级(zIndex)
    • 保存才更新线上(表单状态 vs 已保存状态视觉区分)
  • 本地预览:

    • 表单旁/下方:桌面预览(320px 框)+ 手机预览(375px 框)
    • 未保存修改即时反映到预览(纯本地,不写展示位不上报浏览)
    • 预览使用本地示例数据(渐变色块+文字标注"预览")
    • 预览与生产组件共享展示结构(CSS 变量/组件引用,非复制两套)
  • 嵌入代码卡:只包含用户显式选择的 data-* 覆盖,默认不填(让后台配置生效)

  • vitest(~6:表单→payload 新字段、预览状态绑定、代码卡只含显式覆盖)

  • Playwright(~4:表单配置→预览更新→保存→ov 响应新字段、代码卡内容验证)

  • Commit feat(admin): UI-01 embed slot config form and live preview


Task 8: U08+U09 回归验收 + 文档交接

  • 全量回归:pnpm lint && typecheck && test && build && e2e + flutter analyze && flutter test + loader build+sync
  • UI 验收矩阵(方案 §9.1 UI-A01 至 UI-A16 逐项):
    • 截图采集:优化后全部页面+组件(对照 Task 1 基线)
    • 320/390/430/768/1440 逻辑宽度覆盖
    • 中英文×浅色深色
    • 记录设备/视口/语言/主题
  • 性能记录(改造前后对比):首屏请求数、组件预加载数、loader gzip 大小(不编百分比)
  • 验收记录:完成 UI01-验收记录.md 全部章节(方案 §10 模板)
  • 总纲更新:UI-01 状态注记(不标记 P5 完成)
  • Commit + tag ui01

执行注意

  • 保护规则(方案 §2 全文有效):不覆盖已有未提交改动、不重写上传引擎/原生桥/同步架构、不通过观众 API 获取管理照片、不新增重型 UI 框架、所有文案进语言资源
  • 契约兼容红线:新字段全部 nullable/optional;旧客户端零 break;不做破坏性 schema 变更
  • 性能红线:相册列表封面批量查询(不 N+1);上传进度行级订阅(不整列表重建);组件仅预加载当前+下一张
  • 组件安全红线:postMessage 双向白名单(origin+source+type);loader 不向宿主注入全局 CSS;外链 noopener/noreferrer
  • Flutter 命令前置 source scripts/flutter-env.sh
  • 每任务 TDD + 提交带 Co-Authored-By trailer