Files
WonderQ-Admin-UI/docs/admin-module-config-api.md
duanshuwen 462498660e feat: enhance admin UI with responsive styles and new components
- Updated styles in src/styles.css for better responsiveness and layout adjustments.
- Added new CSS classes for edit drawer and item rail components.
- Introduced animations for drawer transitions.
- Created new types in src/types/admin.ts for better type safety in admin features.
- Modified vite.config.ts to change server port and enable strict port settings for development.
2026-07-01 16:55:23 +08:00

402 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 页面模块配置 Admin API 契约
本文档定义 WonderQ-Admin 后端需要为 WonderQ-Admin-UI 实现的页面模块配置 CRUD 接口。接口用于维护小程序/H5 前台页面模块中的配置数据,例如首页轮播、目的地宫格、主题卡片和底部运营入口。
## 适用模块
当前一期只纳入结构化页面配置模块:
| module | 名称 | 用途 |
| -------------- | ------------ | ---------------------------------------------- |
| `heroSlides` | 顶部轮播 | 首页首屏轮播图、标题短文案、展示排序和启用状态 |
| `destinations` | 目的地 | 首页/目的地页展示、搜索入口和热门标记 |
| `themes` | 主题甄选 | 首页主题卡片和跳转 |
| `ctaBanners` | 底部运营入口 | 权益卡、管家入口、需求入口等 CTA |
商品池、活动商品池和线索跟进继续走独立业务接口,不混入本契约。
## 通用约定
- 所有接口前缀为 `/api/admin`
- 所有接口需要校验 `Authorization: Bearer <token>`
- 请求和响应均为 `application/json`
- 字段使用 camelCase。
- `PATCH` 为部分更新,只修改请求体中出现的字段。
- 删除接口返回 JSON不能返回空 body因为当前前端请求封装会读取 JSON。
错误响应保持当前前端兼容格式:
```json
{
"message": "模块不存在或无权限操作",
"code": "MODULE_CONFIG_FORBIDDEN",
"details": {}
}
```
## 类型定义
```ts
type SiteModule = "heroSlides" | "destinations" | "themes" | "ctaBanners";
type SiteItemPatch = {
title?: string;
kicker?: string;
name?: string;
slug?: string;
region?: string | null;
label?: string;
alt?: string;
image?: string | null;
targetType?: string | null;
targetValue?: string | null;
isHot?: boolean;
isActive?: boolean;
sortOrder?: number;
};
type HeroSlide = {
id: string;
title: string;
kicker: string | null;
image: string | null;
isActive: boolean;
sortOrder: number;
createdAt?: string;
updatedAt?: string;
};
type HeroSlideCreateInput = {
title: string;
kicker?: string | null;
image?: string | null;
isActive?: boolean;
sortOrder?: number;
};
type HeroSlideUpdateInput = Partial<HeroSlideCreateInput>;
```
各模块字段要求:
| module | 创建必填 | 可选字段 |
| -------------- | -------- | ------------------------------------------------------------- |
| `heroSlides` | `title` | `kicker``image``isActive``sortOrder` |
| `destinations` | `name` | `slug``region``image``isHot``isActive``sortOrder` |
| `themes` | `label` | `image``targetType``targetValue``isActive``sortOrder` |
| `ctaBanners` | `alt` | `image``targetType``targetValue``isActive``sortOrder` |
后端可以在创建时补全 `id`、默认 `isActive=true`、默认 `sortOrder=当前模块最后一位`
## 顶部轮播 `heroSlides` 专用契约
顶部轮播对应当前管理端抽屉中的 3 个区域:
- 展示内容:`title``kicker`
- 资源图片:`image`
- 排序与状态:`sortOrder``isActive`
字段语义:
| 字段 | 类型 | 创建 | 更新 | 说明 |
| ----------- | ---------------- | -------- | ---------- | ---------------------------------------------------------------------- |
| `id` | `string` | 后端生成 | 不允许修改 | 顶部轮播配置项唯一 id |
| `title` | `string` | 必填 | 可选 | 前台轮播主标题,提交时需要去除首尾空格,不能为空 |
| `kicker` | `string \| null` | 可选 | 可选 | 副标题/短文案,空字符串可归一化为 `null``""`,前后端需保持响应一致 |
| `image` | `string \| null` | 可选 | 可选 | 单张轮播图地址或素材 URL未上传时为 `null` |
| `sortOrder` | `number` | 可选 | 可选 | 展示顺序,整数;未传时追加到当前模块末尾 |
| `isActive` | `boolean` | 可选 | 可选 | 前台是否展示;未传时默认 `true` |
| `createdAt` | `string` | 后端生成 | 后端维护 | ISO 时间字符串,可选返回 |
| `updatedAt` | `string` | 后端生成 | 后端维护 | ISO 时间字符串,可选返回 |
顶部轮播不定义跳转能力。`heroSlides` 的创建、更新、查询响应中不要返回 `targetType``targetValue`;兼容期如果请求体携带这两个字段,后端可以忽略,但不要写入顶部轮播业务数据。
### 顶部轮播 CRUD
新增顶部轮播:
```http
POST /api/admin/site-config/heroSlides
```
请求体:
```json
{
"title": "新轮播",
"kicker": "贵州小包团首选",
"image": "/assets/source/hero.jpg",
"isActive": true,
"sortOrder": 0
}
```
响应状态码 `201`
```json
{
"id": "slide_001",
"title": "新轮播",
"kicker": "贵州小包团首选",
"image": "/assets/source/hero.jpg",
"isActive": true,
"sortOrder": 0,
"createdAt": "2026-07-01T08:00:00.000Z",
"updatedAt": "2026-07-01T08:00:00.000Z"
}
```
更新顶部轮播:
```http
PATCH /api/admin/site-config/heroSlides/:id
```
请求体为 `HeroSlideUpdateInput`,只提交需要修改的字段:
```json
{
"title": "夏日贵州小包团",
"image": null,
"isActive": false
}
```
响应状态码 `200`,响应体返回更新后的完整 `HeroSlide`
删除顶部轮播:
```http
DELETE /api/admin/site-config/heroSlides/:id
```
响应状态码 `200`
```json
{
"id": "slide_001"
}
```
删除后后端需要重新整理 `heroSlides` 内剩余项的 `sortOrder`
调整顶部轮播顺序:
```http
PATCH /api/admin/site-config/heroSlides/reorder
```
请求体:
```json
{
"itemIds": ["slide_002", "slide_001", "slide_003"]
}
```
响应状态码 `200`
```json
{
"items": [
{
"id": "slide_002",
"title": "第二张",
"kicker": null,
"image": null,
"sortOrder": 0,
"isActive": true
},
{
"id": "slide_001",
"title": "第一张",
"kicker": null,
"image": null,
"sortOrder": 1,
"isActive": true
}
]
}
```
## 接口列表
以下路径由四类页面模块复用;`heroSlides` 的请求体和响应体以“顶部轮播专用契约”为准。
### 获取完整站点配置
```http
GET /api/admin/site-config
```
响应:
```ts
type SiteConfig = {
heroSlides: HeroSlide[];
destinations: Destination[];
themes: ThemeCard[];
ctaBanners: CtaBanner[];
};
```
### 新增模块配置项
```http
POST /api/admin/site-config/:module
```
请求体为 `SiteItemPatch`。响应状态码 `201`,响应体返回创建后的完整配置项:
```json
{
"id": "slide_001",
"title": "新轮播",
"kicker": "",
"image": null,
"isActive": true,
"sortOrder": 5
}
```
### 更新模块配置项
```http
PATCH /api/admin/site-config/:module/:id
```
请求体为 `SiteItemPatch`。响应状态码 `200`,响应体返回更新后的完整配置项。
### 删除模块配置项
```http
DELETE /api/admin/site-config/:module/:id
```
响应状态码 `200`,响应体:
```json
{
"id": "slide_001"
}
```
删除后后端需要重新整理同模块内剩余项的 `sortOrder`
### 调整模块配置顺序
```http
PATCH /api/admin/site-config/:module/reorder
```
请求体:
```json
{
"itemIds": ["slide_002", "slide_001", "slide_003"]
}
```
约束:
- `itemIds` 必须包含该模块当前全部配置项 id。
- 不允许重复 id。
- 不允许混入其他模块 id。
- 后端按数组顺序写入 `sortOrder`,从 0 开始。
响应状态码 `200`
```json
{
"items": [
{ "id": "slide_002", "title": "第二张", "sortOrder": 0, "isActive": true },
{ "id": "slide_001", "title": "第一张", "sortOrder": 1, "isActive": true }
]
}
```
如果后端使用 Express/Fastify 等路由,`/:module/reorder` 需要注册在 `/:module/:id` 之前,避免 `reorder` 被当成 id。
## 状态码
| 状态码 | 场景 |
| ------ | ------------------------------------------------ |
| `200` | 查询、更新、删除、排序成功 |
| `201` | 创建成功 |
| `400` | module 非法、请求体格式错误、排序 id 不完整 |
| `401` | 未登录或 token 无效 |
| `403` | 无权限维护页面配置 |
| `404` | 配置项不存在 |
| `409` | 删除被发布版本、商品或活动引用的配置项时发生冲突 |
| `422` | 字段校验失败,例如必填标题为空 |
| `500` | 服务端异常 |
## 前端联调入口
WonderQ-Admin-UI 当前调用函数位于 `src/api.ts`
- `getSiteConfig()`
- `createSiteConfigItem(module, input)`
- `updateSiteConfigItem(module, id, input)`
- `deleteSiteConfigItem(module, id)`
- `reorderSiteConfigItems(module, itemIds)`
维护地图 UI 位于 `src/pages/structure/StructurePage.tsx`
## 图片素材上传
用于 WonderQ-Admin-UI 在维护页面模块图片时上传本地图片,并把返回的 OSS URL 写入 `image` 字段。
```http
POST /api/admin/media-assets/upload
Content-Type: multipart/form-data
Authorization: Bearer <token>
```
表单字段:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `file` | `File` | 是 | 图片文件,仅支持 JPG、PNG、WebP、GIF |
| `group` | `string` | 否 | 素材分组,只能包含字母、数字、下划线和中划线;默认 `general` |
限制:
- 单个文件最大 5MB。
- 后端会校验 `Content-Type` 和文件头魔数,拒绝 SVG、非图片文件和伪装类型。
- 后端将文件上传到 OSS返回可用于前台展示的 URL并写入 `MediaAsset` 素材记录。
响应状态码 `201`
```json
{
"id": "asset_001",
"url": "https://bucket.oss-cn-example.aliyuncs.com/admin/heroSlides/2026/07/01/example.png",
"name": "hero.png",
"mimeType": "image/png",
"sizeBytes": 102400,
"group": "heroSlides",
"createdAt": "2026-07-01T08:00:00",
"updatedAt": "2026-07-01T08:00:00"
}
```
常见错误:
| 状态码 | `code` | 场景 |
| --- | --- | --- |
| `400` | `MEDIA_UPLOAD_INVALID_TYPE` | 文件不是允许的图片类型 |
| `400` | `MEDIA_UPLOAD_TYPE_MISMATCH` | `Content-Type` 与文件头不一致 |
| `400` | `MEDIA_UPLOAD_INVALID_GROUP` | `group` 格式非法 |
| `413` | `MEDIA_UPLOAD_TOO_LARGE` | 文件超过 5MB |
| `503` | `MEDIA_STORAGE_NOT_CONFIGURED` | OSS 环境配置不完整 |
| `502` | `MEDIA_STORAGE_UPLOAD_FAILED` | OSS 上传失败 |
前端封装位于 `src/api.ts`
```ts
uploadMediaAsset(file: File, group?: string): Promise<MediaAsset>
```
`src/components/admin/SingleImageUploader.tsx` 已使用该封装;结构维护页会把当前模块 id 作为 `group` 上传。