| null = null;
+
+ function stopSweep(): void {
+ if (sweepTimer !== null) {
+ clearInterval(sweepTimer);
+ sweepTimer = null;
+ }
+ }
+
+ function startSweepIfPending(): void {
+ if (sweepTimer === null) {
+ sweepTimer = setInterval(() => sweepPending(), PENDING_SWEEP_MS);
+ }
+ }
+
+ /** 巡检:缓存中的照片转为可见 → 立即出泡;顺带清 TTL。 */
+ function sweepPending(): void {
+ const visible = isPhotoVisible;
+ let any = false;
+ // take 内部清理过期项;可见者并入节流队列。
+ for (const photoId of [...cachedIds()]) {
+ const hearts = pendingTake(photoId);
+ if (hearts > 0 && visible(photoId)) {
+ const batch = throttle.offer(photoId, hearts, now());
+ if (batch !== null) onEmit(batch);
+ } else if (hearts > 0) {
+ // 仍不可见但未超时:放回(take 已删——重新入表沿用原时刻语义可接受,
+ // 概率极低且粒度无感知;此处直接丢弃亦可,选择保守放回)。
+ pending.add(photoId, hearts, now());
+ any = true;
+ }
+ }
+ if (!any) stopSweep();
+ }
+
+ // pending 缓存的只读 id 快照与 take 代理(隔离内部类型)。
+ const cachedIds = (): string[] => pending.snapshotIds();
+ const pendingTake = (photoId: string): number => pending.take(photoId, now());
+
+ function handleBurst(message: ServerMessage): void {
+ if (message.type !== 'likes.burst') return;
+ if (!Number.isFinite(message.delta) || message.delta < 1) return;
+ if (!deduper.accept(message.burstId, now())) return;
+ const hearts = heartsForDelta(message.delta);
+ if (hearts <= 0) return;
+ if (isPhotoVisible(message.photoId)) {
+ const batch = throttle.offer(message.photoId, hearts, now());
+ if (batch !== null) onEmit(batch);
+ } else {
+ pending.add(message.photoId, hearts, now());
+ startSweepIfPending();
+ }
+ }
+
+ function stopClient(): void {
+ client?.stop();
+ client = null;
+ subscribedToken = null;
+ }
+
+ function clearAll(): void {
+ deduper.clear();
+ pending.clear();
+ throttle.clear();
+ stopSweep();
+ }
+
+ function teardown(): void {
+ stopClient();
+ clearAll();
+ if (disconnectTimer !== null) {
+ clearTimeout(disconnectTimer);
+ disconnectTimer = null;
+ }
+ }
+
+ /** 活跃度变化:恢复 → 立即校准重连;失活 → 宽限期后断开(§5.3)。 */
+ function onActiveChange(value: boolean): void {
+ if (disconnectTimer !== null) {
+ clearTimeout(disconnectTimer);
+ disconnectTimer = null;
+ }
+ if (value) {
+ if (client === null && subscribedToken !== albumToken.value) startClient();
+ } else if (client !== null) {
+ disconnectTimer = setTimeout(() => {
+ disconnectTimer = null;
+ stopClient();
+ }, DISCONNECT_GRACE_MS);
+ }
+ }
+
+ function startClient(): void {
+ const token = albumToken.value;
+ if (token === null || !enabled.value || !active.value) return;
+ stopClient();
+ clearAll(); // 换绑:清旧内容与去重(§5.3)
+ client = new RealtimeClient({
+ url: () => wsUrlFromApiBase(apiBase()),
+ albumToken: () => albumToken.value ?? '',
+ onMessage: (message) => handleBurst(message),
+ onStateChange: () => undefined,
+ onResync: () => undefined, // 只读计数流无需补播(§4.6 观众页语义不适用)
+ });
+ client.start();
+ subscribedToken = token;
+ }
+
+ // 换绑/开关/活跃度联动。
+ watch(
+ [albumToken, enabled],
+ ([token, on]) => {
+ if (token !== null && on) startClient();
+ else teardown();
+ },
+ { immediate: true },
+ );
+ watch(active, onActiveChange);
+
+ onBeforeUnmount(teardown);
+
+ return {
+ /** 测试接口:直接注入一帧(绕过 WS;模拟管理预览/单测)。 */
+ ingestForTest(message: ServerMessage): void {
+ handleBurst(message);
+ },
+ };
+}
diff --git a/apps/viewer/src/embed/useVerticalPhotoStream.ts b/apps/viewer/src/embed/useVerticalPhotoStream.ts
index c4d7555..5a069be 100644
--- a/apps/viewer/src/embed/useVerticalPhotoStream.ts
+++ b/apps/viewer/src/embed/useVerticalPhotoStream.ts
@@ -216,6 +216,27 @@ export function useVerticalPhotoStream(options: UseVerticalPhotoStreamOptions) {
return result;
}
+ /**
+ * 照片在视口内的交集矩形(E05 气泡起点):top/height 为视口坐标;不可见
+ * (无交集)返回 null。同一照片多副本时取首个有交集实例。
+ */
+ function visibleRectOf(photoId: string): { top: number; height: number } | null {
+ const s = state.value;
+ if (s === null) return null;
+ let top = -s.offset;
+ for (const item of s.items) {
+ const h = photoDisplayHeight(s.sizes[item.queueIdx], innerWidth);
+ const visibleTop = Math.max(0, top);
+ const visibleBottom = Math.min(viewportHeight, top + h);
+ if (item.photoId === photoId && visibleBottom > visibleTop) {
+ return { top: visibleTop, height: visibleBottom - visibleTop };
+ }
+ top += h + PHOTO_GAP_PX;
+ if (top >= viewportHeight) break;
+ }
+ return null;
+ }
+
watch(photos, (next) => reanchor(next), { immediate: false });
watch(running, (value) => {
if (value) startLoop();
@@ -250,6 +271,7 @@ export function useVerticalPhotoStream(options: UseVerticalPhotoStreamOptions) {
dropPhoto,
nudgeOffset,
visiblePhotoIds,
+ visibleRectOf,
/** 测试接口(§11.4 受控时间):绕过 rAF 按给定 dt 推进一帧并同步渲染。
* 仅单测使用;生产路径不经过本入口。单张/零张静态(不推进,§3.2.8/9)。 */
stepForTest(dtMs: number): void {
diff --git a/docs/designs/app-preview-2026-09-22/01-login.html b/docs/designs/app-preview-2026-09-22/01-login.html
new file mode 100644
index 0000000..9bda1bb
--- /dev/null
+++ b/docs/designs/app-preview-2026-09-22/01-login.html
@@ -0,0 +1 @@
+PhotoLive · Login
\ No newline at end of file
diff --git a/docs/designs/app-preview-2026-09-22/02-settings.html b/docs/designs/app-preview-2026-09-22/02-settings.html
new file mode 100644
index 0000000..b539721
--- /dev/null
+++ b/docs/designs/app-preview-2026-09-22/02-settings.html
@@ -0,0 +1 @@
+PhotoLive · Settings
\ No newline at end of file
diff --git a/docs/designs/app-preview-2026-09-22/03-password.html b/docs/designs/app-preview-2026-09-22/03-password.html
new file mode 100644
index 0000000..ba48936
--- /dev/null
+++ b/docs/designs/app-preview-2026-09-22/03-password.html
@@ -0,0 +1 @@
+PhotoLive · Change password
\ No newline at end of file
diff --git a/docs/designs/app-preview-2026-09-22/04-language.html b/docs/designs/app-preview-2026-09-22/04-language.html
new file mode 100644
index 0000000..aa743d0
--- /dev/null
+++ b/docs/designs/app-preview-2026-09-22/04-language.html
@@ -0,0 +1 @@
+PhotoLive · App language
\ No newline at end of file
diff --git a/docs/designs/app-preview-2026-09-22/05-albums.html b/docs/designs/app-preview-2026-09-22/05-albums.html
new file mode 100644
index 0000000..f122fb9
--- /dev/null
+++ b/docs/designs/app-preview-2026-09-22/05-albums.html
@@ -0,0 +1 @@
+PhotoLive · Albums
\ No newline at end of file
diff --git a/docs/designs/app-preview-2026-09-22/06-album-detail.html b/docs/designs/app-preview-2026-09-22/06-album-detail.html
new file mode 100644
index 0000000..f6c0104
--- /dev/null
+++ b/docs/designs/app-preview-2026-09-22/06-album-detail.html
@@ -0,0 +1 @@
+PhotoLive · Album overview
\ No newline at end of file
diff --git a/docs/designs/app-preview-2026-09-22/07-album-actions.html b/docs/designs/app-preview-2026-09-22/07-album-actions.html
new file mode 100644
index 0000000..2f617eb
--- /dev/null
+++ b/docs/designs/app-preview-2026-09-22/07-album-actions.html
@@ -0,0 +1 @@
+PhotoLive · Album actions
\ No newline at end of file
diff --git a/docs/designs/app-preview-2026-09-22/08-empty-album.html b/docs/designs/app-preview-2026-09-22/08-empty-album.html
new file mode 100644
index 0000000..de26e01
--- /dev/null
+++ b/docs/designs/app-preview-2026-09-22/08-empty-album.html
@@ -0,0 +1 @@
+PhotoLive · Empty album
\ No newline at end of file
diff --git a/docs/designs/app-preview-2026-09-22/09-create-album.html b/docs/designs/app-preview-2026-09-22/09-create-album.html
new file mode 100644
index 0000000..02faf33
--- /dev/null
+++ b/docs/designs/app-preview-2026-09-22/09-create-album.html
@@ -0,0 +1 @@
+PhotoLive · New album
\ No newline at end of file
diff --git a/docs/designs/app-preview-2026-09-22/10-edit-album.html b/docs/designs/app-preview-2026-09-22/10-edit-album.html
new file mode 100644
index 0000000..38c8774
--- /dev/null
+++ b/docs/designs/app-preview-2026-09-22/10-edit-album.html
@@ -0,0 +1 @@
+PhotoLive · Edit album
\ No newline at end of file
diff --git a/docs/designs/app-preview-2026-09-22/11-members.html b/docs/designs/app-preview-2026-09-22/11-members.html
new file mode 100644
index 0000000..10cf3fc
--- /dev/null
+++ b/docs/designs/app-preview-2026-09-22/11-members.html
@@ -0,0 +1 @@
+PhotoLive · Members
\ No newline at end of file
diff --git a/docs/designs/app-preview-2026-09-22/12-categories.html b/docs/designs/app-preview-2026-09-22/12-categories.html
new file mode 100644
index 0000000..a601db7
--- /dev/null
+++ b/docs/designs/app-preview-2026-09-22/12-categories.html
@@ -0,0 +1 @@
+PhotoLive · Categories
\ No newline at end of file
diff --git a/docs/designs/app-preview-2026-09-22/13-camera.html b/docs/designs/app-preview-2026-09-22/13-camera.html
new file mode 100644
index 0000000..884dbf0
--- /dev/null
+++ b/docs/designs/app-preview-2026-09-22/13-camera.html
@@ -0,0 +1 @@
+PhotoLive · Camera
\ No newline at end of file
diff --git a/docs/designs/app-preview-2026-09-22/14-uploads.html b/docs/designs/app-preview-2026-09-22/14-uploads.html
new file mode 100644
index 0000000..3d4537f
--- /dev/null
+++ b/docs/designs/app-preview-2026-09-22/14-uploads.html
@@ -0,0 +1 @@
+PhotoLive · Uploads
\ No newline at end of file
diff --git a/docs/designs/app-preview-2026-09-22/15-upload-errors.html b/docs/designs/app-preview-2026-09-22/15-upload-errors.html
new file mode 100644
index 0000000..1756a19
--- /dev/null
+++ b/docs/designs/app-preview-2026-09-22/15-upload-errors.html
@@ -0,0 +1 @@
+PhotoLive · Needs attention
\ No newline at end of file
diff --git a/docs/designs/app-preview-2026-09-22/16-upload-history.html b/docs/designs/app-preview-2026-09-22/16-upload-history.html
new file mode 100644
index 0000000..0f65ef1
--- /dev/null
+++ b/docs/designs/app-preview-2026-09-22/16-upload-history.html
@@ -0,0 +1 @@
+PhotoLive · Upload history
\ No newline at end of file
diff --git a/docs/designs/app-preview-2026-09-22/17-photos.html b/docs/designs/app-preview-2026-09-22/17-photos.html
new file mode 100644
index 0000000..c7a64a5
--- /dev/null
+++ b/docs/designs/app-preview-2026-09-22/17-photos.html
@@ -0,0 +1 @@
+PhotoLive · Manage photos
\ No newline at end of file
diff --git a/docs/designs/app-preview-2026-09-22/18-photo-detail.html b/docs/designs/app-preview-2026-09-22/18-photo-detail.html
new file mode 100644
index 0000000..0f8c13d
--- /dev/null
+++ b/docs/designs/app-preview-2026-09-22/18-photo-detail.html
@@ -0,0 +1 @@
+PhotoLive · Photo details
\ No newline at end of file
diff --git a/docs/designs/app-preview-2026-09-22/19-share.html b/docs/designs/app-preview-2026-09-22/19-share.html
new file mode 100644
index 0000000..20c9658
--- /dev/null
+++ b/docs/designs/app-preview-2026-09-22/19-share.html
@@ -0,0 +1 @@
+PhotoLive · Share album
\ No newline at end of file
diff --git a/docs/designs/app-preview-2026-09-22/20-hide-photo.html b/docs/designs/app-preview-2026-09-22/20-hide-photo.html
new file mode 100644
index 0000000..d3d71ee
--- /dev/null
+++ b/docs/designs/app-preview-2026-09-22/20-hide-photo.html
@@ -0,0 +1 @@
+PhotoLive · Hide photo
\ No newline at end of file
diff --git a/docs/designs/app-preview-2026-09-22/README.md b/docs/designs/app-preview-2026-09-22/README.md
new file mode 100644
index 0000000..29a44ab
--- /dev/null
+++ b/docs/designs/app-preview-2026-09-22/README.md
@@ -0,0 +1,72 @@
+# PhotoLive App 全页面 HTML 交互原型
+
+日期:2026-09-22。打开 **index.html** 查看页面目录和交互预览,点击“全部页面总览”查看全部20页的实际渲染截图。也可以直接打开 `01-login.html` 等独立入口。无须启动服务、安装依赖或访问网络。
+
+## 设计依据
+
+- `../../plans/2026-09-21-UI03-App全页面新版效果图落地实施方案.md`
+- `../../plans/2026-09-22-UI03R-App高保真还原完整实施方案.md`
+- `../app-redesign-2026-09-21/` 下五张设计板。
+
+本目录为独立 HTML 设计原型,未修改 Flutter、API、观众页或嵌入组件业务代码。各页面是 HTML/CSS 控件,不是把整张设计板作为界面。摄影素材通过 CSS 显示已有生成设计图中的摄影区域,来源为本项目 `viewer-mobile-concept-2026-09-21.png`;`assets/reference.png` 为离线副本。摄影主题接近参考图,但并非五张板中的每张原始照片;低分辨率局部素材仅用于预览,不作为生产图片素材。
+
+## 页面清单
+
+| 编号 | 独立文件 | 内容 |
+|---|---|---|
+| 01 | 01-login.html | 摄影拼贴、示例账号登录 |
+| 02 | 02-settings.html | 账号摘要、设置、退出 |
+| 03 | 03-password.html | 密码校验演示 |
+| 04 | 04-language.html | 语言选择与 Done 提交 |
+| 05 | 05-albums.html | 三张摄影封面相册卡 |
+| 06 | 06-album-detail.html | 相册详情、九宫格、拍摄/选图 |
+| 07 | 07-album-actions.html | 相册操作面板 |
+| 08 | 08-empty-album.html | 空相册与上传入口 |
+| 09 | 09-create-album.html | 创建表单、日期、时区、地点、语言 |
+| 10 | 10-edit-album.html | 详情/权限页签,草稿统一保存 |
+| 11 | 11-members.html | 所有者、协作者、内部账号选择 |
+| 12 | 12-categories.html | 分类列表、新建与编辑面板 |
+| 13 | 13-camera.html | 深色相机取景演示、模拟入队 |
+| 14 | 14-uploads.html | 本机摘要、进度与状态 |
+| 15 | 15-upload-errors.html | 可重试与需重新选择的异常任务 |
+| 16 | 16-upload-history.html | 服务端跨设备历史示例 |
+| 17 | 17-photos.html | 三列照片网格与公开/隐藏筛选 |
+| 18 | 18-photo-detail.html | 管理大图、元信息、隐藏/删除 |
+| 19 | 19-share.html | 分享卡、演示二维码、预览链接 |
+| 20 | 20-hide-photo.html | 隐藏确认面板 |
+
+## 可操作的演示路径
+
+1. 登录(不用真实密码)→相册→详情→拍摄→快门模拟入队→上传中心。
+2. 相册详情→更多→编辑→切换 Details/Access→保存,预览会话中的标题/权限更新。
+3. 成员→邀请同事→搜索示例内部账号→添加;成员菜单可模拟移除。
+4. 分类→填写名称→保存;点击分类铅笔编辑。
+5. 照片网格→大图→隐藏→确认→Hidden 筛选;大图可重新公开及模拟删除。
+6. 设置→语言→选择 English/中文/跟随系统→Done。关闭面板不提交选择。
+7. 上传→Needs attention→重试;不可恢复的源文件项进入示例选图面板。
+8. 分享→打开此前的观众页原型,或复制本地文件预览地址。
+
+状态仅保存在当前文档内存,刷新/从目录重新打开某页会恢复示例。直接在预览内跳转保留当前会话变更。账号密码不发送、不持久化;请勿输入真实凭证。
+
+## 边界说明
+
+- 相机是示例图片,快门仅增加示例队列数;不请求设备摄像头权限。
+- 上传进度、服务端记录、账号、角色与时间为视觉样本,不接入 OSS/网络/原生后台上传。
+- 浏览器HTML不能验证 Flutter 的系统相机、后台/锁屏、原生手势、iOS/Android 安全区或性能。
+- 二维码是真实编码的演示 URL `https://example.com/photolive-preview`,不是公开相册,不能用于活动分发。正式 QR 必须由真实公开相册链接生成。
+- 分享中的“打开观众页预览”依赖本目录上一层 `viewer-preview-2026-09-22.html`;单独搬运本目录时,App 本身可离线打开,但此跨原型链接会失效。
+- 所有管理写操作仅在原型内模拟,不构成权限、接口或生产功能验收。
+- 参考板是英文浅色,本套以该基准制作;中文可切换,未提供全套系统深色主题、双指缩放和原生键盘还原。
+
+## 文件结构
+
+- `index.html`:目录、固定尺寸预览、截图总览。
+- `01-*.html` 至 `20-*.html`:20个独立页面入口。
+- `app.js` / `styles.css`:公共界面和交互实现。
+- `pages.js`:页面索引。
+- `assets/`:离线摄影素材与演示二维码。
+- `screenshots/`:实际 HTML 渲染截图。
+- `validation.json`:自动检查结果,不能替代原生 App 验收。
+- `build-preview.cjs`:本仓库环境中重新生成入口与二维码;浏览原型无需运行。
+
+在本目录使用 `node build-preview.cjs` 可以重新生成入口,所需 qrcode 使用仓库已有依赖。不要通过此脚本修改原生 App 或线上配置。
diff --git a/docs/designs/app-preview-2026-09-22/app.js b/docs/designs/app-preview-2026-09-22/app.js
new file mode 100644
index 0000000..b85ec66
--- /dev/null
+++ b/docs/designs/app-preview-2026-09-22/app.js
@@ -0,0 +1,44 @@
+/* Offline design prototype. No account, camera, upload, or production API calls. */
+const $=s=>document.querySelector(s), $$=s=>[...document.querySelectorAll(s)];
+const paths={back:'m15 18-6-6 6-6',next:'m9 18 6-6-6-6',close:'m6 6 12 12M6 18 18 6',plus:'M12 5v14M5 12h14',more:'M5 12h.01M12 12h.01M19 12h.01',album:'M3 7h7l2-3h9v16H3ZM3 9h18',photo:'M3 4h18v16H3ZM4 16l5-5 4 4 3-3 5 5M15 8h.01',upload:'M7 18H6a4 4 0 0 1-1-8 7 7 0 0 1 13-2 5 5 0 0 1 0 10h-1M12 19V9m-4 4 4-4 4 4',settings:'m9 3-.5 3-2 1-3-.8L2 10l2.5 2L4 15l2 3 3-.5 2 3 4-1 .5-3 3-1 2-4-2-2V6l-4-.5L13 3ZM9 12a3 3 0 1 0 6 0 3 3 0 1 0-6 0',user:'M8 7a4 4 0 1 0 8 0 4 4 0 1 0-8 0M4 21v-3a8 8 0 0 1 16 0v3',users:'M4 8a3 3 0 1 0 6 0 3 3 0 1 0-6 0M14 8a3 3 0 1 0 6 0 3 3 0 1 0-6 0M1 21v-3a6 6 0 0 1 12 0v3M14 14a6 6 0 0 1 9 4v3',lock:'M5 10h14v11H5ZM8 10V7a4 4 0 0 1 8 0v3M12 14v3',eye:'M2 12s3-7 10-7 10 7 10 7-3 7-10 7S2 12 2 12ZM9 12a3 3 0 1 0 6 0 3 3 0 1 0-6 0',globe:'M2 12h20M12 2a10 10 0 1 0 0 20 10 10 0 1 0 0-20ZM12 2c-6 6-6 14 0 20 6-6 6-14 0-20',help:'M9 9a3 3 0 1 1 5 2c-2 1-2 2-2 3M12 17h.01M12 2a10 10 0 1 0 0 20 10 10 0 1 0 0-20',logout:'M9 3H3v18h6M9 12h13m-5-5 5 5-5 5',calendar:'M4 5h16v16H4ZM4 10h16M8 3v4M16 3v4M8 14h2M14 14h2',pin:'M12 22S4 13 4 9a8 8 0 0 1 16 0c0 4-8 13-8 13ZM9 9a3 3 0 1 0 6 0 3 3 0 1 0-6 0',tag:'M3 3h8l10 10-8 8L3 11ZM7 7h.01',share:'M9 8l6-3M9 15l6 4M3 12a3 3 0 1 0 6 0 3 3 0 1 0-6 0M15 4a3 3 0 1 0 6 0 3 3 0 1 0-6 0M15 20a3 3 0 1 0 6 0 3 3 0 1 0-6 0',edit:'m4 16 12-12 4 4L8 20H4ZM13 7l4 4',camera:'M3 7h4l2-3h6l2 3h4v14H3ZM8 14a4 4 0 1 0 8 0 4 4 0 1 0-8 0',flip:'M3 11a9 9 0 0 1 16-6M19 1v5h-5M21 13a9 9 0 0 1-16 6M5 23v-5h5',clock:'M12 2a10 10 0 1 0 0 20 10 10 0 1 0 0-20ZM12 6v6l4 2',check:'m5 12 4 4L19 6',trash:'M3 6h18M9 6V3h6v3M5 6l1 15h12l1-15M10 10v7M14 10v7',link:'m9 15 6-6M8 16l-2 2a4 4 0 0 1-6-6l5-5a4 4 0 0 1 6 0M16 8l2-2a4 4 0 0 1 6 6l-5 5a4 4 0 0 1-6 0',logo:'M12 2 21 7v10l-9 5-9-5V7ZM12 2l4 7-4 7-8 1M21 7h-9l-4 7 4 8M21 17l-5-8H8L3 7'};
+function icon(n){return ``}
+let lang='en',page=1,returnPage=2,albumTitle='Canton Fair · Autumn',editTab='access',uploadFilter='all',photoFilter='all',currentPhoto=1,categoryNames=['Exhibition','People','Highlights'],members=['Emma Chen','Daniel Lee'],extraTasks=0,hiddenPhotos=new Set([6]),removedPhotos=new Set(),retried=false,publicAccess=true,uploadAllowed=true,downloads=true,original=false,draftLanguage='en',toastTimer;
+const T=(en,zh)=>lang==='zh'?zh:en, esc=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
+const pics=[[262,486,230,128],[845,248,358,435],[264,661,224,122],[506,656,185,142],[265,824,224,126],[504,823,181,129],[260,100,477,193],[848,300,345,378]];
+function photo(i,cls='',height=''){const r=pics[((i%pics.length)+pics.length)%pics.length],[x,y,w,h]=r;return ``}
+function button(label,action,cls='primary',ic=''){return ``}
+function row(ic,label,sub,action='',trailing=''){return `<${action?'button':'div'} class="row" ${action?`data-action="${action}"`:''}>${icon(ic)}${label}${sub?`${sub}
`:''}${trailing?`${trailing}`:''}${action?`${icon('next')}`:''}${action?'button':'div'}>`}
+function bar(title,back=5,right='',root=false){return `${root?`${title}
`:`${title}`}${right}`}
+function nav(selected){return ``}
+function field(label,id,type='text',value='',placeholder=''){const pw=type==='password';return ``}
+function seg(items,selected,action){return `${items.map(([id,label])=>``).join('')}
`}
+function status(){return `${icon('globe')}${publicAccess?T('Public','公开'):T('Private','未公开')}${icon('upload')}${uploadAllowed?T('Uploads on','可上传'):T('Uploads off','上传关闭')}
`}
+function grid(count=9,offset=0){return `${Array.from({length:count},(_,k)=>k+offset).filter(i=>!removedPhotos.has(i)&&(photoFilter!=='hidden'||hiddenPhotos.has(i))&&(photoFilter!=='published'||!hiddenPhotos.has(i))).map(i=>``).join('')}
`}
+function login(){return ``}
+function settings(){return bar(T('Settings','设置'),5,'',true)+``+nav(2)}
+function password(){return bar(T('Change password','修改密码'),2)+``}
+function albums(){return bar('PhotoLive',5,``,true)+``+nav(5)}
+function detail(empty=false){const title=empty?T('Team Stories','团队故事'):albumTitle;return bar('',5,``)+`${button(T('Take photos','拍摄照片'),uploadAllowed?'go:13':'uploadOff','primary','camera')}${button(T('Choose photos','选择照片'),uploadAllowed?'choose':'uploadOff','secondary','photo')}
`}
+function albumForm(edit=false){const details=`${field(T('Album title','相册名称'),'albumTitle','text',edit?albumTitle:T('Canton Fair · Autumn','广交会 · 秋季'))}
${field(T('Starts','开始日期'),'startDate','date','2026-10-15')}${field(T('Ends','结束日期'),'endDate','date','2026-10-19')}${field(T('Venue','地点'),'venue','text','Guangzhou')}${field(T('Address','详细地址'),'address','text',T('China Import and Export Fair Complex','中国进出口商品交易会展馆'))}
${T('Description & languages','说明与语言')}
`;const access=`${[['uploads',T('Allow uploads','允许上传'),T('Team members can upload photos to this album.','团队成员可以向本相册上传照片。'),uploadAllowed],['public',T('Public access','公开访问'),T('Anyone with the link can view this album.','获得链接的观众可以浏览相册。'),publicAccess],['downloads',T('Allow downloads','允许下载'),T('Viewers can download photos from this album.','观众可以下载相册照片。'),downloads],['original',T('Original downloads','原图下载'),T('Allow downloading full-resolution files.','允许观众下载完整分辨率的原图。'),original]].map(([id,title,desc,on])=>`
`).join('')}
${row('globe',T('Content language','内容语言'),T('Default language for album content','相册内容的默认语言'),'contentLanguage','English')}
`;return bar(edit?T('Edit album','编辑相册'):T('New album','新建相册'),6)+``}
+function memberPage(){return bar(T('Members','成员'),6,``)+`${button(T('Invite colleague','邀请同事'),'invite','secondary','users')}
`}
+function categories(){return bar(T('Categories','分类'),6,``)+``}
+function camera(){return bar(`${esc(albumTitle)}
`,6,``)+`${photo(1)}
${T('Camera preview · sample photo','相机效果演示 · 示例照片')}
${T('No device camera or OSS connection','未调用设备相机,未连接 OSS')}
`}
+function uploadItem(i,type='upload'){let name='IMG_'+String(85+i).padStart(3,'0')+'.jpg';const attention=type==='attention',history=type==='history';let state=attention?(i===0?T('Connection interrupted','连接中断'):T('Source file unavailable','源文件不可用')):history?[T('Uploaded','已上传'),T('Processing','处理中'),T('Ready','已就绪')][i%3]:[T('Uploading 68%','上传中 68%'),T('Waiting for network','等待网络'),T('Confirming upload','确认上传中'),T('Uploaded','已上传')][i%4];return `${photo(i+2,'upload-thumb')}${name}${history?'Today 14:'+String(32-i*3):esc(albumTitle)}
${!history?`
${T('Exhibition','展会现场')}
`:''}
${!attention&&i===0&&!history?'
':icon(attention?'help':i%3===0?'check':'clock')}${state}
${attention?`
${i===0?T('The network was lost during upload. You can retry this file.','上传时网络断开,可以重试此任务。'):T('The original file cannot be found on this device. Please select it again.','本机找不到源文件,请重新选择。')}
${button(i===0?T('Retry','重试'):T('Select again','重新选择'),i===0?'retry':'reselect','secondary')}`:history?`
${i%2?T('Other device','其他设备'):T('This device','本机')}
`:''}
`}
+function uploads(attention=false){const active=attention?'attention':uploadFilter;return bar(T('Uploads','上传'),5,``,true)+``+nav(14)}
+function historyPage(){return bar(T('Upload history','上传历史'),14)+``}
+function managePhotos(){return bar(T('Photos','照片'),6)+``}
+function photoDetails(){const hidden=hiddenPhotos.has(currentPhoto);return bar('',17,``)+`${photo(currentPhoto,'large-photo')}
${button(hidden?T('Publish again','重新公开'):T('Hide photo','隐藏照片'),hidden?'publish':'go:20','secondary','eye')}${button(T('Delete','删除'),'delete','danger','trash')}
`}
+function sharePage(){return bar(T('Share album','分享相册'),6)+``}
+function languageSheet(){return `${T('App language','应用语言')}
${[['auto','Follow device','跟随系统'],['en','English','English'],['zh','中文','中文']].map(([value,en,zh])=>``).join('')}${button(T('Done','完成'),'saveLanguage')}`}
+function categorySheet(index='new'){const edit=index!=='new';return `${edit?T('Edit category','编辑分类'):T('New category','新建分类')}
${seg([['en','English'],['zh','中文']],'en','categoryLang')}`}
+function hideSheet(){return `${T('Hide this photo?','隐藏这张照片?')}
${photo(currentPhoto)}
${T('Viewers will no longer see this photo. Authorized staff can publish it again.','观众将无法继续看到这张照片,有权限的工作人员可以重新公开。')}
${button(T('Hide photo','隐藏照片'),'confirmHide')}${button(T('Cancel','取消'),'closeSheet','ghost cancel')}`}
+function overlay(content,close='closeSheet'){const layer=document.createElement('div');layer.className='modal-layer';layer.innerHTML=``;layer.onclick=e=>{if(e.target===layer)action(close)};$('#app').append(layer);[...$('#app').children].filter(el=>el!==layer).forEach(el=>el.inert=true);layer.querySelector('input,button')?.focus({preventScroll:true});bindForms();return layer}
+function closeSheet(){const layer=$('.modal-layer');if(layer)layer.remove();[...$('#app').children].forEach(el=>el.inert=false);if([4,7,20].includes(page))go(page===4?returnPage:page===7?6:18);else $('.bar button')?.focus({preventScroll:true})}
+function render(){page=Number(location.hash.slice(1))||Number(document.body.dataset.page)||1;page=Math.max(1,Math.min(page,20));$('#app').className='app'+([13,18,20].includes(page)?' dark':'');document.documentElement.lang=lang==='zh'?'zh-CN':'en';let html='';switch(page){case 1:html=login();break;case 2:case 4:html=settings();break;case 3:html=password();break;case 5:html=albums();break;case 6:case 7:html=detail();break;case 8:html=detail(true);break;case 9:html=albumForm();break;case 10:html=albumForm(true);break;case 11:html=memberPage();break;case 12:html=categories();break;case 13:html=camera();break;case 14:html=uploads();break;case 15:html=uploads(true);break;case 16:html=historyPage();break;case 17:html=managePhotos();break;case 18:case 20:html=photoDetails();break;case 19:html=sharePage();break}$('#app').innerHTML=html;bindForms();if(page===4){draftLanguage=lang;overlay(languageSheet())}if(page===7)overlay(`${T('Album actions','相册操作')}
${row('edit',T('Edit album','编辑相册'),'','go:10')}${row('users',T('Manage members','管理成员'),'','go:11')}${row('tag',T('Categories','分类'),'','go:12')}${row('share',T('Share album','分享相册'),'','go:19')}${button(T('Cancel','取消'),'closeSheet','ghost')}`);if(page===12)overlay(categorySheet());if(page===20)overlay(hideSheet());fitPhoto();document.title=`PhotoLive · ${String(page).padStart(2,'0')} · App preview`;if(window.parent!==window)window.parent.postMessage({type:'photolive-preview-page',page},'*')}
+function go(id){if(Number(id)===page){render();return}location.hash=String(id).padStart(2,'0')}
+function toast(text){$('.toast')?.remove();const el=document.createElement('div');el.className='toast';el.setAttribute('role','status');el.textContent=text;$('#app').append(el);clearTimeout(toastTimer);toastTimer=setTimeout(()=>el.remove(),2800)}
+function fitPhoto(){const stage=$('#photoStage');if(!stage)return;const r=pics[currentPhoto%pics.length],w=Math.min(stage.clientWidth,stage.clientHeight*r[2]/r[3]);$('.large-photo').style.width=w+'px';$('.large-photo').style.height=w*r[3]/r[2]+'px'}
+function invite(){overlay(`${T('Invite colleague','邀请同事')}
${field(T('Search company accounts','搜索企业账号'),'searchMember','search','','Search')}${['Olivia Wang','James Wilson','Sophia Li'].map(name=>`
`).join('')}
${T('Demo internal directory · no invitation is sent','示例企业通讯录 · 不发送真实邀请')}
`);$('#searchMember').oninput=e=>$$('.candidate').forEach(el=>el.hidden=!el.textContent.toLowerCase().includes(e.target.value.toLowerCase()))}
+function action(raw){const [name,...rest]=raw.split(':'),value=rest.join(':');switch(name){case 'go':go(value);break;case 'language':returnPage=page;draftLanguage=lang;go(4);break;case 'saveLanguage':{const selected=$('input[name=language]:checked').value;lang=selected==='auto'?(navigator.language.startsWith('zh')?'zh':'en'):selected;go(returnPage);break}case 'closeSheet':closeSheet();break;case 'logout':overlay(`${T('Sign out of the preview?','退出预览账号?')}
${T('Production uploads remain tied to your account. This prototype contains sample tasks only.','正式版任务按账号保留,此原型中仅有示例任务。')}
${button(T('Sign out','退出'),'go:1','danger')}${button(T('Cancel','取消'),'closeSheet','ghost')}`);break;case 'help':overlay(`${T('Upload help','上传帮助')}
${T('Keep the app connected to the network. Interrupted uploads can be retried from Uploads.','请保持网络连接。中断的上传可从上传中心重试。')}
${T('Background and lock-screen uploads depend on the operating system, permissions and power settings. Reopen the app after a force quit to resume pending work.','后台和锁屏上传受到操作系统、权限和省电设置影响。强制退出后请重新打开应用恢复任务。')}
${T('Uploaded does not mean publicly available; server processing and album access still apply.','已上传不代表已公开,仍需完成服务端处理并遵守相册权限。')}
${button(T('Got it','知道了'),'closeSheet')}`);break;case 'uploadOff':toast(T('Uploads are disabled for this album.','该相册已关闭上传。'));break;case 'choose':case 'reselect':overlay(`${T('Choose sample photos','选择示例照片')}
${T('This demonstrates the photo selection flow without accessing your device library.','这里演示选图流程,不访问您的设备相册。')}
${[0,1,2].map(i=>``).join('')}
${button(T('Cancel','取消'),'closeSheet','ghost')}`);break;case 'selectSample':extraTasks++;closeSheet();toast(T('Sample photo added to the demo upload queue.','示例照片已加入演示上传队列。'));break;case 'editTab':{editTab=value;$('#detailsFields').hidden=value!=='details';$('#accessFields').hidden=value!=='access';$$('[data-action^="editTab:"]').forEach(el=>el.classList.toggle('active',el.dataset.action.endsWith(value)));break}case 'contentLanguage':overlay(`${T('Content language','内容语言')}
${button('English','setContent:English','ghost')}${button('中文','setContent:中文','ghost')}`);break;case 'setContent':$('#contentLanguage').value=value;closeSheet();toast(T('Content language draft updated.','内容语言草稿已更新。'));break;case 'invite':invite();break;case 'inviteName':if(!members.includes(value))members.push(value);closeSheet();render();toast(T('Added to the sample team.','已加入示例协作团队。'));break;case 'removeMember':overlay(`${T('Remove collaborator?','移除协作者?')}
${esc(members[Number(value)])}
${button(T('Remove from sample team','从示例团队移除'),'confirmRemove:'+value,'danger')}${button(T('Cancel','取消'),'closeSheet','ghost')}`);break;case 'confirmRemove':members.splice(Number(value),1);render();break;case 'category':overlay(categorySheet(value));break;case 'categoryLang':{const field=$('#categoryName');field.dataset[$('.tabs button.active')?.dataset.action.endsWith('zh')?'zh':'en']=field.value;field.value=field.dataset[value]||'';$$('[data-action^="categoryLang:"]').forEach(el=>el.classList.toggle('active',el.dataset.action.endsWith(value)));break}case 'cameraCategory':overlay(`${T('Select category','选择分类')}
${categoryNames.map(n=>button(esc(n),'chooseCategory:'+n,'ghost')).join('')}`);break;case 'chooseCategory':closeSheet();toast(T('Selected: ','已选择:')+value);break;case 'shutter':extraTasks++;$('#cameraNote').textContent=T('Added to demo upload queue · ','已加入演示上传队列 · ')+extraTasks;break;case 'flip':$('.camera-view>.art').style.transform=$('.camera-view>.art').style.transform?'':'scaleX(-1)';toast(T('Simulated camera direction switch','已模拟切换相机方向'));break;case 'uploadFilter':uploadFilter=value;if(value==='attention')go(15);else if(page===15)go(14);else render();break;case 'retry':retried=true;render();toast(T('Eligible sample task queued again.','可恢复的示例任务已重新排队。'));break;case 'photoFilter':photoFilter=value;render();break;case 'view':currentPhoto=Number(value);go(18);break;case 'photoPrev':case 'photoNext':{let n=currentPhoto;do{n=(n+(name==='photoNext'?1:17))%18}while(removedPhotos.has(n)&&removedPhotos.size<18);currentPhoto=n;render();break}case 'photoMore':overlay(`${T('Photo actions','照片操作')}
${button(T('View metadata','查看照片信息'),'metadata','ghost')}${button(T('Delete photo','删除照片'),'delete','danger')}${button(T('Cancel','取消'),'closeSheet','ghost')}`);break;case 'metadata':closeSheet();toast(T('Sample · Emma Chen · 2400 × 1600 · 2.8 MB','示例 · Emma Chen · 2400 × 1600 · 2.8 MB'));break;case 'confirmHide':hiddenPhotos.add(currentPhoto);go(18);break;case 'publish':hiddenPhotos.delete(currentPhoto);render();toast(T('Sample photo is visible again.','示例照片已重新公开。'));break;case 'delete':$('.modal-layer')?.remove();overlay(`${T('Delete this photo?','删除这张照片?')}
${T('This removes it from this preview only. Production deletion follows server retention rules; it does not promise immediate physical deletion from OSS.','此处仅从预览移除。正式删除遵循服务端保留规则,不代表立即从 OSS 物理删除。')}
${button(T('Delete from preview','从预览删除'),'confirmDelete','danger')}${button(T('Cancel','取消'),'closeSheet','ghost')}`);break;case 'confirmDelete':removedPhotos.add(currentPhoto);go(17);break;case 'copy':{const url=new URL('../viewer-preview-2026-09-22.html',location.href).href;if(navigator.clipboard)navigator.clipboard.writeText(url).then(()=>toast(T('Local preview link copied','本地预览链接已复制'))).catch(()=>prompt(T('Copy preview link','复制预览链接'),url));else prompt(T('Copy preview link','复制预览链接'),url);break}case 'openViewer':window.open('../viewer-preview-2026-09-22.html','_blank','noopener,noreferrer');break}}
+function bindForms(){const login=$('#loginForm');if(login)login.onsubmit=e=>{e.preventDefault();if(!$('#account').value.trim()){$('#formError').textContent=T('Enter a demo account.','请输入演示账号。');return}go(5)};const password=$('#passwordForm');if(password)password.onsubmit=e=>{e.preventDefault();const a=$('#newPassword').value,b=$('#confirmPassword').value;if(!$('#oldPassword').value||!a||a!==b){$('#formError').textContent=T('Complete all fields and make sure the new passwords match.','请填写全部字段,并确保两次新密码一致。');return}go(2);setTimeout(()=>toast(T('Validation passed · no password was saved','演示校验通过 · 未保存密码')),60)};const form=$('#albumForm');if(form)form.onsubmit=e=>{e.preventDefault();if(!$('#albumTitle').value.trim()||!$('#startDate').value||!$('#endDate').value||$('#endDate').value<$('#startDate').value){$('#formError').textContent=T('Enter a title and valid start/end dates.','请填写相册名称及有效起止日期。');return}albumTitle=$('#albumTitle').value.trim();if(page===10){publicAccess=$('#access-public').checked;uploadAllowed=$('#access-uploads').checked;downloads=$('#access-downloads').checked;original=downloads&&$('#access-original').checked}go(6)};const cat=$('#categoryForm');if(cat)cat.onsubmit=e=>{e.preventDefault();const val=$('#categoryName').value.trim();if(!val){$('#categoryError').textContent=T('A name is required.','请填写分类名称。');return}if(cat.dataset.index==='new')categoryNames.push(val);else categoryNames[Number(cat.dataset.index)]=val;closeSheet();$('#app').innerHTML=categories();toast(T('Sample category saved.','示例分类已保存。'))};const download=$('#access-downloads');if(download)download.onchange=()=>{const original=$('#access-original');original.disabled=!download.checked;if(!download.checked)original.checked=false};['historyAlbum','historyDate'].forEach(id=>{const el=$('#'+id);if(el)el.onchange=()=>{$('#historyRows').innerHTML=Array.from({length:el.selectedIndex?2:5},(_,i)=>uploadItem(i+el.selectedIndex,'history')).join('');toast(T('Sample history filter updated','示例历史筛选已更新'))}})}
+document.addEventListener('click',e=>{const eye=e.target.closest('[data-eye]');if(eye){const input=$('#'+eye.dataset.eye);input.type=input.type==='password'?'text':'password';return}const target=e.target.closest('[data-go],[data-action]');if(target){if(target.dataset.go)go(target.dataset.go);else action(target.dataset.action)}});document.addEventListener('dblclick',e=>{if(e.target.closest('.large-photo'))$('.large-photo').classList.toggle('zoom')});document.addEventListener('keydown',e=>{const layer=$('.modal-layer');if(layer){if(e.key==='Escape')closeSheet();if(e.key==='Tab'){const els=[...layer.querySelectorAll('button,input,select,textarea')].filter(x=>!x.disabled);if(els.length){if(e.shiftKey&&document.activeElement===els[0]){e.preventDefault();els.at(-1).focus()}else if(!e.shiftKey&&document.activeElement===els.at(-1)){e.preventDefault();els[0].focus()}}}}else if(page===18&&!e.target.matches('input,textarea')){if(e.key==='ArrowLeft')action('photoPrev');if(e.key==='ArrowRight')action('photoNext');if(e.key==='Escape')go(17)}});let touchStart=null;document.addEventListener('pointerdown',e=>{if(e.target.closest('.photo-stage')&&!e.target.closest('button'))touchStart=[e.clientX,e.clientY]});document.addEventListener('pointerup',e=>{if(touchStart&&page===18){const dx=e.clientX-touchStart[0],dy=e.clientY-touchStart[1];if(Math.abs(dx)>60&&Math.abs(dx)>Math.abs(dy))action(dx<0?'photoNext':'photoPrev')}touchStart=null});window.addEventListener('resize',fitPhoto);window.addEventListener('hashchange',render);render();
diff --git a/docs/designs/app-preview-2026-09-22/assets/demo-qr.svg b/docs/designs/app-preview-2026-09-22/assets/demo-qr.svg
new file mode 100644
index 0000000..d889fe1
--- /dev/null
+++ b/docs/designs/app-preview-2026-09-22/assets/demo-qr.svg
@@ -0,0 +1 @@
+
diff --git a/docs/designs/app-preview-2026-09-22/assets/reference.png b/docs/designs/app-preview-2026-09-22/assets/reference.png
new file mode 100644
index 0000000..cbf3362
Binary files /dev/null and b/docs/designs/app-preview-2026-09-22/assets/reference.png differ
diff --git a/docs/designs/app-preview-2026-09-22/build-preview.cjs b/docs/designs/app-preview-2026-09-22/build-preview.cjs
new file mode 100644
index 0000000..03ed4ca
--- /dev/null
+++ b/docs/designs/app-preview-2026-09-22/build-preview.cjs
@@ -0,0 +1,10 @@
+const fs=require('fs'),path=require('path');
+const root=__dirname;
+const pages=[['login','登录','Login'],['settings','设置','Settings'],['password','修改密码','Change password'],['language','语言选择','App language'],['albums','相册列表','Albums'],['album-detail','相册详情','Album overview'],['album-actions','相册操作','Album actions'],['empty-album','空相册','Empty album'],['create-album','创建相册','New album'],['edit-album','编辑相册','Edit album'],['members','协作成员','Members'],['categories','分类管理','Categories'],['camera','拍摄','Camera'],['uploads','上传中心','Uploads'],['upload-errors','异常任务','Needs attention'],['upload-history','上传历史','Upload history'],['photos','照片管理','Manage photos'],['photo-detail','管理大图','Photo details'],['share','相册分享','Share album'],['hide-photo','隐藏确认','Hide photo']].map(([slug,zh,en],i)=>({id:i+1,file:`${String(i+1).padStart(2,'0')}-${slug}.html`,zh,en}));
+fs.mkdirSync(path.join(root,'assets'),{recursive:true});
+fs.copyFileSync(path.join(root,'../viewer-mobile-concept-2026-09-21.png'),path.join(root,'assets/reference.png'));
+for(const page of pages)fs.writeFileSync(path.join(root,page.file),`PhotoLive · ${page.en}`);
+fs.writeFileSync(path.join(root,'pages.js'),'window.PREVIEW_PAGES='+JSON.stringify(pages,null,2)+';');
+const QR=require(require.resolve('qrcode',{paths:[path.resolve(root,'../../../apps/admin')]}));
+QR.toString('https://example.com/photolive-preview',{type:'svg',margin:2,color:{dark:'#172033',light:'#ffffff'}},(err,svg)=>{if(err)throw err;fs.writeFileSync(path.join(root,'assets/demo-qr.svg'),svg)});
+console.log('Generated 20 page entry points and offline assets.');
diff --git a/docs/designs/app-preview-2026-09-22/index.html b/docs/designs/app-preview-2026-09-22/index.html
new file mode 100644
index 0000000..155689b
--- /dev/null
+++ b/docs/designs/app-preview-2026-09-22/index.html
@@ -0,0 +1,10 @@
+PhotoLive · App 全页面交互预览UI–03 / INTERACTIVE PROTOTYPE
真实 HTML 组件中英文可点击跳转本地摄影素材
在左侧选择页面,或直接在原型内操作。登录页与设置页可切换语言。相机、上传、成员和权限修改均为当前预览会话的模拟状态。
分享页二维码为演示码,指向 example.com;“打开观众页预览”可连接此前生成的观众页文件。
diff --git a/docs/designs/app-preview-2026-09-22/pages.js b/docs/designs/app-preview-2026-09-22/pages.js
new file mode 100644
index 0000000..55d9390
--- /dev/null
+++ b/docs/designs/app-preview-2026-09-22/pages.js
@@ -0,0 +1,122 @@
+window.PREVIEW_PAGES=[
+ {
+ "id": 1,
+ "file": "01-login.html",
+ "zh": "登录",
+ "en": "Login"
+ },
+ {
+ "id": 2,
+ "file": "02-settings.html",
+ "zh": "设置",
+ "en": "Settings"
+ },
+ {
+ "id": 3,
+ "file": "03-password.html",
+ "zh": "修改密码",
+ "en": "Change password"
+ },
+ {
+ "id": 4,
+ "file": "04-language.html",
+ "zh": "语言选择",
+ "en": "App language"
+ },
+ {
+ "id": 5,
+ "file": "05-albums.html",
+ "zh": "相册列表",
+ "en": "Albums"
+ },
+ {
+ "id": 6,
+ "file": "06-album-detail.html",
+ "zh": "相册详情",
+ "en": "Album overview"
+ },
+ {
+ "id": 7,
+ "file": "07-album-actions.html",
+ "zh": "相册操作",
+ "en": "Album actions"
+ },
+ {
+ "id": 8,
+ "file": "08-empty-album.html",
+ "zh": "空相册",
+ "en": "Empty album"
+ },
+ {
+ "id": 9,
+ "file": "09-create-album.html",
+ "zh": "创建相册",
+ "en": "New album"
+ },
+ {
+ "id": 10,
+ "file": "10-edit-album.html",
+ "zh": "编辑相册",
+ "en": "Edit album"
+ },
+ {
+ "id": 11,
+ "file": "11-members.html",
+ "zh": "协作成员",
+ "en": "Members"
+ },
+ {
+ "id": 12,
+ "file": "12-categories.html",
+ "zh": "分类管理",
+ "en": "Categories"
+ },
+ {
+ "id": 13,
+ "file": "13-camera.html",
+ "zh": "拍摄",
+ "en": "Camera"
+ },
+ {
+ "id": 14,
+ "file": "14-uploads.html",
+ "zh": "上传中心",
+ "en": "Uploads"
+ },
+ {
+ "id": 15,
+ "file": "15-upload-errors.html",
+ "zh": "异常任务",
+ "en": "Needs attention"
+ },
+ {
+ "id": 16,
+ "file": "16-upload-history.html",
+ "zh": "上传历史",
+ "en": "Upload history"
+ },
+ {
+ "id": 17,
+ "file": "17-photos.html",
+ "zh": "照片管理",
+ "en": "Manage photos"
+ },
+ {
+ "id": 18,
+ "file": "18-photo-detail.html",
+ "zh": "管理大图",
+ "en": "Photo details"
+ },
+ {
+ "id": 19,
+ "file": "19-share.html",
+ "zh": "相册分享",
+ "en": "Share album"
+ },
+ {
+ "id": 20,
+ "file": "20-hide-photo.html",
+ "zh": "隐藏确认",
+ "en": "Hide photo"
+ }
+];
\ No newline at end of file
diff --git a/docs/designs/app-preview-2026-09-22/styles.css b/docs/designs/app-preview-2026-09-22/styles.css
new file mode 100644
index 0000000..3dd2d24
--- /dev/null
+++ b/docs/designs/app-preview-2026-09-22/styles.css
@@ -0,0 +1 @@
+:root{--blue:#1a73e8;--bg:#f6f8fb;--surface:#fff;--ink:#172033;--muted:#69778e;--line:#e5eaf1;--soft:#edf4ff;--red:#e74e58;--green:#279966}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.5 -apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC',sans-serif}button,input,select,textarea{font:inherit}button,a,input,select,textarea{-webkit-tap-highlight-color:transparent}button{border:0;background:none;color:inherit;cursor:pointer;min-height:44px}button:focus-visible,a:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible{outline:3px solid #79b4ff;outline-offset:2px}a{color:var(--blue)}svg.icon{width:22px;height:22px;flex:none;vertical-align:middle;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}h1,h2,h3,p{margin:0}h1{font-size:24px;font-weight:700;line-height:1.25;letter-spacing:-.5px}h2{font-size:20px;line-height:1.35;letter-spacing:-.3px}h3{font-size:16px;font-weight:650}small,.small{font-size:12px}.muted{color:var(--muted)}.blue{color:var(--blue)}.red{color:var(--red)}.green{color:var(--green)}.amber{color:#d89020}.app{height:100dvh;max-width:600px;margin:auto;display:flex;flex-direction:column;position:relative;overflow:hidden;background:var(--bg)}.app.dark{background:#10161e;color:#fff}.safe-top{height:24px;flex:none;background:var(--surface)}.dark .safe-top{background:#10161e}.bar{height:56px;flex:none;display:flex;align-items:center;gap:8px;padding:0 12px;background:var(--surface)}.bar .back,.bar .right{width:44px;flex:none;display:grid;place-items:center}.bar .right{margin-left:auto}.bar .bar-title{font-size:18px;font-weight:650;flex:1;line-height:1.25}.bar.root{height:62px;padding:0 20px;background:transparent}.bar.root h1{flex:1}.bar.dark{background:#10161e}.bar .plus{width:34px;height:34px;min-height:34px;border-radius:50%;background:var(--blue);color:white;display:grid;place-items:center}.scroll{flex:1;min-height:0;overflow:auto;overscroll-behavior:contain;padding:16px}.scroll.flush{padding:0}.scroll.white{background:var(--surface)}.scroll::-webkit-scrollbar{width:4px}.scroll::-webkit-scrollbar-thumb{background:#b3bfd04d;border-radius:8px}.nav{display:flex;flex:none;height:92px;padding:7px 16px 24px;background:var(--surface);border-top:1px solid var(--line);justify-content:space-around}.nav button{width:33%;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;color:var(--muted);font-size:11px;position:relative}.nav button.active{color:var(--blue)}.nav svg{width:24px;height:24px}.nav .badge{position:absolute;background:#f15b64;color:white;top:0;left:calc(50% + 6px);font-size:10px;padding:0 5px;min-width:16px;border-radius:10px;border:2px solid white}.actions{display:flex;gap:10px;flex:none;padding:12px 16px calc(20px + env(safe-area-inset-bottom));background:var(--surface);border-top:1px solid var(--line)}.actions>*{flex:1}.btn{display:flex;align-items:center;justify-content:center;gap:8px;min-height:48px;padding:12px 16px;border-radius:12px;font-weight:600;font-size:14px;text-decoration:none}.btn.primary{background:var(--blue);color:white}.btn.secondary{border:1px solid #89b5f0;color:var(--blue);background:var(--surface)}.btn.ghost{background:#eef2f7;color:#42536d}.btn.danger{color:var(--red);background:#fff1f2}.btn.full{width:100%}.stack{display:flex;flex-direction:column;gap:16px}.card{background:var(--surface);border:1px solid #edf0f5;border-radius:16px;padding:16px}.group{background:var(--surface);border-radius:16px;padding:0 16px;border:1px solid #edf0f5}.row{display:flex;align-items:center;gap:12px;min-height:64px;border-bottom:1px solid var(--line);width:100%;text-align:left;padding:12px 0}.row:last-child{border-bottom:0}.row .grow{flex:1;min-width:0}.row .arrow{color:#8694a9}.row p{font-size:12px;color:var(--muted);margin-top:3px}.row .trailing{color:var(--muted);font-size:13px;margin-left:auto}.avatar{width:48px;height:48px;border-radius:50%;display:grid;place-items:center;background:#e3edff;color:#2862ab;font-size:17px;flex:none;font-weight:600}.avatar.lg{width:66px;height:66px;font-size:22px}.avatar.lilac{background:#eeebff;color:#7060bd}.avatar.mint{background:#e6f3ee;color:#398d70}.art{position:relative;overflow:hidden;background:#dce6ef}.art img{position:absolute;max-width:none;pointer-events:none}.tag{border-radius:7px;display:inline-flex;align-items:center;gap:5px;font-size:11px;padding:5px 10px;background:#edf4ff;color:#49658e}.tag svg{width:15px;height:15px}.tag.success{background:#eaf6ef;color:#288358}.status-row{display:flex;gap:10px;margin-top:12px}.hint-card{padding:15px;border-radius:12px;background:#edf3fc;display:flex;gap:12px;color:#526b8d;font-size:13px}.hint-card svg{flex:none}.field{display:flex;flex-direction:column;gap:7px;margin-bottom:18px;font-size:13px}.field:last-child{margin-bottom:0}.field input,.field select,.field textarea{border:1px solid #dbe3ef;border-radius:10px;min-height:48px;padding:12px;color:var(--ink);background:var(--surface);width:100%;min-width:0}.field textarea{min-height:96px;resize:vertical}.input-wrap{display:flex;align-items:center;position:relative}.input-wrap>svg{position:absolute;left:12px;color:#78889e;width:19px}.input-wrap input{padding-left:40px;padding-right:44px}.input-wrap button{position:absolute;right:2px;top:2px;width:42px;min-height:42px;color:#78889e}.input-wrap button svg{width:19px}.error{color:var(--red);font-size:12px;min-height:18px;margin-top:8px}.note{color:var(--muted);font-size:12px;line-height:1.55}.section-label{font-size:14px;font-weight:600;margin:8px 0 12px}.tabs{display:flex;gap:4px;padding:4px;border-radius:9px;background:#edf1f6;margin-bottom:16px}.tabs button{flex:1;min-height:36px;font-size:12px;border-radius:7px;padding:5px 8px}.tabs button.active{background:var(--blue);color:white}.tabs.light button.active{background:#dbeaff;color:#145bb9}.album-card{display:block;width:100%;padding:0;border-radius:16px;position:relative;overflow:hidden;text-align:left;height:200px;background:#263c54}.album-card+.album-card{margin-top:16px}.album-card .art{position:absolute;inset:0}.album-card .scrim{position:absolute;inset:30% 0 0;background:linear-gradient(transparent,#0c1728e8)}.album-card .caption{position:absolute;bottom:14px;left:14px;right:14px;color:white}.album-card h3{font-size:17px;margin-bottom:3px}.album-card p{font-size:12px;opacity:.85}.album-card .caption-tags{display:flex;gap:12px;font-size:11px;margin-top:9px}.album-card .caption-tags span{display:flex;align-items:center;gap:6px}.caption-tags svg{width:16px;height:16px}.album-hero{height:164px;width:100%;flex:none}.album-info{padding:15px 16px 12px;background:var(--surface)}.album-info h2{margin-bottom:9px}.metadata{font-size:12px;color:var(--muted);display:flex;align-items:center;gap:7px;margin-top:5px}.metadata svg{width:16px;height:16px}.quick{display:flex;padding:6px 16px 16px;gap:8px;background:var(--surface)}.quick button{flex:1;min-width:0;border-radius:12px;background:#f3f7fd;padding:11px 3px;display:flex;flex-direction:column;align-items:center;gap:7px;font-size:10px;color:#42618b}.recent{padding:0 16px 16px;background:var(--surface)}.heading-row{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}.heading-row button{color:var(--blue);font-size:12px;min-height:32px}.photo-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:4px}.photo-grid button{padding:0;aspect-ratio:1;position:relative;overflow:hidden;border-radius:3px}.photo-grid .art{width:100%;height:100%}.photo-grid .flag{position:absolute;left:4px;bottom:4px;max-width:calc(100% - 8px);font-size:9px;padding:3px 5px;border-radius:5px;background:#172033c9;color:white;display:flex;gap:3px;align-items:center}.photo-grid .flag svg{width:11px;height:11px}.photo-grid .is-hidden{filter:brightness(.55)}.empty{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:38px 22px;min-height:280px;background:var(--surface)}.empty-illustration{width:105px;height:90px;display:grid;place-items:center;border-radius:12px;background:#edf3fb;border:6px solid #f7faff;transform:rotate(-7deg);margin-bottom:25px;box-shadow:-13px -9px 0 -2px #e8eff8}.empty-illustration svg{width:56px;height:56px;color:#a5b9d4}.empty p{font-size:13px;margin-top:8px;color:var(--muted)}.mosaic{display:grid;grid-template-columns:repeat(3,1fr);gap:4px;height:238px;flex:none}.mosaic .art{height:117px}.login-content{text-align:center;padding:22px 20px 24px}.wordmark{font-size:25px;font-weight:750;display:flex;align-items:center;justify-content:center;gap:8px;letter-spacing:-1px}.wordmark svg{color:var(--blue);width:30px;height:30px}.login-content h1{margin:24px 0 5px;font-size:27px}.login-form{text-align:left;margin-top:25px}.login-form .field{margin-bottom:12px}.login-note{margin-top:24px;text-align:center;font-size:11px;color:#8390a4}.login-top{height:44px;display:flex;justify-content:flex-end;padding:0 20px;background:white}.login-top button{font-size:12px;display:flex;align-items:center;gap:4px}.login-top svg{width:14px}.switch{appearance:none;width:48px;height:28px;border-radius:16px;background:#d9e0e9;position:relative;cursor:pointer;flex:none}.switch:before{content:'';position:absolute;left:3px;top:3px;width:22px;height:22px;background:white;border-radius:50%;box-shadow:0 1px 3px #1925361a;transition:transform .15s}.switch:checked{background:var(--blue)}.switch:checked:before{transform:translateX(20px)}.switch:disabled{opacity:.4;cursor:default}.access-row{min-height:103px}.access-row strong{display:block;font-size:14px}.access-row p{max-width:245px;font-size:12px}.summary{padding:12px 4px 24px}.summary .big{font-size:32px;line-height:1.4;font-weight:750;letter-spacing:-1px;margin:3px 0}.summary .counts{font-size:12px;color:var(--muted)}.upload-row{display:flex;gap:12px;background:var(--surface);border:1px solid var(--line);border-radius:12px;padding:12px;margin-bottom:10px}.upload-thumb{width:76px;height:88px;border-radius:6px;flex:none}.upload-data{flex:1;min-width:0}.upload-data strong{font-size:13px}.upload-data p{font-size:11px;color:var(--muted);margin:3px 0}.upload-status{display:flex;gap:5px;align-items:center;font-size:11px;margin-top:8px}.upload-status svg{width:16px;height:16px}.progress{height:5px;border-radius:3px;background:#e0ebfc;overflow:hidden;flex:1}.progress span{display:block;width:68%;height:100%;background:var(--blue);border-radius:3px}.upload-row .btn{min-height:34px;padding:5px 10px;font-size:12px;border-radius:8px;width:100%;margin-top:10px}.history-row{border:0;border-bottom:1px solid var(--line);border-radius:0;padding:12px 0;margin:0;background:transparent}.camera-view{flex:1;min-height:0;position:relative;overflow:hidden}.camera-view>.art{height:100%;width:100%}.camera-chrome{height:116px;flex:none;display:flex;align-items:center;justify-content:space-around;padding-bottom:14px}.shutter{width:74px;height:74px;border:3px solid white;border-radius:50%;padding:4px;background:transparent}.shutter:before{content:'';display:block;width:100%;height:100%;background:white;border-radius:50%}.camera-thumb{width:48px;height:56px;overflow:hidden;border-radius:7px;padding:0;border:1px solid #fff9}.camera-thumb .art{height:100%;width:100%}.camera-note{position:absolute;left:50%;bottom:18px;transform:translateX(-50%);background:#10161ecc;padding:8px 14px;border-radius:9px;font-size:11px;white-space:nowrap;border:1px solid #ffffff33}.photo-stage{flex:1;min-height:80px;display:flex;align-items:center;justify-content:center;position:relative;overflow:hidden;touch-action:pan-y}.photo-stage .art{max-width:100%;max-height:100%;cursor:zoom-in}.photo-stage .art.zoom{transform:scale(1.5)}.photo-stage .arrow{position:absolute;top:calc(50% - 22px);width:44px;height:44px;border-radius:50%;background:#0005;display:grid;place-items:center;z-index:2}.photo-stage .arrow.prev{left:6px}.photo-stage .arrow.next{right:6px}.photo-meta{flex:none;padding:18px 16px 12px;color:#c6cdd6;font-size:12px}.photo-meta .topline{display:flex;align-items:center;justify-content:space-between;margin-bottom:5px;color:white;font-size:15px}.dark .actions{background:#10161e;border-color:#293440}.dark .btn.secondary{background:transparent;border-color:#526077;color:white}.dark .btn.danger{background:transparent;color:#ff7784}.share-card{text-align:center;padding:12px}.share-card .cover{height:190px;border-radius:12px}.share-card .share-info{text-align:left;padding:16px 4px}.qr-area{padding:12px;background:#f5f7fa;border-radius:12px;margin-bottom:16px}.qr-area img{width:156px;height:156px;display:block;margin:auto}.qr-area p{font-size:12px;color:#74829a;margin-top:8px}.share-note{text-align:center;font-size:11px;color:var(--muted);margin-top:14px}.modal-layer{position:absolute;inset:0;background:#111b2b78;z-index:10;display:flex;align-items:flex-end}.sheet{background:var(--surface);color:var(--ink);border-radius:22px 22px 0 0;width:100%;max-height:88%;overflow:auto;padding:12px 20px 28px;box-shadow:0 -10px 30px #0c1c3012}.handle-line{width:44px;height:4px;background:#d5dde7;border-radius:3px;margin:2px auto 20px}.sheet h2{font-size:20px;margin-bottom:18px}.sheet .row{min-height:62px}.sheet .row>input[type=radio]{accent-color:var(--blue);width:21px;height:21px;margin-right:10px}.sheet .btn{margin-top:16px;width:100%}.sheet .preview-row{display:flex;align-items:center;gap:16px;margin-bottom:18px;font-size:13px;color:var(--muted)}.sheet .preview-row .art{width:76px;height:80px;border-radius:8px;flex:none}.sheet .cancel{margin-top:8px}.toast{position:absolute;z-index:30;bottom:110px;left:20px;right:20px;background:#203651ed;color:white;border-radius:12px;padding:13px 16px;text-align:center;font-size:12px;box-shadow:0 4px 20px #0f203633}.app .demo-label{font-size:10px;color:#8b98ac;text-align:center;padding:7px 12px}.sheet-close{float:right;min-height:32px;width:32px}.help-copy p{margin:12px 0;color:var(--muted);font-size:13px}.empty-list{text-align:center;padding:40px 16px;color:var(--muted)}[hidden]{display:none!important}@media(max-width:350px){.btn{font-size:12px;padding:10px}.mosaic{height:190px}.mosaic .art{height:93px}.login-content h1{font-size:24px}.upload-thumb{width:62px}.photo-grid{grid-template-columns:repeat(2,1fr)}}@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important;transition:none!important}}
diff --git a/docs/designs/embed-preview-2026-09-22.html b/docs/designs/embed-preview-2026-09-22.html
new file mode 100644
index 0000000..15ae694
--- /dev/null
+++ b/docs/designs/embed-preview-2026-09-22.html
@@ -0,0 +1,28 @@
+PhotoLive · 嵌入照片窗预览CONNECT. EXPLORE. BE INSPIRED.
A world of ideas.
A moment to connect.
Meet the people, stories and possibilities shaping tomorrow. Experience the fair, wherever you are.
Explore the event ↗Inside the experience
GUANGZHOU · AUTUMN 2026A closer look at the conversations and discoveries that bring us together.
diff --git a/docs/designs/embed-preview-desktop-2026-09-22.png b/docs/designs/embed-preview-desktop-2026-09-22.png
new file mode 100644
index 0000000..b432626
Binary files /dev/null and b/docs/designs/embed-preview-desktop-2026-09-22.png differ
diff --git a/docs/designs/embed-preview-mobile-2026-09-22.png b/docs/designs/embed-preview-mobile-2026-09-22.png
new file mode 100644
index 0000000..8857d6f
Binary files /dev/null and b/docs/designs/embed-preview-mobile-2026-09-22.png differ
diff --git a/docs/designs/ui04-implementation/actual/desktop/e02-static-desktop.png b/docs/designs/ui04-implementation/actual/desktop/e02-static-desktop.png
index 941d71c..129f197 100644
Binary files a/docs/designs/ui04-implementation/actual/desktop/e02-static-desktop.png and b/docs/designs/ui04-implementation/actual/desktop/e02-static-desktop.png differ
diff --git a/docs/designs/ui04-implementation/actual/mobile/e02-static-mobile.png b/docs/designs/ui04-implementation/actual/mobile/e02-static-mobile.png
index ec3aa6b..69c2343 100644
Binary files a/docs/designs/ui04-implementation/actual/mobile/e02-static-mobile.png and b/docs/designs/ui04-implementation/actual/mobile/e02-static-mobile.png differ
diff --git a/docs/designs/ui04-implementation/recordings/e03-scroll-32s.webm b/docs/designs/ui04-implementation/recordings/e03-scroll-32s.webm
index 246b522..34a7b8d 100644
Binary files a/docs/designs/ui04-implementation/recordings/e03-scroll-32s.webm and b/docs/designs/ui04-implementation/recordings/e03-scroll-32s.webm differ
diff --git a/docs/designs/viewer-preview-2026-09-22.html b/docs/designs/viewer-preview-2026-09-22.html
new file mode 100644
index 0000000..39813b3
--- /dev/null
+++ b/docs/designs/viewer-preview-2026-09-22.html
@@ -0,0 +1,29 @@
+
+PhotoLive · Gallery preview
diff --git a/docs/designs/viewer-preview-desktop-2026-09-22.png b/docs/designs/viewer-preview-desktop-2026-09-22.png
new file mode 100644
index 0000000..6138195
Binary files /dev/null and b/docs/designs/viewer-preview-desktop-2026-09-22.png differ
diff --git a/docs/designs/viewer-preview-lightbox-2026-09-22.png b/docs/designs/viewer-preview-lightbox-2026-09-22.png
new file mode 100644
index 0000000..84657d2
Binary files /dev/null and b/docs/designs/viewer-preview-lightbox-2026-09-22.png differ
diff --git a/docs/designs/viewer-preview-mobile-2026-09-22.png b/docs/designs/viewer-preview-mobile-2026-09-22.png
new file mode 100644
index 0000000..18a4822
Binary files /dev/null and b/docs/designs/viewer-preview-mobile-2026-09-22.png differ
diff --git a/docs/plans/2026-09-22-UI03R-App高保真还原完整实施方案.md b/docs/plans/2026-09-22-UI03R-App高保真还原完整实施方案.md
new file mode 100644
index 0000000..f843a40
--- /dev/null
+++ b/docs/plans/2026-09-22-UI03R-App高保真还原完整实施方案.md
@@ -0,0 +1,550 @@
+# UI-03R:App 全页面高保真还原实施方案(替代 UI-03 的视觉验收标准)
+
+版本:2.0
+日期:2026-09-22
+状态:待执行。本文只规划调整,不代表页面已还原。
+目标:以五张设计板为视觉依据,逐页修复已实现 App 的形态偏差,交付可核验的真实 Flutter 界面。
+
+## 0. 给执行者的强制说明
+
+这不是自由设计任务,也不是“功能存在即可”的美化任务。先看图、再看本文件、再检查当前代码,按既定规格实现。保留现有业务和上传引擎,重点替换不匹配的布局与组件外观。
+
+本文件替代 UI-03 中“接近设计板”“约25%–32%”“常规样式自主决定”等宽泛表述。对于未被本文覆盖的业务规则,继续执行 UI-03/UI-01 与现有权限契约;Web/嵌入组件不在范围。
+
+本方案的数值是依据效果图重新制定的**工程还原基准**,不是声称从 AI 图片精确测得的全部原始设计参数。AI 图中的字体、图标、文字和画面存在细微不一致,不可能用自动像素差证明100%一致;必须采用“固定素材和数据 + 固定视口 + 布局测量 + 逐页视觉复核”组合验收。不能口头承诺像素级一致后省略证据。
+
+- 不得直接把设计板作为运行界面背景。
+- 不得重新发挥为另一套 Material 默认风格。
+- 不得只更新颜色、标题、注释和验收表。
+- 不得用缺素材/空数据页面证明照片主导页面还原完成。
+- 不得把截图工作推迟到全部页面完成之后。
+- 不得把 widget/集成测试通过等同于视觉验收通过。
+- 不得用“模拟器不是手机”为由跳过能在固定 widget surface 完成的布局检查。
+- 不覆盖用户当前改动,不自动提交、推送、打标签或部署。
+
+## 1. 设计依据与已核实偏差
+
+### 1.1 唯一视觉参考
+
+| 板 | 文件 | 页面 |
+|---|---|---|
+| 01 | [账号与设置](../designs/app-redesign-2026-09-21/01-account-settings.png) | 01–04 |
+| 02 | [相册工作台](../designs/app-redesign-2026-09-21/02-albums-workspace.png) | 05–08 |
+| 03 | [创建与协作](../designs/app-redesign-2026-09-21/03-create-collaborate.png) | 09–12 |
+| 04 | [拍摄与上传](../designs/app-redesign-2026-09-21/04-capture-uploads.png) | 13–16 |
+| 05 | [照片与分享](../designs/app-redesign-2026-09-21/05-photos-share.png) | 17–20 |
+
+每张四屏,按从左到右读取。外部标题、设备外圆角和画板灰底不画入 App。效果图中的年份、人物账号、二维码、描述字数不构成业务规则。
+
+### 1.2 已检查的运行截图与代码问题
+
+依据:`docs/plans/progress/ui03-screenshots/after/` 的 01-login、05b-albums-en、06-album-detail、14-uploads,以及当前 theme.dart 和页面代码。
+
+| 偏差 | 实际观察 | 本轮修正 |
+|---|---|---|
+| 登录摄影素材缺失 | 顶部蓝色渐变和半透明矩形 | 必须真实摄影拼贴;占位不能通过主视觉验收 |
+| 登录品牌/排版不同 | 品牌与标题左对齐、中文“现场影像”、默认字段外观 | 英文品牌固定、主体居中、字段图标/间距按页面01 |
+| 相册封面缺失 | 灰色占位成为主视觉 | 使用可追溯测试摄影素材和有效封面响应验收正常态 |
+| 根导航形态不同 | 紫蓝胶囊指示器、较重默认样式 | 无胶囊,蓝色选中图标和文字 |
+| 详情首屏信息位置不同 | 标题在AppBar重复,封面占高,工具组过厚,照片首屏几乎不可见 | 顶栏仅返回/更多;标题在封面下;薄工具区;照片在首屏出现 |
+| 上传摘要不同 | 白色大卡、完成数字弱、彩色文字分散 | 直接排在背景上,32号主数字、紧凑同一副行 |
+| 上传筛选不同 | 带黑边的默认SegmentedButton胶囊 | 轻底色圆角8分段,选中实蓝,无外描边 |
+| 上传条目不同 | 相册显示内部ID、素材为测试色块、信息层级松散 | 显示真实名称;固定缩略图和状态排版 |
+| 验收证据不足 | 多页面无截图;05b文件名为en而画面仍为中文 | 所有20项保存真实指定语言截图并人工核对文件与内容 |
+| 视口不一致 | 历史设备540×960 @240dpi,不能直接与设计板比较 | 固定逻辑画布,另保留设备截图,不按物理像素混比 |
+
+历史 UI03 验收记录用于了解实现,不作为本轮视觉通过证据。保留历史记录,新建 UI03R 记录,不篡改旧记录。
+
+## 2. 开发顺序与决策边界
+
+先完整修好 **01登录、05相册列表、14上传中心** 三个样板页并截图对照,再复用组件推广其余17项。不能先机械改完20页再补截图。
+
+出现冲突按以下优先级:
+1. 权限、数据真实性、账号隔离与上传可靠性。
+2. 本文冻结的布局和组件规格。
+3. 对应设计图的视觉层级与图片比例。
+4. 原UI-03一般性说明。
+5. Flutter组件默认值(优先级最低,不能作为不还原的理由)。
+
+常规组件拆分无需询问;素材或环境缺失可以继续其他任务,但相关视觉状态标为“未完成”,不能用文字说明换取通过。真机拍摄、iOS编译等资源关口与UI布局验收分开登记。
+
+## 3. 固定比较环境
+
+### 3.1 参考画布与坐标
+
+- 规范画布:逻辑 **390×844 dp**,英文、浅色、文字缩放1.0、无键盘。
+- 为稳定渲染,测试固定顶部 inset=24、底部 inset=24;测试画布的坐标包含这些区域。
+- 正式App使用真实系统inset,不绘制假状态栏、时间、电池或Home指示线;截图比较时系统栏蒙版排除。
+- 常规内容可用区域 y=24..820;内页顶栏 y=24..80,高56。
+- 根导航视觉主体 y=752..820,高68,底部系统区另计;root body不能延伸到导航下面而无法滚达。
+- 内页固定双按钮区主体高72,底部再加实际安全区。
+- 下述y坐标适用于指定参考画布和短英文内容,只用于约束首屏组成。生产长文字、大字体允许向下自然展开并滚动,禁止为保坐标裁字。
+- 不用整体Transform.scale缩放390画布适配所有设备。以真实约束自适应,保持按钮触控和字号。
+
+### 3.2 两套证据必须都有
+
+A. 固定画布widget截图:本地确定性素材/数据,复用真实页面及组件,验证布局。明确标记视觉fixture,不能声称相机或网络实装成功。
+B. 模拟器/真机运行截图:真实页面入口与隔离测试API,验证路由、交互、实际inset和资源加载。
+
+需要系统检查逻辑尺寸与DPR,不能看到540px截图就当540dp。缺390设备时固定widget画布完成精确比较,再在现有设备补运行检查;不能强制修改用户设备全局分辨率而不恢复。
+
+### 3.3 统一截图状态
+
+固定测试时间、账号、英文文案、相册封面与图片顺序;禁动画、等待字体和图片加载完成、隐藏debug banner。不要直接复用某张错误态截图充当正常态。
+
+## 4. 素材、字体、图标:先到位再调排版
+
+### 4.1 素材清单(必做)
+
+建立 `docs/designs/ui03r-assets-manifest.md`:
+
+| 素材ID | 用途 | 最低要求 |
+|---|---|---|
+| brand-mark | 登录/列表品牌标志 | 同一蓝色光圈或已确认正式标志,所有页一致 |
+| login-01..06 | 登录六图拼贴 | 清晰活动摄影,竖横图可裁切,风格与设计板同类 |
+| event-cover-01..03 | 三张相册封面 | 展馆/活动/团队各一,至少720px宽 |
+| event-photo-01..12 | 网格、缩略图、大图 | 真实摄影风格;包含横图、竖图、人物、产品、场地 |
+| avatar-owner | 设置/成员 | 有合法素材才用照片,否则首字母;记录视觉偏差 |
+| empty-album | 空相册插画 | 低对比叠放照片图形,不能换成大彩色卡通 |
+
+逐项记录路径、来源、许可或用户提供依据、尺寸、SHA256、用途、是否仅测试使用。
+
+**取消旧计划中以渐变占位视为可交付的做法。** 缺摄影素材时优先查仓库已有资源;可复用用户已认可的生成图中干净摄影区域作为测试参考(不得包含UI文字/边框,质量不足不可使用),或在有图像工具与授权条件时生成独立摄影素材。是否可执行裁切/生成按实际工具规范操作。生产素材来源未确定则单独列阻塞,不假称完成。不得从网络随便抓图。
+
+将测试照片存入测试fixture,生产登录摄影拼贴使用随App打包的合规资源;生产相册卡仍读取真实API,不给所有用户硬塞测试封面。测试API资产必须可访问且未过期。
+
+### 4.2 字体冻结
+
+- 拉丁字形目标:Inter 或仓库已经具备的视觉等价字体,400/500/600/700四档,优先本地随包。
+- 先检查字体可用性与许可;不可联网获取时不能假称已使用Inter。登记替代字体和字宽差异,对照校准后再通过。
+- 中文回退到可用中文系统无衬线字体,不强行用拉丁字体显示方块。
+- 不在每页随机指定fontFamily,不运行时下载字体。
+- 设置字体后重新测量文本,因为默认Roboto与Inter会改变换行。
+- 表格中数字采用tabularFigures;禁全局锁死textScaler。
+
+### 4.3 文字样式(实际写入TextStyle)
+
+| token | size/lineHeight(dp) | weight | 用途 |
+|---|---|---|---|
+| brand | 26/32 | 700 | PhotoLive |
+| pageTitle | 24/30 | 700 | Albums/Settings |
+| innerTitle | 18/24 | 600 | 返回页标题 |
+| heroTitle | 28/34 | 700 | 登录主标题 |
+| sectionTitle | 17/24 | 600 | Recent photos |
+| body | 15/22 | 400 | 说明 |
+| field | 15/20 | 400 | 输入内容 |
+| rowTitle | 15/20 | 600 | 列表标题 |
+| secondary | 13/18 | 400 | 时间地点 |
+| caption | 12/16 | 400 | 辅助标签 |
+| button | 15/20 | 600 | CTA |
+| metric | 32/40 | 700 | 82 uploaded |
+
+不要把所有AppBar都设成24:内页居中18,根页左对齐24,摄影页独立。
+
+### 4.4 图标冻结
+
+同一种线型,常规22dp,线宽视觉约1.8–2。若使用现有Material图标,逐项选择outlined版本;禁止相同入口在不同页换成info/check等不相关图标。
+Public=globe,Uploads on=cloud upload,Members=people,Categories=tag,Share=share nodes,History=clock arrow,Settings=gear。
+底部选中可填充,同类轮廓一致。没有正式Logo时不要每页使用不同的相机/图库图标冒充品牌。
+
+## 5. 组件级样式冻结
+
+### 5.1 配色与表面
+
+```text
+primary #1A73E8
+page #F6F8FB
+surface #FFFFFF
+textPrimary #172033
+textSecondary #647084
+line #E5EAF1
+softBlue #EEF5FF
+photoBackground #10161E
+successText #16794B
+waitingText #996300
+errorText #B42318
+```
+
+- 禁止种子派生色自动渗入导航、开关、筛选、操作卡。显式指定每个组件状态的颜色。
+- 普通卡无明显浮起阴影;需要阴影时black 4%、blur12、offset(0,3),不同时粗边框+重阴影。
+- card radius16,form field/button radius10,segment radius8,image tile radius4。
+- 错误、等待色不映射成全局secondary后被导航当选中色使用。
+
+### 5.2 根底部导航(必须替换当前胶囊样式)
+
+- 可用NavigationBar但indicatorColor透明、indicatorShape不显示、selected/unselected颜色显式分开;若默认内部布局无法满足则用语义完整的自定义三等分组件。
+- 白底、顶部1px line、主体高68,图标24,上距12,图标与标签间6,标签11/14。
+- 选中图标+标签primary,未选textSecondary;无圆角底色、无紫色胶囊。
+- 每个项目触控区域整个三等分格。
+- badge直径16,红底白字10,靠上传图标右上;99以上显示99+,不推移布局。
+
+### 5.3 顶栏与操作
+
+- 根页标题区56高,左右20;内页返回按钮44,居中innerTitle,右侧44操作。
+- 内页不在AppBar再重复活动名;详情活动名只在封面下。
+- surfaceTintColor透明,elevation=0,scrolledUnderElevation=0。
+- 大图/相机顶栏背景深色,白图标。
+
+### 5.4 输入框
+
+- label在输入框上方,非浮动label;label13/18,与框间6。
+- 输入框高52、白底、1px line、radius10,左右14。
+- 聚焦1.5px primary;错误1px error并在下方显示说明。
+- 登录字段允许以placeholder为主且leading22图标;禁止空框只靠浮动中文标签模仿设计。
+- 密码eye有效宽44;多行描述至少100高;不使用超粗默认outline。
+
+### 5.5 按钮与分段
+
+- Primary:高50,primary白字,radius10,无elevation;disabled用明确灰蓝,不能与可点击同色。
+- Secondary:白底1px primary线,primary字;danger:红色文字+图标,默认不整条红底。
+- Tabs/segments:高40,浅灰#EEF1F6轨道,padding3,radius8;选中primary实底白字,未选textSecondary;不显示选中check、不画外黑边、不做大胶囊。
+- 图中Details/Access是浅蓝选中风格:白/浅灰轨道,选中#DDEBFF+primary文字。不要和上传分段混用状态颜色。
+
+### 5.6 卡片与Sheet
+
+- 普通分组卡:白底radius16,内边距16,外分组间20;行高56–64,分隔从文字起始位置对齐。
+- Sheet:白底、顶部radius20,拖柄36×4,距顶10;左右20,标题18/24,底部SafeArea。
+- Sheet scrim黑色32%,不能灰得看不清背景层次;顶部内容不浮在遮罩之外。
+- 单选行高60,radio20,row点击生效;不使用Material默认过大的行高。
+- 内页双按钮条:padding(16,12,16,12),gap12,两等宽,高50,白底;系统inset另算。
+
+### 5.7 图片封面卡
+
+- 宽=可用宽,390时左右20得350;高=width×9/16≈197。
+- 圆角12而非巨圆角,间距16。
+- scrim:顶部透明,0.42开始变暗,底部黑70%;不把整张图覆盖成灰色。
+- 标题18/24 semibold白,左16、底部状态行上方约42;meta12/18,status行12/16。
+- Public左、Uploads on右,中间Spacer;**无黑色独立胶囊背景,无绿色勾徽章**,只白色图标与文字。
+- 默认单行短日期,使用“Oct 15–19 · Guangzhou”一类真实格式,完整时区/时间放详情;禁止列表用完整起止时间和“未定”撑满。
+- 正常态必须有摄影封面,缺图态单独验收。
+
+## 6. 逐屏布局:390×844基准
+
+所有坐标以第3节画布计。表中“高”指基准英文数据;长文字允许重排,不能硬塞进固定高度。固定按钮区域使用真实SafeArea。
+
+### 01 登录(图01第一屏)
+
+| 区域 | x / y / w / h |
+|---|---|
+| 白色上部语言区 | 0 / 24 / 390 / 62 |
+| 摄影六图拼贴 | 0 / 86 / 390 / 254 |
+| 品牌居中 | 65 / 368 / 260 / 32 |
+| 主标题居中 | 20 / 424 / 350 / 34 |
+| 副标题居中 | 20 / 466 / 350 / 22 |
+| 账号框 | 20 / 538 / 350 / 52 |
+| 密码框 | 20 / 604 / 350 / 52 |
+| Sign in | 20 / 678 / 350 / 50 |
+| 管理员说明 | 20 / 758 / 350 / 36 |
+
+拼贴3列×2行,缝4,图片cover,禁止渐变矩形替代。语言在右上,正文、品牌、标题居中;账号/锁leading图标。短屏和键盘时图片缩小或滚出,表单可达;不把整个390版压扁。现有loginAskAdmin、用户名等走l10n,品牌英文固定不随UI翻译成“现场影像”。
+
+### 02 设置(图01第二屏)
+
+- 根标题x20 y48,24/30。
+- 账号卡x20 y104 w350 h112:头像56,左16;文字x100;姓名17/24,账号13/18,角色可次行。
+- 选项组x20 y240 w350:4行各60;leading22,左右16;分隔仅文字区。
+- 退出卡y504 h64,红色图标与文字,其余白色。
+- 根导航按统一规格。不要把所有设置都做成散落的ListTile,没有分组表面。
+- 账号卡无实际编辑能力不画箭头;图中的人物只用于固定fixture。
+
+### 03 修改密码(图01第三屏)
+
+内页顶栏后y104安全提示卡h76;y204起三个字段组(label18+gap6+框52),组间20;主按钮距最后框24;下方提示组距32。无第二个页面大标题重复AppBar。
+说明只是建议,不创建新的密码强制政策;键盘时整页滚动。
+
+### 04 语言Sheet(图01第四屏)
+
+参考顶边y440、底部844(含safearea),标题区域高64;三行60,底部Done高50,上下留16。白底、radio左、语言名两行说明在有意义时出现。临时选择,Done提交;关闭不提交。高度随文字增大可扩展至85%屏,不能截断选项。
+
+### 05 相册列表(图02第一屏)
+
+- 顶部PhotoLive x20 y42,右加号36圆形但有效44。
+- Albums x20 y104,说明y139;首卡y184,h197;第二卡y397;第三卡y610部分进入首屏,继续滚动。
+- 三张封面色彩/构图接近设计板,短白字叠层。正常列表不要用巨大灰色占位。
+- 根导航y752覆盖区前有列表底部padding,让末卡可完整滚出。
+- 中文标题同层级,不把PhotoLive隐藏成只有“相册”。
+
+### 06 相册详情(图02第二屏)
+
+- 顶栏y24–80仅返回与更多。
+- 封面x0 y80 w390 h165,照片edge-to-edge。
+- 信息x16 y260:标题22/28;日期行y299、地点行y325,分开,不把全部信息拼成三行长字符串。
+- 两个状态单元y357 h32,浅蓝底、普通蓝灰图标;Uploads on不是绿色success大徽章。
+- 四工具单元x16 y407,单元约80×64,gap12;每个独立极浅底radius10,icon22+label11,**无包裹它们的第二层厚白大卡**。
+- Recent photos标题y493,grid首行y525;至少一整行照片在首屏可见。
+- 底部动作固定在可用区域末72,正文让位。
+- 不重复AppBar中的活动标题,不把封面做成接近半屏高。
+
+### 07 操作Sheet(图02第三屏)
+
+复用详情作为背景,Sheet顶边约y450。标题18,4行52,行图标22、字15;Cancel单独轻灰行50。只显示有权限项。不要顶部多一个巨大的默认菜单标题条。
+
+### 08 空相册(图02第四屏)
+
+保留详情封面/活动信息至y390;空态插画约112×88居中y486;标题17/24 y605、说明13/20 y640;底部动作同详情。空态不显示“Recent photos + 空列表”两套重复说明。无上传权限则移除动作,空态说明相应调整。
+
+### 09 新建(图03第一屏)
+
+- 居中内页标题;body左右16,顶部20。
+- 基础白色卡x16 y100 w358;标题输入、日期两行、时区、地点分行。卡内部padding16、行高52–56。
+- 描述语言卡距20:标题17,默认语言选择框52,描述区至少100。
+- Create album固定底栏;表单底部预留空间。
+- 所有标签在框上方或有对齐的行label,不把全部字段变成无分组的默认TextFormField。
+- 真实必填字段完整,设计板首屏没展示的字段允许向下滚动。
+
+### 10 编辑Access(图03第二屏)
+
+Tabs x16 y104 w358 h40,距下卡20。开关卡内4项,每项标题15/20与说明13/18,右开关44×26;行高最低86,说明长允许扩高。iOS样式细长开关或自定义视觉,不直接用明显不同的大Material轨道。
+Content language单独卡距20 h84;Save固定底栏。
+Details共用新建表单,保持草稿。禁止为了还原图修改服务端开关语义。
+
+### 11 成员(图03第三屏)
+
+顶栏返回、标题、加号。Owner分组标题y108,卡y144高88。Collaborators标题y256,白卡y292,两项各88。头像44、名字15 semibold、账号12、权限说明12;文字不溢出右侧overflow按钮。
+说明框用softBlue和小info,padding12。Invite colleague为底部outlined高50。
+内部账号搜索面板复用成员能力;图上邮箱不是必须字段;真实缺省显示account。禁止借口“无头像”把所有行改成没有头像占位的稀疏文本。
+
+### 12 分类与编辑Sheet(图03第四屏)
+
+列表左右16,行高92,图标/可用缩略图56×56;分类名15/20,编辑22、更多22。没有封面用柔和图标,属于允许偏差。
+新增Sheet顶边约y452,标题+关闭、语言tabs、label、52高输入、底部Save。语言tabs浅蓝选中,不能采用上传实蓝样式。
+列表和面板CRUD复用现有实现,不新增照片封面数据表。
+
+### 13 相机(图04第一屏)
+
+顶部黑色区域y24–100:返回左、相册/分类居中、真实上传状态右。取景舞台y100–650,按真实相机比例contain或必要的预览适配,禁止人物拉伸。底部黑色控制区:缩略图48,快门76(外圈2、内实白)、切换摄像头44。
+“Added to upload queue”半透明反馈在取景下边缘上方,不覆盖快门。
+无Square/Pano;相机无资源时只可用注入fixture验证布局,真机预览另列未验收,不造实机截图。
+
+### 14 上传中心(图04第二屏)
+
+- 顶部Uploads x20 y48,History右,不重复品牌占据标题。
+- Today · This device x20 y108,13/18。
+- 82 uploaded x20 y142,32/40;数字和标签同一大字号。
+- 副行x20 y192,13/18,中性次文字,不把所有状态染成一串彩色数字。
+- 分段x16 y238 w358 h40;其他相册/日期筛选放History旁图标/底层面板,不能挤掉三个分段。
+- 第一任务卡y300,左右16,白底radius12,minHeight124,内padding12;下一卡gap12。
+- 缩略图76×88,右文字区域从x116起;文件名15/20粗,相册/分类12/17次色,状态在下方。
+- uploading:进度条高4,状态文字12;uploaded绿色图标+文字,无巨绿胶囊;confirming小spinner。
+- **摘要没有包裹白色大卡;筛选无黑色描边、无紫色选中背景。**
+- 正常截图至少4种任务状态,不用唯一完成记录+大面积空白证明还原。
+- 相册名必须显示标题;无匹配名称时本地化“相册信息暂不可用”,不要显示长UUID作为正常副标题。
+
+### 15 Needs attention(图04第三屏)
+
+同上传摘要和分段;下面两张异常卡,至少152高。缩略图76、文件名、相册、红色错误句;错误解释13/18;Retry或Select again在文字列下方,高40但触控44。底部Retry eligible tasks高50,独立于根导航。
+不改变原失败分类,不新增UI控制的假重试。摘要仍明确“Today · This device”,不会错误声称为异常筛选结果总数。
+
+### 16 历史(图04第四屏)
+
+内页标题、subtitle y104、相册/日期两个筛选y140高40;首记录y206。白/页面背景平铺,无每行重阴影;缩略图80×88,行高124,右侧文件名、状态、时间、设备来源分行,细分隔。
+状态由服务端真实字段决定,原接口没提供的缩略图或文件名不能硬造。生产回退与视觉fixture正常态分别记录。
+
+### 17 照片网格(图05第一屏)
+
+内页顶栏左返回,中间/左标题Photos与小相册名;分段y104高40,网格y164。左右8,列缝4,3列每格约122×150(网格可固定裁切,**管理页不是观众瀑布流**)。优先保持均匀行高。
+角标只显示Processing/Hidden,半透明深底,图标12+文字11,靠左下,padding6。
+无根底部导航;列表最后一行可完整滚动,数据仍走管理预览和权限。
+
+### 18 管理大图(图05第二屏)
+
+顶栏深色y24–80;图片舞台y104–604,完整contain;左右控制44半透明黑圆。底部信息x20 y636,上传者15粗,时间13,尺寸13;状态badge靠右,同一行。
+分隔y722,动作条y742–796:Hide photo为深色1px灰线按钮w190,Delete右侧红字图标,无整条鲜红按钮。短屏根据剩余高度缩小舞台,不能遮住动作。
+不添加观众点赞工具栏。保持现有手势、分页、签名刷新、返回位置。
+
+### 19 分享(图05第三屏)
+
+顶栏Share album;分享白卡x16 y104 w358,边线1px、radius16、内padding16。封面w326 h172,radius10;标题18/24,地点日期13分两行。
+QR区域约196×196,居中浅灰底,真实二维码周围留足quiet zone;下方说明12居中。Copy link高50,与QR说明间20。公开说明位于卡内底部。
+不把裸链接+一个大二维码直接铺满整个屏幕;原有浏览器打开入口可放更多或次级文本,不抢主按钮视觉。未公开时修改说明,不伪造可访问。
+
+### 20 隐藏确认(图05第四屏)
+
+管理大图作为背景,黑32%遮罩;Sheet y≈520,顶拖柄,标题居中18/24,缩略图64×80左、说明13/20右;primary50与cancel50各占整行,间12。
+说明按权限:“Authorized album managers can publish it again.”,不要向无恢复权限者承诺本人可恢复。
+photoId打开时捕获,切图/重复点击不作用到别图;真实请求错误保持可理解反馈。
+
+## 7. 素材/数据/视口固定的视觉fixture
+
+建立专用 `test/visual/` 与清晰命名的debug-only设计预览入口,复用**真实生产页面和组件**并注入数据,不复制一套漂亮但不被生产使用的静态页面。
+
+固定样本:
+- 账号Alex Morgan,头像/首字母,英语。
+- 三相册Canton Fair · Autumn / Product Launch / Team Stories;首相册Oct15–19,2026,Guangzhou。
+- 至少12张摄影图片与正确宽高;封面非null;一张Hidden、一张Processing。
+- 上传范围Today · This device:82完成、3传输中、13等待、2需处理。必须通过可控fixture/provider表达,不写死生产数字。
+- 两名协作者;三分类;两条失败任务;5条服务端历史。
+- 分享二维码使用测试相册URL真实生成。
+- 配套空/断网/不可公开/无预览权限fixture,不能与正常态混为一张。
+
+debug入口不能携带真实账号密码、不触生产写操作;正式构建不可进入fixture模式。相机测试预览必须标识为fixture,不能作为原生相机验收证据。
+
+## 8. 精确的截图对照流程(每页都执行)
+
+1. 打开对应原设计板,定位第几屏。可用图像裁切工具制作参考屏裁片;裁切只用于比较,不用来代替运行界面。保留原图,记录裁框。
+2. 将参考屏裁片等比例统一宽度至390(不做非等比拉伸);由于生成图屏幕比例不一致,系统栏/画板边界排除。坐标冲突以本文第6节工程基准为准。
+3. 渲染固定390×844 fixture截图,文件名明确screenId/lang/theme/scale/dataVersion。
+4. 输出参考/实际左右对照图、50%透明叠加图;对系统栏、不同照片内容、日期文字等合理差异设置明确mask并记录。
+5. 测量关键框:标题、首卡、首图、分段、底栏;出一行目标/实测/差值表。
+6. 按差异修复 → 重拍。不得在没查看真实截图时自行写“视觉通过”。
+7. 相同页面的生产服务冒烟截图另存,证明不是仅fixture页面好看。
+
+不能用SSIM/整图像素差单一阈值判断通过:照片内容和字体抗锯齿会干扰。可计算结构区差异辅助,但最终必须逐页检查。
+
+### 8.1 可量化通过标准
+
+参考画布下:
+- 主要容器x/y/w/h相对第6节目标偏差≤4dp;涉及自然文本高度累计偏差≤8dp且记录原因。
+- 明确token的字号偏差≤1sp,字重一致(400/500/600/700);行高偏差≤2dp。
+- 圆角偏差≤2dp,常规间距偏差≤2dp。
+- 首屏预期出现的区域必须出现:详情首行照片、上传至少主要任务行、登录主按钮等;不能整体下推到屏外。
+- 色值以token为准,不接受主题种子自动派生为另一色相。
+- 不允许残留占位摄影拼贴、根导航胶囊、默认黑框分段、正常态ID裸露、重复活动标题。
+- 标准英文fixture关键标题换行与规范一致;中文及大字体按可用性允许重排。
+- 触控、语义、长文案通过独立检查,不能为坐标牺牲可访问性。
+
+超出阈值必须写出具体数值及业务/平台原因,并标记待修或接受的明确偏差;不能以“效果接近”一笔带过。
+
+## 9. 逐任务实施计划
+
+### R00 复核基线
+- [ ] Git状态、最新UI03代码/验收、五图全部读取。
+- [ ] 创建 docs/plans/progress/UI03R-验收记录.md。
+- [ ] 建立20屏“参考/当前/偏差/目标”表。
+- [ ] 确认逻辑尺寸、字体、图片来源、可用截图工具。
+出口:不能沿用旧UI03全通过结论。
+
+### R01 素材和fixture
+- [ ] 生成资产清单、补足摄影素材及品牌一致性。
+- [ ] 固定英文数据、12张照片、正常和异常状态。
+- [ ] fixture注入真实组件;生产开关隔离。
+出口:正常态不是灰色占位,且数据不会污染生产。
+
+### R02 公共组件重构
+- [ ] theme/tokens显式定义,不依赖默认Material外观。
+- [ ] 无胶囊根导航、两种segment、输入、按钮、Sheet、封面卡。
+- [ ] 给公共组件截图和语义检查。
+出口:默认风格造成的偏差已消除。
+
+### R03 三个样板页
+- [ ] 完成01登录、05相册列表、14上传中心。
+- [ ] 三页必须有390英文正常态、对照图和几何差值表。
+- [ ] 达到第8节标准再推广,不等待用户确认常规像素调整。
+出口:缺素材或仍用默认样式时不能进入“样板已通过”。
+
+### R04 账号与工作台
+- [ ] 页面02/03/04/06/07/08。
+- [ ] 逐页对照,特别检查详情首屏照片和工具区高度。
+出口:8页完整证据。
+
+### R05 表单与协作
+- [ ] 页面09/10/11/12,邀请与编辑配套弹层。
+- [ ] 表单草稿、权限、双语内容不回归。
+出口:分组/字号/开关对齐,不是默认表单集合。
+
+### R06 拍摄与上传
+- [ ] 页面13/15/16,与14视觉一致。
+- [ ] 拍摄fixture与实际设备验证分开,记录真实资源。
+- [ ] 行级进度、恢复重试、账号隔离回归。
+出口:明确本地队列/服务端历史区别。
+
+### R07 照片与分享
+- [ ] 页面17/18/19/20。
+- [ ] 大图舞台、资料、底部动作、QR卡和确认Sheet。
+- [ ] 照片权限、签名过期、切图及返回位置回归。
+出口:管理摄影界面与图05结构一致。
+
+### R08 全量验证
+- [ ] 20页英文正常态全部截图,不以widget测试替代缺图。
+- [ ] 中文、320短屏、430、平板、横屏、大字体、深色抽查。
+- [ ] 功能和视觉验收分开记录,失败项回到对应任务。
+出口:证据齐全,不能以“留待G2”跳过可模拟的文字/布局检查。
+
+### R09 交接
+- [ ] 资产清单、截图索引、差异表、测试结果、外部关口整理。
+- [ ] 总纲标记UI03R真实状态,不覆盖原UI03历史。
+- [ ] 如有未完成的摄影/字体/截图/真机项,最终明确标为部分完成。
+出口:用户可打开每个页面对照,而非只能相信测试数字。
+
+## 10. 回归、响应式与命令
+
+### 10.1 视口矩阵
+
+- 主基准390×844 / en / light /1.0:20项全量。
+- 320×568:登录键盘、详情、编辑Access、上传、分享、Sheet。
+- 430×932:列表、照片网格、大图。
+- 844×390横屏:拍摄布局、大图和分享可达。
+- 768平板:列表/表单/成员;内容maxWidth按版式合理限制,不能把字段拉成全屏一条。
+- textScaler1.3、2.0:设置、表单、上传错误、隐藏确认。
+- zh-CN:20页运行检查,至少8个主要页截图,文件名和画面语言一致。
+- dark:设置、表单、上传、列表;摄影页维持既定深色。
+
+### 10.2 功能保护
+
+账号登录/密码/退出,语言持久化,表单未保存,成员/分类权限,上传关闭,公开关闭,拍摄入队,等待网络/重试,源文件丢失,凭证续签,队列清理,照片预览权限和下线,分享复制失败都要按既有逻辑回归。
+不修改native_transport/upload_engine原生可靠性语义;如样式工程确实触及控制逻辑,必须补针对性测试并登记。
+
+### 10.3 执行入口
+
+在apps/mobile中使用仓库固定Flutter:
+
+```text
+flutter gen-l10n
+flutter analyze
+flutter test
+```
+
+另行添加可重复运行的visual fixture测试命令/截图脚本,并在README写明。检查工具是否重置数据,不对生产运行。不能自动更新golden来“让测试变绿”:先查看差异、修好界面,再建立新基线。
+
+先跑定向后跑全量;只改App样式不必用无关Web构建缓存结果充当主要验收证据。
+
+## 11. 双轨验收与交付目录
+
+### 11.1 每页记录模板
+
+| 字段 | 必填内容 |
+|---|---|
+| 页面ID/名称 | 01 Login等 |
+| 代码入口 | 实际页面和公共组件 |
+| 参考屏 | 板号/屏号/裁片路径 |
+| 正常态截图 | 390/en/light路径 |
+| 当前数据 | fixture版本、素材清单版本 |
+| 结构测量 | 目标/实测/差值,至少4个关键框 |
+| 视觉差异 | 已修/剩余及原因 |
+| 功能结果 | 通过/失败/未执行 |
+| 视觉结果 | 通过/失败/未完成 |
+| 可访问性 | 触控、语义、字体缩放 |
+| 外部限制 | 真机/素材/字体等,具体影响 |
+
+“功能通过、视觉未完成”必须允许存在;不能合成一个✅遮盖差异。
+
+### 11.2 目录
+
+```text
+docs/designs/ui03r-assets-manifest.md
+docs/designs/ui03r-implementation/
+ references/ 设计板各屏参考裁片及裁框记录
+ actual/en-light/ 20个固定画布截图
+ actual/zh-light/
+ actual/responsive/
+ device/ 模拟器/真机证据,记录平台参数
+ comparisons/ 左右对照、透明叠加
+ geometry/ 每页测量结果
+ README.md 页面链接和复现命令
+docs/plans/progress/UI03R-验收记录.md
+```
+
+### 11.3 完成门槛
+
+- 五张图对应20项全部有正常态固定画布截图和视觉结论。
+- 三个样板页满足尺寸/字体/颜色/素材要求。
+- 无本方案列明的默认样式偏差残留。
+- 真实运行路径复用同一套组件,不是只做设计预览页。
+- 功能回归通过,未执行外部关口如实标注。
+- 存在主视觉素材缺失或关键页面未截图时,本阶段只能标记“部分完成”。
+- 严禁在没有看图、没有测量的情况下以源码注释或测试数量宣布视觉100%还原。
+
+## 12. 可直接交给 Claude 的执行指令
+
+> 请执行 docs/plans/2026-09-22-UI03R-App高保真还原完整实施方案.md。这次是纠正UI03与效果图差异,不是自由发挥或再次仅做功能。先查看五张原设计板和旧实际截图,准备真实摄影素材与固定390×844英文fixture,显式覆盖Material默认导航/分段/输入/按钮样式。首先完成01登录、05相册列表、14上传中心三页,输出同尺寸对照和关键框测量,达到文档标准后再完成其余17项。每页都要截图、查看、修正,功能和视觉分开验收。不得用渐变占位代替摄影拼贴、不得保留导航胶囊和默认黑框分段、不得用测试通过代替视觉还原。保护当前业务与未提交改动,保留权限和上传引擎,不修改Web与嵌入组件。最终交付真实可运行App、完整20页证据、素材清单、UI03R验收记录及剩余关口;未完成视觉项不得标完成。未经另行授权不推送、不部署、不操作云资源。
+