Files
photo-live/packages/embed-loader/README-embed.md
T
浅念 3bba8d1dc5 feat(admin,viewer,ui04): E06 management preview and v2 integration — real-component preview page, form mode fields, README
- viewer /embed-preview route: real EmbedStream with sample photography (picsum seeds), zero requests/WS (preview mode), postMessage config + simulated bursts through the real validation pipeline
- EmbedStream: preview prop (disables real WS subscription per §8.2 isolation) + exposed simulateBurst
- admin form: displayMode radio (new default), fixed 195x280 readonly, speed slider, reactions switch; v1 layout fields hidden in v2 (not silently effective)
- admin preview: v2 branch embeds the real preview iframe, white v2 handle, simulate-like tool button with sample-data note
- README-embed: v2 section (integration, hard sizing/docking, interactions/limits, artifact/version caching)
- tests: admin vitest 83 (+1 v2 placement), admin e2e 5 (v1 pinned carousel regression + full v2 chain incl. preview WYSIWYG heart and ov settings projection), viewer 244, v2 e2e 16

Note: part of the admin-side E06 changes (form/preview/utils/locales, mid-state of e2e spec) was swept into parallel commit eebf3b3 by a concurrent session's git add; this commit carries the final state of the remaining files and completes the set.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-22 18:20:50 +08:00

