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.
This commit is contained in:
@@ -3,5 +3,6 @@
|
||||
本目录存放后台管理前端相关文档。
|
||||
|
||||
- `admin-backend-plan.md`:从原 MiniAPP 迁出的后台管理规划,保留管理模块、页面能力和与后端 API 的协作边界。
|
||||
- `admin-module-config-api.md`:页面模块配置 CRUD 接口契约,供 WonderQ-Admin 后端实现首页轮播、目的地、主题卡和 CTA 等模块配置接口。
|
||||
|
||||
后端 API 文档位于 `D:\www\znkj\WonderQ-Admin\docs`。
|
||||
后端 API 文档位于 `D:\www\znkj\WonderQ-Admin\docs`。
|
||||
|
||||
401
docs/admin-module-config-api.md
Normal file
401
docs/admin-module-config-api.md
Normal file
@@ -0,0 +1,401 @@
|
||||
# 页面模块配置 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` 上传。
|
||||
Reference in New Issue
Block a user