feat(embed,ui04): E01 v2 contract skeleton — displayMode/scrollSpeed/reactionsEnabled, v2 loader entry and fixed 195x280 shell

- contracts: EmbedSlotSettings optional v2 fields (carousel default) + SlotSettingsPayload; gen + redocly green
- api: DTO validation + normalization full-set (cn manage + ov public projection share normalizeStoredSettings)
- embed-loader: params v2 parse/merge/url family, loader-v2.js fixed shell (border/radius/#10161E/safe-area, resize ignored, stack gap 12), dual-artifact build+sync; v1 source frozen
- viewer: EmbedPage v=2 branch, EmbedStream (viewport/track/real anchors/collapse/empty), stream.ts pure fns, i18n zh/en
- admin: types + utils (create default vertical-stream, clampScrollSpeed, v1/v2 snippet selection)
- tests: loader 36 (+9 v2), viewer 222, admin 82, api unit 51, v1 embed e2e 20, v2 E01 gate 2 (195x280 desktop+mobile cross-origin, no footer)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-22 12:50:28 +08:00
parent f8dbc3ab9b
commit 66d1eb3e9f
25 changed files with 1383 additions and 52 deletions
+24
View File
@@ -46,6 +46,11 @@ export const EMBED_OFFSET_BOTTOM_MAX = 240;
export const EMBED_Z_INDEX_MIN = 1;
export const EMBED_Z_INDEX_MAX = 2147483000;
/** UI-04 新增字段枚举与边界(vertical-stream 展示模式,全部可选)。 */
const EMBED_SLOT_DISPLAY_MODES = ['carousel', 'vertical-stream'] as const;
export const EMBED_SCROLL_SPEED_MIN = 8;
export const EMBED_SCROLL_SPEED_MAX = 32;
export class EmbedSlotSettingsDto {
@IsIn(EMBED_SLOT_POSITIONS)
position!: (typeof EMBED_SLOT_POSITIONS)[number];
@@ -91,6 +96,25 @@ export class EmbedSlotSettingsDto {
@Min(EMBED_Z_INDEX_MIN)
@Max(EMBED_Z_INDEX_MAX)
zIndex?: number;
// —— UI-04 可选扩展(vertical-stream 展示模式;旧 JSON 兼容同 UI-01 模式)——
/** 展示模式:carousel=旧轮播(缺省);vertical-stream=v2 垂直照片流。 */
@IsOptional()
@IsIn(EMBED_SLOT_DISPLAY_MODES)
displayMode?: (typeof EMBED_SLOT_DISPLAY_MODES)[number];
/** v2 自动上滚速度(CSS px/秒,8..32);缺省 16。 */
@IsOptional()
@IsInt()
@Min(EMBED_SCROLL_SPEED_MIN)
@Max(EMBED_SCROLL_SPEED_MAX)
scrollSpeed?: number;
/** v2 点赞心形气泡开关;缺省 true(false 仅隐藏气泡)。 */
@IsOptional()
@IsBoolean()
reactionsEnabled?: boolean;
}
export class EmbedSlotCreateRequestDto {
+15 -1
View File
@@ -52,7 +52,7 @@ export type EmbedSlotSettingsResponse =
const PUBLIC_TOKEN_LENGTH = 24;
const PUBLIC_TOKEN_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz';
/** 契约 EmbedSlotSettings 各字段缺省值(整体缺省时落库形态;UI-01 新字段同列)。 */
/** 契约 EmbedSlotSettings 各字段缺省值(整体缺省时落库形态;UI-01/UI-04 新字段同列)。 */
export const DEFAULT_EMBED_SLOT_SETTINGS: EmbedSlotSettingsResponse = {
position: 'right',
size: 'md',
@@ -63,6 +63,12 @@ export const DEFAULT_EMBED_SLOT_SETTINGS: EmbedSlotSettingsResponse = {
offsetX: 16,
offsetBottom: 24,
zIndex: 1000,
// UI-04:displayMode 缺省 carousel——UI-04 前旧记录读取归一为旧模式;
// 新建走 v2 由管理端表单显式提交 vertical-stream(契约默认与服务端缺省
// 一致保持保守,避免存量展示位一夜切换新布局)。
displayMode: 'carousel',
scrollSpeed: 16,
reactionsEnabled: true,
};
/**
@@ -109,6 +115,10 @@ function normalizeSettings(
offsetBottom:
settings.offsetBottom ?? DEFAULT_EMBED_SLOT_SETTINGS.offsetBottom,
zIndex: settings.zIndex ?? DEFAULT_EMBED_SLOT_SETTINGS.zIndex,
displayMode: settings.displayMode ?? DEFAULT_EMBED_SLOT_SETTINGS.displayMode,
scrollSpeed: settings.scrollSpeed ?? DEFAULT_EMBED_SLOT_SETTINGS.scrollSpeed,
reactionsEnabled:
settings.reactionsEnabled ?? DEFAULT_EMBED_SLOT_SETTINGS.reactionsEnabled,
};
}
@@ -132,6 +142,10 @@ export function normalizeStoredSettings(
offsetX: row.offsetX ?? DEFAULT_EMBED_SLOT_SETTINGS.offsetX,
offsetBottom: row.offsetBottom ?? DEFAULT_EMBED_SLOT_SETTINGS.offsetBottom,
zIndex: row.zIndex ?? DEFAULT_EMBED_SLOT_SETTINGS.zIndex,
displayMode: row.displayMode ?? DEFAULT_EMBED_SLOT_SETTINGS.displayMode,
scrollSpeed: row.scrollSpeed ?? DEFAULT_EMBED_SLOT_SETTINGS.scrollSpeed,
reactionsEnabled:
row.reactionsEnabled ?? DEFAULT_EMBED_SLOT_SETTINGS.reactionsEnabled,
};
}