# 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 方案](2026-09-21-UI01-App与悬浮组件体验优化实施方案.md)完成 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