167 lines
9.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PhotoLive 悬浮组件接入说明(embed-loader v1)
宿主页只需引入一个版本化脚本文件即可嵌入照片悬浮轮播组件。加载器零依赖、零全局污染
(仅 `window.__photoliveEmbedV1` 版本标记)、出错静默不破坏宿主。
## 接入代码
```html
<script
src="https://{EMBED_ORIGIN}/embed/v1/loader.js"
data-slot="{展示位 token(后台展示位管理页复制)}"
defer
></script>
```
- `data-slot`(必填):展示位 publicToken;缺失时加载器静默不渲染任何内容。
- `data-lang`(可选):`auto`(缺省,按相册 defaultLocale)| `zh-CN` | `en`,非法值回退 `auto`。
- `data-position`(可选):`right`(缺省)| `left`,悬浮侧。
- `data-interval`(可选):轮播切换间隔毫秒,夹取到 3000–30000,缺省 5000。
仅影响轮播切换,不影响取数轮询(恒 30s 契约基准)。
- `data-image-fit`(可选,UI-01):`contain`(缺省,4:3 舞台完整显示)| `cover`
(填满裁切)。
- `data-offset-x`(可选,UI-01):距宿主边缘水平偏移 px,0–120,缺省 16。
- `data-offset-bottom`(可选,UI-01):距宿主底部垂直偏移 px,0–240,缺省 24。
- `data-z-index`(可选,UI-01):wrapper 层级,1–2147483000,缺省 1000。宿主页面
自身存在高层级元素(如全屏遮罩)时按需调高;把手与卡片共用该层级。
- `data-poll`(可选,仅测试):加速取数轮询毫秒数,经 iframe URL `&poll=` 透传,
**仅 dev 构建生效**(生产构建忽略)。e2e 用;生产接入不需要。
同一页面引入多个不同 `data-slot` 的标签即嵌入多个展示位;同一 `data-slot` 重复出现时
后者静默忽略。
### 配置优先级(UI-01 §5.4)
任一字段:**宿主 data-\* 显式提供 > 后台展示位设置 > 契约默认值**。未提供的
data-\* 不视为覆盖,后台设置变更经 iframe 轮询发现后以 `config` 消息动态应用
(loader 二次校验:仅白名单字段 + 版本单调递增,不信任任意 CSS/HTML)。
## 多槽与响应式(UI-01)
- **同侧多槽纵排**:同一侧(left/right)多个展示位按引入顺序自下而上堆叠,
间距 8px;视口高度不足时后引入的槽自动收起为把手(不写用户偏好——空间约束
非用户选择)。
- **窗口变化**:`resize` / `orientationchange` 重新评估全部几何;宽度取「配置宽
与视口剩余宽」的较小值(极窄视口下限 180px 防溢出)。
- **窄屏**(viewport <480px):强制小尺寸(280px 宽)且默认收起为把手。
- **收起把手**:图片图标 + 「照片 / Photos」胶囊按钮,高 44px、有效点击区
≥44×44、可键盘操作(Enter/Space)。收起状态会话记忆在 `sessionStorage`
(键 `pl-embed-collapsed:{slot}`),刷新后保持,不反复弹出。
## CSP 要求(宿主页)
| 指令 | 值 | 说明 |
| --- | --- | --- |
| `script-src` | `{EMBED_ORIGIN}` | 加载 loader.js |
| `frame-src` | `{EMBED_ORIGIN}` | 悬浮轮播 iframe(`{EMBED_ORIGIN}/embed/{slot}`) |
组件自身不内联脚本;iframe 内图片来自对象存储签名域名(`img-src` 无需为组件放开
项目域)。`frame-ancestors` 由**我们**的 iframe 页配置(限制可嵌入站点),不是宿主
的关注点。
## 生命周期消息(iframe → 宿主)
| type | 触发 | loader 行为 |
| --- | --- | --- |
| `resize` | iframe 内容高度变化 | wrapper 高度夹取 60–640px 应用 |
| `collapse` | 用户点击 iframe 内 × | 收起为把手 + 会话记忆 |
| `expand` | (保留) | — |
| `config` | 后台设置变更(configVersion 升值) | 校验后动态应用非 data-\* 显式字段 |
| `destroy` | 槽 404/410/停用/解绑(服务端终止展示) | **移除 wrapper + 把手 + 会话键**,不留无效按钮(与用户收起语义不同) |
宿主 → iframe 仅 `expand`(把手点击,目标源严格为 iframe 源)。
## 安全模型
- **iframe sandbox**:`allow-scripts allow-popups allow-popups-to-escape-sandbox
allow-same-origin`。必须含 `allow-same-origin`:否则 iframe 源为 opaque `null`,
下方 origin 白名单会拒掉自己 iframe 的合法消息,且 iframe 内调公开 API 需要全站
ACAO `*`。iframe 对宿主天然跨源(宿主域 ≠ 项目域),`allow-same-origin` 仅使
iframe 保有**项目域自己的**源,不会获得宿主页源。
- **postMessage 双向白名单**:loader 仅接受「`event.source` 等于已登记 iframe 的
`contentWindow` 且 `event.origin` 严格等于该 iframe src 源」的消息,且载荷须为
`{photoliveEmbed: true, type: resize|collapse|expand|config|destroy, ...}`;
resize 高度夹取 60–640px,config 经版本闸 + 字段白名单校验。向 iframe 发消息
(把手展开)同样以 iframe 源为目标,绝不使用 `*`。
- **零信任宿主数据**:slot token 经 encodeURIComponent 进 URL;
lang/position/interval/imageFit/offset/zIndex 白名单或夹取校验后才拼入查询串。
- **存储与 Cookie**:loader 不读写 Cookie、不写宿主 localStorage;仅上述
sessionStorage 收起标记(destroy 时一并清除)。无埋点、无对外数据上报。
- **DOM 足迹与样式免疫**:仅向 body 追加 wrapper 与收起把手两个 div(全部 inline
style,不注入样式表/类名,不影响宿主 CSS,也不受宿主全局样式影响——敌意 reset
下组件仍保持 fixed 定位与几何)。
## 产物与发布
- 发布物即文件本身(不走 npm):`packages/embed-loader/dist/v1/loader.js`
(esbuild bundle + minify,IIFE,target es2018)。
- **viewer 构建携带**:`pnpm --filter @photo/embed-loader run sync` 将产物拷贝至
`apps/viewer/public/embed/v1/loader.js`(sha256 校验)。viewer 的 vite publicDir
使 dev server 直接可访问、`vite build` 自动将其带入 dist/。两份产物均提交入库,
修改 loader 后需重新 build + sync。
- 路径 `/embed/v1/` 长期稳定;破坏性变更才会出现 `/embed/v2/`。
## 开发
```bash
pnpm --filter @photo/embed-loader run build # 打包 dist/v1/loader.js
pnpm --filter @photo/embed-loader run test # params 纯函数单测(vitest)
pnpm --filter @photo/embed-loader run sync # 拷贝至 viewer public/(含 hash 校验)
```
宿主页交互行为(iframe 出现/收起/记忆/窄屏/多槽堆叠/跨源宿主/postMessage 白名单/
后台设置动态生效)的端到端覆盖见 viewer Playwright `e2e/embed.spec.ts` 与
`e2e/embed-crossorigin.spec.ts`(宿主 fixture 页 / 3999 端口跨源静态页)。
---
# UI-04 v2:垂直照片流窗(/embed/v2/loader.js)
新版展示模式:**PC/手机均固定 195×280 CSS px、单列照片连续自动上滚、真实点赞
心形气泡、无标题栏与底部工具栏**(仅右上收起入口;点击照片打开观众深链)。
v1(旧轮播)源码冻结保留——存量接入不受影响;切换新版需更新 script 地址。
## 接入代码
```html
<script src="{EMBED_ORIGIN}/embed/v2/loader.js" data-slot="{publicToken}" defer></script>
```
- 后台展示位 `displayMode=vertical-stream` 时,嵌入代码卡自动生成 v2 入口;
- `data-speed`(8–32,像素/秒,缺省 16)与 `data-reactions="off"` 为显式覆盖,
缺省让后台设置生效(优先级同 v1:data-* > 后台 > 默认);
- v2 下 `data-size / data-interval / data-image-fit` 无布局作用(固定 195×280、
连续滚动),不生成也不解析。
## 尺寸与停靠(硬性)
- 宽 195 / 高 280(border-box,含 1px 白边);视口不足时按 `min(195, 可用宽)` /
`min(280, 可用高)` 收缩,低于可用下限转收起入口;**不整体缩放**(transform
scale 会同缩文字与触控区);
- 默认停靠:桌面 right 16 / 手机(≤599px 视口)right 12,bottom 24 + 安全区
(`offsetX/offsetBottom` 可配置,0–120 / 0–240);z-index 缺省 1000 可配置;
- 窄屏(<480px)默认收起(与 v1 策略一致);展开后两端尺寸一致;
- iframe 内容不得经 resize 消息反向改变窗口高度(v2 忽略 resize)。
## 交互与限制
- 悬停(鼠标)/键盘焦点/按住暂停,离开 500ms 恢复;Space 显式暂停(不自动
恢复);Escape 收起;↑↓/滚轮/拖动手动滚动(双向回绕,跟手 1:1);
- 点击照片:pointerdown 锁定目标,拖动 >8px 取消——动画中不点错图;新标签
打开 `/a/{albumToken}/p/{photoId}`(noopener);
- 点赞气泡只由真实 `likes.burst`(WebSocket 只读订阅相册公开 token)驱动:
断线不出泡、不伪造;组件自身不提交点赞、不上报浏览;
- 减少动态效果(prefers-reduced-motion):停自动滚动与粒子,手动可查看。
## 产物与版本
- 构建产物 `dist/v1/loader.js` 与 `dist/v2/loader.js`(同 build/sync 脚本;
viewer publicDir `public/embed/{v1,v2}/` 携带)。版本入口稳定可长缓存
(immutable 语义同 `/embed/v1/`,见 viewer nginx 模板);升级走新版本路径,
不原地变更已发布文件语义。
v2 行为的端到端覆盖见 viewer Playwright `e2e/embed-v2.spec.ts`(E01–E05 出口
门/尺寸测量/滚动连续性/交互/真实点赞气泡/录屏证据);管理端表单与真实组件
预览(`/embed-preview`,样例数据 + postMessage 配置/模拟事件)见 admin
`e2e/embed-slots.spec.ts` UI-04 用例。