docs: 广告位实施计划 Task 5-12 勾选
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -490,15 +490,15 @@ private buildPayload(ad: AdRow): AdUpdatedPayload {
|
||||
|
||||
**Files:** Modify `services/api/src/public-ov/public-ov.controller.ts`、`public-ov.service.ts`;Test `services/api/test/public-ov.e2e-spec.ts`(追加段)
|
||||
|
||||
- [ ] **Step 1: 写失败测试**(public-ov.e2e 追加,ov app 实例 + prismaOv 直种 ads_ov/albums_ov):
|
||||
- [x] **Step 1: 写失败测试**(public-ov.e2e 追加,ov app 实例 + prismaOv 直种 ads_ov/albums_ov):
|
||||
```ts
|
||||
// ① token 有效 + 全局广告 + 绑定本册广告 + 绑定他册广告 → items 恰含前两者,
|
||||
// sortOrder 升序、并列 createdAt 升序;imageUrl 现签(URL 含签名参数形态);
|
||||
// ② 相册不存在/offline → 404 同公开相册码;③ 无广告 → items:[];
|
||||
// ④ 匿名 200(security: [])。
|
||||
```
|
||||
- [ ] **Step 2: 跑测试确认失败** —— `pnpm --filter api run test:e2e -- public-ov`。
|
||||
- [ ] **Step 3: 实现**:
|
||||
- [x] **Step 2: 跑测试确认失败** —— `pnpm --filter api run test:e2e -- public-ov`。
|
||||
- [x] **Step 3: 实现**:
|
||||
```ts
|
||||
// controller(getPublicAlbum 之后):
|
||||
/** 公开广告列表:全局+绑定本册,现签图 URL(广告位轮播 2026-09-28 设计 §5.2)。 */
|
||||
@@ -526,14 +526,14 @@ private buildPayload(ad: AdRow): AdUpdatedPayload {
|
||||
return { items };
|
||||
}
|
||||
```
|
||||
- [ ] **Step 4: 跑测试确认通过**。
|
||||
- [ ] **Step 5: Commit** —— `feat(api): GET /v1/public/albums/{token}/ads 公开广告(全局+定向/现签)`
|
||||
- [x] **Step 4: 跑测试确认通过**。
|
||||
- [x] **Step 5: Commit** —— `feat(api): GET /v1/public/albums/{token}/ads 公开广告(全局+定向/现签)`
|
||||
|
||||
### Task 10: viewer——AdCarousel 与页面集成
|
||||
|
||||
**Files:** Create `apps/viewer/src/components/ads/AdCarousel.vue`、`apps/viewer/src/ads/carousel.ts`;Modify `apps/viewer/src/api/client.ts`、`apps/viewer/src/pages/ActivityPage.vue`、`apps/viewer/src/i18n/locales/{zh-CN,en}.ts`;Test `apps/viewer/src/ads/carousel.spec.ts`(vitest,对照 viewer 既有 spec 位置与配置)
|
||||
|
||||
- [ ] **Step 1: 纯逻辑模块 + 失败测试**(carousel.ts:轮播推进/环形回绕/暂停语义纯函数,便于 vitest 直测):
|
||||
- [x] **Step 1: 纯逻辑模块 + 失败测试**(carousel.ts:轮播推进/环形回绕/暂停语义纯函数,便于 vitest 直测):
|
||||
```ts
|
||||
/** 轮播推进:环形(末→首);单条恒 0(不切换)。 */
|
||||
export function nextIndex(count: number, current: number, step: 1 | -1 = 1): number {
|
||||
@@ -553,13 +553,13 @@ export function swipeStep(dx: number, dy: number): 1 | -1 | 0 {
|
||||
(无圆点导航节点);多条圆点点击切换;✕ 点击 emit close;slide 点击调
|
||||
window.open spy(jest.fn 挂 global);autoplay 用 vi.useFakeTimers 推进 4s 断言
|
||||
index 前进、pointerenter 后暂停。
|
||||
- [ ] **Step 2: client.ts** —— 类型 + 请求(对照 getPublicAlbum 的既有写法逐字模仿其 apiRequest 用法):
|
||||
- [x] **Step 2: client.ts** —— 类型 + 请求(对照 getPublicAlbum 的既有写法逐字模仿其 apiRequest 用法):
|
||||
```ts
|
||||
export type PublicAdListResponse = components['schemas']['PublicAdListResponse'];
|
||||
export type PublicAd = components['schemas']['PublicAd'];
|
||||
export async function getPublicAds(token: string): Promise<PublicAdListResponse> { /* 同形 */ }
|
||||
```
|
||||
- [ ] **Step 3: AdCarousel.vue**(完整实现;视觉 token 一律 var(--viewer-*)):
|
||||
- [x] **Step 3: AdCarousel.vue**(完整实现;视觉 token 一律 var(--viewer-*)):
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// 广告轮播(设计 §6.2):4s 自动/悬停与触摸暂停/圆点/滑动/新开标签;
|
||||
@@ -610,7 +610,7 @@ onBeforeUnmount(() => { if (timer !== undefined) clearInterval(timer); });
|
||||
✕ 右上 ≥44px 触达;圆点底部居中;全部颜色/圆角用 --viewer-* token。 -->
|
||||
```
|
||||
(样式块按上述要点补全为 scoped CSS——实现时对照 viewer 既有组件样式风格。)
|
||||
- [ ] **Step 4: ActivityPage 集成**:
|
||||
- [x] **Step 4: ActivityPage 集成**:
|
||||
```ts
|
||||
import AdCarousel from '../components/ads/AdCarousel.vue';
|
||||
import { getPublicAds, type PublicAd } from '../api/client';
|
||||
@@ -627,15 +627,15 @@ function dismissAds(): void {
|
||||
}
|
||||
```
|
||||
模板:`<AlbumHeader … />` 与 `<PhotoFeed … />` 之间插 `<AdCarousel v-if="ads.length > 0" :ads="ads" @close="dismissAds" />`。
|
||||
- [ ] **Step 5: i18n** —— locales/zh-CN.ts:`ads: { label: '广告', close: '关闭广告', open: '打开广告链接', dots: '轮播位置', goto: '第 {n} 张' }`;en:`ads: { label: 'Ad', close: 'Close ads', open: 'Open ad link', dots: 'Carousel position', goto: 'Go to {n}' }`(对照两文件既有结构与导出方式插入)。
|
||||
- [ ] **Step 6: 验证** —— `pnpm --filter viewer run test`(含新 spec)+ `pnpm --filter viewer run typecheck` + `pnpm --filter viewer run build`。
|
||||
- [ ] **Step 7: Commit** —— `feat(viewer): 相册页广告轮播——4s 自动/暂停/滑动/徽标/会话级关闭`
|
||||
- [x] **Step 5: i18n** —— locales/zh-CN.ts:`ads: { label: '广告', close: '关闭广告', open: '打开广告链接', dots: '轮播位置', goto: '第 {n} 张' }`;en:`ads: { label: 'Ad', close: 'Close ads', open: 'Open ad link', dots: 'Carousel position', goto: 'Go to {n}' }`(对照两文件既有结构与导出方式插入)。
|
||||
- [x] **Step 6: 验证** —— `pnpm --filter viewer run test`(含新 spec)+ `pnpm --filter viewer run typecheck` + `pnpm --filter viewer run build`。
|
||||
- [x] **Step 7: Commit** —— `feat(viewer): 相册页广告轮播——4s 自动/暂停/滑动/徽标/会话级关闭`
|
||||
|
||||
### Task 11: admin——广告位管理页
|
||||
|
||||
**Files:** Create `apps/admin/src/api/ads.ts`、`apps/admin/src/types/api/ads.d.ts`、`apps/admin/src/utils/ads/adsLogic.ts` + `adsLogic.spec.ts`、`apps/admin/src/views/system/ads/{index.vue,AdsFormDialog.vue}`;Modify 路由/菜单注册点、locales;先定位:`grep -rn "embed-slots" apps/admin/src/router apps/admin/src/config` 找到 route+menu 注册文件
|
||||
|
||||
- [ ] **Step 1: 纯逻辑 + 失败测试**(adsLogic.ts,vitest——对照 utils/system/embedSlots.spec.ts 模式):
|
||||
- [x] **Step 1: 纯逻辑 + 失败测试**(adsLogic.ts,vitest——对照 utils/system/embedSlots.spec.ts 模式):
|
||||
```ts
|
||||
/** 投放范围标签:全局 → 'global';定向 → 'targeted'。 */
|
||||
export function scopeKind(albumIds: string[] | null): 'global' | 'targeted' {
|
||||
@@ -652,7 +652,7 @@ export function isValidAdLinkUrl(u: string): boolean {
|
||||
return /^https?:\/\/.{3,2041}$/.test(u) && u.length <= 2048;
|
||||
}
|
||||
```
|
||||
- [ ] **Step 2: api/ads.ts**(对照 embedSlots.ts 逐字风格):
|
||||
- [x] **Step 2: api/ads.ts**(对照 embedSlots.ts 逐字风格):
|
||||
```ts
|
||||
import request from '@/utils/http'
|
||||
export function fetchAds() {
|
||||
@@ -671,20 +671,20 @@ export function fetchDeleteAd(id: string) {
|
||||
return request.del<void>({ url: `/v1/admin/ads/${id}` })
|
||||
}
|
||||
```
|
||||
- [ ] **Step 3: 页面**(views/system/ads/index.vue + AdsFormDialog.vue,对照 embed-slots/index.vue + SlotFormDialog.vue 结构):
|
||||
- [x] **Step 3: 页面**(views/system/ads/index.vue + AdsFormDialog.vue,对照 embed-slots/index.vue + SlotFormDialog.vue 结构):
|
||||
- index.vue:ElCard 列表——缩略图(行内 imageUrl)、title、linkUrl、投放范围(scopeKind → 「全局」ElTag / 绑定相册数)、enabled ElSwitch(change→fetchUpdateAd({enabled}))、上移/下移(与相邻行互换 sortOrder:两次 fetchUpdateAd,完成后重拉列表)、编辑、删除(ElMessageBox.confirm → fetchDeleteAd)。
|
||||
- AdsFormDialog.vue:新建/编辑双态——图片上传(fetchInitAdUpload → 裸 fetch PUT(仿 AlbumBannerUpload 的直传写法,Content-Type 与申明一致)→ 持 stagingKey/bytes/contentType 于表单态)、链接输入(isValidAdLinkUrl 规则)、备注名、绑定相册 ElSelect multiple filterable remote(数据源 GET /v1/albums 分页接口——对照相册管理页既有调用)、提交 normalizeAlbumIds 后 fetchCreateAd/fetchUpdateAd。
|
||||
- 上传控件展示建议文案:1200×360、≤2MB、jpeg/png/heic。
|
||||
- [ ] **Step 4: 路由/菜单/i18n** —— 在 Step 定位到的注册点按 embed-slots 同款登记「广告位」(图标用 ElementPlus 任意广告语义图标,i18n key `route.ads` 等——对照既有菜单多语言落点补 zh-CN/en)。
|
||||
- [ ] **Step 5: 验证** —— `pnpm --filter admin run test`(含 adsLogic.spec)+ `pnpm --filter admin run typecheck` + `pnpm --filter admin run build`。
|
||||
- [ ] **Step 6: Commit** —— `feat(admin): 广告位管理页——上传/链接/绑定相册/开关/排序`
|
||||
- [x] **Step 4: 路由/菜单/i18n** —— 在 Step 定位到的注册点按 embed-slots 同款登记「广告位」(图标用 ElementPlus 任意广告语义图标,i18n key `route.ads` 等——对照既有菜单多语言落点补 zh-CN/en)。
|
||||
- [x] **Step 5: 验证** —— `pnpm --filter admin run test`(含 adsLogic.spec)+ `pnpm --filter admin run typecheck` + `pnpm --filter admin run build`。
|
||||
- [x] **Step 6: Commit** —— `feat(admin): 广告位管理页——上传/链接/绑定相册/开关/排序`
|
||||
|
||||
### Task 12: 全量回归与红线自查
|
||||
|
||||
- [ ] **Step 1: 全套件** —— `pnpm typecheck && pnpm lint && pnpm test`(turbo 全包;**先杀游离 dev:worker**)。
|
||||
- [ ] **Step 2: e2e 全量** —— `pnpm --filter api run test:e2e`(含新增 ads/public-ov 追加段;health 偶发长跑瞬时失败按既有登记单独重跑确认)。
|
||||
- [ ] **Step 3: 红线自查清单** —— ① 账本表无任何 DELETE/TRUNCATE 迁移或代码(`git diff -- '*migration*' | grep -i 'delete\|truncate'` 应只命中 ads 相关新表);② openapi 新 schema 无必填进旧端点;③ flutter 零改动(`git status apps/mobile` 干净);④ viewer 嵌入路由(EmbedPage 树)无 AdCarousel 引用。
|
||||
- [ ] **Step 4: Commit**(如有修补)+ 设计文档勾选回填。
|
||||
- [x] **Step 1: 全套件** —— `pnpm typecheck && pnpm lint && pnpm test`(turbo 全包;**先杀游离 dev:worker**)。
|
||||
- [x] **Step 2: e2e 全量** —— `pnpm --filter api run test:e2e`(含新增 ads/public-ov 追加段;health 偶发长跑瞬时失败按既有登记单独重跑确认)。
|
||||
- [x] **Step 3: 红线自查清单** —— ① 账本表无任何 DELETE/TRUNCATE 迁移或代码(`git diff -- '*migration*' | grep -i 'delete\|truncate'` 应只命中 ads 相关新表);② openapi 新 schema 无必填进旧端点;③ flutter 零改动(`git status apps/mobile` 干净);④ viewer 嵌入路由(EmbedPage 树)无 AdCarousel 引用。
|
||||
- [x] **Step 4: Commit**(如有修补)+ 设计文档勾选回填。
|
||||
|
||||
### Task 13: 部署上线(**需用户明确确认后执行**;顺序硬约束:ov 先 cn 后)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user