diff --git a/apps/admin/.env.example b/apps/admin/.env.example index fa3f6be..cc36254 100644 --- a/apps/admin/.env.example +++ b/apps/admin/.env.example @@ -22,3 +22,8 @@ VITE_LOCK_ENCRYPT_KEY = change-me-lock-key # viewer 部署基址(相册详情「分享」卡拼 viewer URL 与二维码;缺省兜底 # http://127.0.0.1:5175 —— 本机 dev 端口,apps/viewer vite 固定 5175) VITE_VIEWER_BASE = http://127.0.0.1:5175 + +# 嵌入组件(悬浮轮播)部署基址:展示位「嵌入代码」卡拼 loader 引用 +# ' + ) + expect(buildEmbedSnippet('http://127.0.0.1:5175//', 'tok')).toBe( + '' + ) + }) + + it('环境基址:VITE_EMBED_ORIGIN 缺省/空白兜底本机 dev', () => { + expect(embedOriginFromEnv({})).toBe(EMBED_ORIGIN_FALLBACK) + expect(embedOriginFromEnv({ VITE_EMBED_ORIGIN: ' ' })).toBe(EMBED_ORIGIN_FALLBACK) + expect(embedOriginFromEnv({ VITE_EMBED_ORIGIN: ' https://cdn.example.com/ ' })).toBe( + 'https://cdn.example.com' + ) + }) +}) + +describe('bindChangeKind 绑定变更分类', () => { + it('null→id 绑定 / id→id 换绑 / id→null 解绑 / 值不变 none', () => { + expect(bindChangeKind(null, 'album-1')).toBe('bind') + expect(bindChangeKind('album-1', 'album-2')).toBe('rebind') + expect(bindChangeKind('album-1', null)).toBe('unbind') + expect(bindChangeKind('album-1', 'album-1')).toBe('none') + expect(bindChangeKind(null, null)).toBe('none') + }) +}) + +describe('绑定变更确认文案(A22:configVersion 将递增,官网无需改动)', () => { + const zh = zhMessages.embedSlotsManage as Record + const en = enMessages.embedSlotsManage as Record + const zhBind = zh.bind as Record + const enBind = en.bind as Record + + it('zh/en 键集合一致(embedSlotsManage 子树)', () => { + const keysOf = (obj: Record, prefix = ''): string[] => + Object.entries(obj).flatMap(([key, value]) => + typeof value === 'object' && value !== null + ? keysOf(value as Record, `${prefix}${key}.`) + : [`${prefix}${key}`] + ) + expect(keysOf(en)).toEqual(keysOf(zh)) + }) + + it('三类确认文案均携带 A22 短语(换绑不改官网:loader 只认 token)', () => { + for (const key of ['confirmBind', 'confirmRebind', 'confirmUnbind']) { + expect(zhBind[key]).toContain('configVersion 将递增') + expect(zhBind[key]).toContain('官网无需改动') + expect(enBind[key]).toContain('configVersion will increase') + expect(enBind[key]).toContain('no change is needed on the host site') + } + }) +}) diff --git a/apps/admin/src/utils/system/embedSlots.ts b/apps/admin/src/utils/system/embedSlots.ts new file mode 100644 index 0000000..0b3e110 --- /dev/null +++ b/apps/admin/src/utils/system/embedSlots.ts @@ -0,0 +1,94 @@ +/** + * 展示位管理页纯逻辑(无组件依赖,vitest 直接覆盖;P4b Task 5) + * + * - 嵌入代码:`` +} + +/** intervalMs 夹取到契约区间(非有限值回退缺省 5000;四舍五入整毫秒) */ +export function clampIntervalMs(value: number): number { + if (!Number.isFinite(value)) return DEFAULT_EMBED_SETTINGS.intervalMs + return Math.min(EMBED_INTERVAL_MAX_MS, Math.max(EMBED_INTERVAL_MIN_MS, Math.round(value))) +} + +/** 表单草稿 → 完整 settings 载荷(整体提交:字段齐全 + intervalMs 夹取) */ +export function buildSettingsPayload(draft: EmbedSettingsDraft): Api.EmbedSlot.Settings { + return { ...draft, intervalMs: clampIntervalMs(draft.intervalMs) } +} + +/** settings 是否有任一字段变化(编辑载荷只送变更面) */ +function settingsChanged(current: Api.EmbedSlot.Settings, next: EmbedSettingsDraft): boolean { + return ( + current.position !== next.position || + current.size !== next.size || + current.intervalMs !== clampIntervalMs(next.intervalMs) || + current.lang !== next.lang || + current.collapsedDefault !== next.collapsedDefault + ) +} + +/** 编辑提交载荷:name/settings 只送变更字段;settings 送则整体替换 */ +export function buildSlotEditPayload( + current: Api.EmbedSlot.Item, + name: string, + draft: EmbedSettingsDraft +): Api.EmbedSlot.UpdatePayload { + const payload: Api.EmbedSlot.UpdatePayload = {} + const trimmed = name.trim() + if (trimmed !== '' && trimmed !== current.name) payload.name = trimmed + if (settingsChanged(current.settings, draft)) { + payload.settings = buildSettingsPayload(draft) + } + return payload +} + +/** 绑定变更分类(确认文案与提交动作共用;none=不提交) */ +export type BindChangeKind = 'bind' | 'rebind' | 'unbind' | 'none' + +/** 当前绑定 → 下一步绑定 的变更分类(值不变 none,不制造服务端 no-op) */ +export function bindChangeKind( + currentAlbumId: string | null, + nextAlbumId: string | null +): BindChangeKind { + if (nextAlbumId === currentAlbumId) return 'none' + if (nextAlbumId === null) return 'unbind' + return currentAlbumId === null ? 'bind' : 'rebind' +} diff --git a/apps/admin/src/views/system/embed-slots/EmbedCodeDialog.vue b/apps/admin/src/views/system/embed-slots/EmbedCodeDialog.vue new file mode 100644 index 0000000..a11c95b --- /dev/null +++ b/apps/admin/src/views/system/embed-slots/EmbedCodeDialog.vue @@ -0,0 +1,77 @@ + + + + diff --git a/apps/admin/src/views/system/embed-slots/SlotBindDialog.vue b/apps/admin/src/views/system/embed-slots/SlotBindDialog.vue new file mode 100644 index 0000000..00013d5 --- /dev/null +++ b/apps/admin/src/views/system/embed-slots/SlotBindDialog.vue @@ -0,0 +1,190 @@ + + + + diff --git a/apps/admin/src/views/system/embed-slots/SlotFormDialog.vue b/apps/admin/src/views/system/embed-slots/SlotFormDialog.vue new file mode 100644 index 0000000..02117d6 --- /dev/null +++ b/apps/admin/src/views/system/embed-slots/SlotFormDialog.vue @@ -0,0 +1,185 @@ + + + + diff --git a/apps/admin/src/views/system/embed-slots/index.vue b/apps/admin/src/views/system/embed-slots/index.vue new file mode 100644 index 0000000..841b2e5 --- /dev/null +++ b/apps/admin/src/views/system/embed-slots/index.vue @@ -0,0 +1,203 @@ + + + +