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

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-21 18:55:15 +08:00
parent 1f67b7c818
commit ddf26a177f
22 changed files with 2274 additions and 326 deletions
+55 -18
View File
@@ -16,10 +16,37 @@
- `data-slot`(必填):展示位 publicToken;缺失时加载器静默不渲染任何内容。
- `data-lang`(可选):`auto`(缺省,按相册 defaultLocale)| `zh-CN` | `en`,非法值回退 `auto`。
- `data-position`(可选):`right`(缺省)| `left`,悬浮侧。
- `data-interval`(可选):轮播间隔毫秒,夹取到 3000–30000,缺省 5000。
- `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` 重复出现时
后者静默忽略。位置/尺寸/语言等展示位级设置以后台配置为准,data-* 为宿主侧覆盖。
后者静默忽略。
### 配置优先级(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 要求(宿主页)
@@ -28,14 +55,21 @@
| `script-src` | `{EMBED_ORIGIN}` | 加载 loader.js |
| `frame-src` | `{EMBED_ORIGIN}` | 悬浮轮播 iframe(`{EMBED_ORIGIN}/embed/{slot}`) |
组件自身不内联脚本、不加载其他域资源。`frame-ancestors` 由**我们**的 iframe 页在 P5
配置(限制可嵌入站点),不是宿主的关注点。
组件自身不内联脚本;iframe 内图片来自对象存储签名域名(`img-src` 无需为组件放开
项目域)。`frame-ancestors` 由**我们**的 iframe 页配置(限制可嵌入站点),不是宿主
的关注点。
## 窄屏行为
## 生命周期消息(iframe → 宿主)
viewport 宽度 <480px 时:组件强制降为小尺寸(280px 宽)并默认收起为侧边圆形把手,
点击把手展开。会话内收起状态记忆在 `sessionStorage`(键 `pl-embed-collapsed:{slot}`),
刷新后保持收起,不反复弹出。
| type | 触发 | loader 行为 |
| --- | --- | --- |
| `resize` | iframe 内容高度变化 | wrapper 高度夹取 60–640px 应用 |
| `collapse` | 用户点击 iframe 内 × | 收起为把手 + 会话记忆 |
| `expand` | (保留) | — |
| `config` | 后台设置变更(configVersion 升值) | 校验后动态应用非 data-\* 显式字段 |
| `destroy` | 槽 404/410/停用/解绑(服务端终止展示) | **移除 wrapper + 把手 + 会话键**,不留无效按钮(与用户收起语义不同) |
宿主 → iframe 仅 `expand`(把手点击,目标源严格为 iframe 源)。
## 安全模型
@@ -46,19 +80,21 @@ viewport 宽度 <480px 时:组件强制降为小尺寸(280px 宽)并默认
iframe 保有**项目域自己的**源,不会获得宿主页源。
- **postMessage 双向白名单**:loader 仅接受「`event.source` 等于已登记 iframe 的
`contentWindow` 且 `event.origin` 严格等于该 iframe src 源」的消息,且载荷须为
`{photoliveEmbed: true, type: resize|collapse|expand, ...}`;resize 高度夹取
60–640px。向 iframe 发消息(把手展开)同样以 iframe 源为目标,绝不使用 `*`。
- **零信任宿主数据**:slot token 经 encodeURIComponent 进 URL;lang/position/interval
白名单校验后拼入查询串。
`{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 收起标记。无埋点、无对外数据上报。
- **DOM 足迹**:仅向 body 追加 wrapper 与收起把手两个 div(全部 inline style,
不注入样式表/类名,不影响宿主 CSS)。
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,≤5KB 目标)。
(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/。两份产物均提交入库,
@@ -73,5 +109,6 @@ pnpm --filter @photo/embed-loader run test # params 纯函数单测(vitest
pnpm --filter @photo/embed-loader run sync # 拷贝至 viewer public/(含 hash 校验)
```
宿主页交互行为(iframe 出现/收起/记忆/窄屏)的端到端覆盖见 viewer Playwright
`e2e/embed.spec.ts`(P4b Task 4,宿主 fixture 页)。
宿主页交互行为(iframe 出现/收起/记忆/窄屏/多槽堆叠/跨源宿主/postMessage 白名单/
后台设置动态生效)的端到端覆盖见 viewer Playwright `e2e/embed.spec.ts` 与
`e2e/embed-crossorigin.spec.ts`(宿主 fixture 页 / 3999 端口跨源静态页)。