diff --git a/.ai/product/feature-spec/README.md b/.ai/product/feature-spec/README.md index d61e4c1..f7e62d8 100644 --- a/.ai/product/feature-spec/README.md +++ b/.ai/product/feature-spec/README.md @@ -41,3 +41,4 @@ **P2 三模块队列(设计立项 / 编码另批):** [P2-BACKLOG.md](P2-BACKLOG.md) **竞品逆向(测测前端全量):** [cece-frontend-re/](cece-frontend-re/README.md) · **完整设计包:** [cece-frontend-re/complete-design/](cece-frontend-re/complete-design/README.md) · 方法见 [../../design/reverse-engineering-spec.md](../../design/reverse-engineering-spec.md) +| [ops-feed-slot.md](ops-feed-slot.md) | OpsCMS FeedSlot | §7 | `admin-h5` `/cms` · `GET /admin/cms/feed-slots*` | Ops-D · **ECR-025 Closed** | diff --git a/.ai/product/feature-spec/ops-feed-slot.md b/.ai/product/feature-spec/ops-feed-slot.md new file mode 100644 index 0000000..1a061f6 --- /dev/null +++ b/.ai/product/feature-spec/ops-feed-slot.md @@ -0,0 +1,41 @@ +# Feature Spec: OpsCMS · FeedSlot(Ops · ECR-025) + +> Status: `Active`(Loop continuous · **ECR-025 Closed**) +> Parent: WAVE0-FROZEN · Predecessor: ECR-024 Closed +> Capability: `OpsCMS` · BC: `Ops_CMS_NoUGC` +> 授权:`docs/WAVE0/LOOP_AUTHORIZATION.md` + +## Non-goals + +FeedSlot 写发布 · ScheduledPublication · UGC · 真支付 · Banner 写 + +## L2 Domain + +| 概念 | 语义 | +|------|------| +| `FeedSlot` | 本切片只读目录;code 唯一(若适用) | + +## L3 API + +| Method | Path | 权限 | 语义 | +|--------|------|------|------| +| GET | `/admin/cms/feed-slots` | `admin.cms.read` | 只读 | +| GET | `/admin/cms/feed-slots/{id}` | `admin.cms.read` | 只读 | + +## Migration + +`000026`:表 + 种子(若有)(权限复用) + +## L4 AC + +| ID | Then | +|----|------| +| AC-F-01 | list 含种子或空列表合法 | +| AC-F-02 | 已知 id get 200 | +| AC-F-03 | 未知 id → 404 | +| AC-S-01 | 无 Admin → 401 | +| AC-S-02 | 无权限 → 403 | +| AC-P-01 | list < 500ms | +| AC-O-01 | N/A 只读 | + +contract_diff: `docs/CONTRACT_DIFF/ECR-025.yaml` diff --git a/apps/admin-h5/src/api/client.ts b/apps/admin-h5/src/api/client.ts index b7d7cf8..ccd12eb 100644 --- a/apps/admin-h5/src/api/client.ts +++ b/apps/admin-h5/src/api/client.ts @@ -413,6 +413,30 @@ export const adminApi = { system: boolean updated_at: string }>('GET', `/cms/banners/${id}`), + feedSlots: () => + request<{ + items: Array<{ + id: string + code: string + title: string + slot_key: string + placement: string + active: boolean + system: boolean + updated_at: string + }> + }>('GET', '/cms/feed-slots'), + feedSlot: (id: string) => + request<{ + id: string + code: string + title: string + slot_key: string + placement: string + active: boolean + system: boolean + updated_at: string + }>('GET', `/cms/feed-slots/${id}`), orders: () => request<{ items: Array<{ diff --git a/apps/admin-h5/src/pages/CMSPage.vue b/apps/admin-h5/src/pages/CMSPage.vue index 5fa8479..de8a86e 100644 --- a/apps/admin-h5/src/pages/CMSPage.vue +++ b/apps/admin-h5/src/pages/CMSPage.vue @@ -3,12 +3,18 @@ import { onMounted, ref } from 'vue' import { adminApi } from '@/api/client' type Banner = Awaited>['items'][number] +type FeedSlot = Awaited>['items'][number] const loading = ref(false) const error = ref('') const items = ref([]) const selected = ref(null) +const slotsLoading = ref(false) +const slotsError = ref('') +const slots = ref([]) +const selectedSlot = ref(null) + async function load() { loading.value = true error.value = '' @@ -22,6 +28,19 @@ async function load() { } } +async function loadSlots() { + slotsLoading.value = true + slotsError.value = '' + try { + const res = await adminApi.feedSlots() + slots.value = res.items || [] + } catch (e) { + slotsError.value = e instanceof Error ? e.message : '加载失败' + } finally { + slotsLoading.value = false + } +} + async function openBanner(id: string) { try { selected.value = await adminApi.banner(id) @@ -30,13 +49,24 @@ async function openBanner(id: string) { } } -onMounted(load) +async function openSlot(id: string) { + try { + selectedSlot.value = await adminApi.feedSlot(id) + } catch { + selectedSlot.value = null + } +} + +onMounted(() => { + void load() + void loadSlots() +})