feat(admin): 广告位管理页——上传/链接/绑定相册/开关/排序
Task 11(广告位轮播):admin 后台广告位治理页。 - utils/ads/adsLogic:scopeKind/normalizeAlbumIds/isValidAdLinkUrl 纯逻辑 (与服务端 dto LINK_URL_PATTERN、albumIds 归一语义同源)+ vitest 6 例 - api/ads + types/api/ads.d.ts:list/create/update/delete/initUpload 五端点 (对照 embedSlots.ts 逐字风格;列表全量不分页、imageUrl 现签) - views/system/ads:列表(缩略图/备注名/链接截断/投放范围标签/启停开关 失败重拉回滚/上移下移 sortOrder 互换/硬删确认)+ 新建编辑对话框 (staging 直传 init→裸 fetch PUT、Content-Type 与申明一致;相册多选 远程搜索复用 /v1/albums 分页+本地过滤,绑定 ID 超限/失效以裸 ID 兜底; 编辑换图才带 image 三字段;albumIds 归一空→null 转全局) - 路由/菜单:system 组 ads 页(ri:advertisement-line,workspace 分组); locales zh/en 补 menus.system.ads + adsManage 子树(键集一致入单测) Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import zhMessages from '@/locales/langs/zh.json'
|
||||
import enMessages from '@/locales/langs/en.json'
|
||||
import { isValidAdLinkUrl, normalizeAlbumIds, scopeKind } from './adsLogic'
|
||||
|
||||
describe('scopeKind 投放范围分类', () => {
|
||||
it('null / 空数组 → global;非空 → targeted', () => {
|
||||
expect(scopeKind(null)).toBe('global')
|
||||
expect(scopeKind([])).toBe('global')
|
||||
expect(scopeKind(['album-1'])).toBe('targeted')
|
||||
expect(scopeKind(['album-1', 'album-2'])).toBe('targeted')
|
||||
})
|
||||
})
|
||||
|
||||
describe('normalizeAlbumIds 提交前归一化(服务端语义对齐)', () => {
|
||||
it('null 透传;空数组 → null(全局投放落库归一)', () => {
|
||||
expect(normalizeAlbumIds(null)).toBeNull()
|
||||
expect(normalizeAlbumIds([])).toBeNull()
|
||||
})
|
||||
|
||||
it('去重保序;非空结果原样返回', () => {
|
||||
expect(normalizeAlbumIds(['a', 'a'])).toEqual(['a'])
|
||||
expect(normalizeAlbumIds(['b', 'a', 'b', 'c'])).toEqual(['b', 'a', 'c'])
|
||||
expect(normalizeAlbumIds(['a', 'b'])).toEqual(['a', 'b'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('isValidAdLinkUrl 表单链接校验(与服务端 pattern 同源)', () => {
|
||||
it('http/https 前缀放行;其他协议/裸域名拒绝', () => {
|
||||
expect(isValidAdLinkUrl('https://example.com/promo')).toBe(true)
|
||||
expect(isValidAdLinkUrl('http://a.cn')).toBe(true)
|
||||
expect(isValidAdLinkUrl('ftp://example.com')).toBe(false)
|
||||
expect(isValidAdLinkUrl('example.com/promo')).toBe(false)
|
||||
expect(isValidAdLinkUrl('')).toBe(false)
|
||||
})
|
||||
|
||||
it('总长 ≤2048 边界:恰 2048 放行、2049 拒绝(契约 maxLength)', () => {
|
||||
expect(
|
||||
isValidAdLinkUrl(`https://example.com/${'a'.repeat(2048 - 'https://example.com/'.length)}`)
|
||||
).toBe(true)
|
||||
expect(
|
||||
isValidAdLinkUrl(`https://example.com/${'a'.repeat(2049 - 'https://example.com/'.length)}`)
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('adsManage 词条 zh/en 键集一致(对照 embedSlotsManage 先例)', () => {
|
||||
it('zh/en 键集合一致(adsManage 子树)', () => {
|
||||
const keysOf = (obj: Record<string, unknown>, prefix = ''): string[] =>
|
||||
Object.entries(obj).flatMap(([key, value]) =>
|
||||
typeof value === 'object' && value !== null
|
||||
? keysOf(value as Record<string, unknown>, `${prefix}${key}.`)
|
||||
: [`${prefix}${key}`]
|
||||
)
|
||||
expect(keysOf(enMessages.adsManage as Record<string, unknown>)).toEqual(
|
||||
keysOf(zhMessages.adsManage as Record<string, unknown>)
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* 广告位管理纯逻辑(广告位轮播 Task 11)——无 Vue 依赖,vitest 直测。
|
||||
*
|
||||
* - scopeKind/normalizeAlbumIds 对齐服务端归一语义:albumIds null=全局投放,
|
||||
* 空数组提交落库归一 null(AdCreateRequestDto 注释口径)
|
||||
* - isValidAdLinkUrl 与服务端 pattern 同源(dto.ts LINK_URL_PATTERN:
|
||||
* ^https?:// 前缀 + 契约 maxLength 2048)
|
||||
*/
|
||||
|
||||
/** 投放范围:null 或空数组 → 'global';非空 → 'targeted'。 */
|
||||
export function scopeKind(albumIds: string[] | null): 'global' | 'targeted' {
|
||||
return albumIds === null || albumIds.length === 0 ? 'global' : 'targeted'
|
||||
}
|
||||
|
||||
/** 提交前归一化:去重 + 空数组→null(服务端语义对齐)。 */
|
||||
export function normalizeAlbumIds(v: string[] | null): string[] | null {
|
||||
if (v === null) return null
|
||||
const uniq = [...new Set(v)]
|
||||
return uniq.length === 0 ? null : uniq
|
||||
}
|
||||
|
||||
/** 表单链接校验(与服务端 pattern 同源:^https?://,总长 ≤2048)。 */
|
||||
export function isValidAdLinkUrl(u: string): boolean {
|
||||
return /^https?:\/\//.test(u) && u.length <= 2048
|
||||
}
|
||||
Reference in New Issue
Block a user