新增设计规格涵盖: - media_folder / media_item / banner 三个实体 - 媒体库 API(CRUD + 上传 + 裁剪 + 文件夹管理) - 轮播图管理 API(CRUD + 排序 + 定时上下架) - 公开端点(签名 URL 机制)+ 公开/私有访问控制 - 管理后台 UI(方案 A 左树+网格)+ MediaPicker 组件 - 小程序访客页改造(动态轮播图 + 文章卡片列表)
24 KiB
媒体库与轮播图管理 — 设计规格
日期: 2026-05-10 | 模块: erp-health | 状态: 设计中
1. 背景与目标
1.1 问题
小程序访客首页存在两个硬编码问题:
- 轮播图硬编码 — CSS 渐变背景,无图片,无后台管理。运营人员无法动态更新轮播内容。
- "核心功能"区域硬编码 — 3 张静态功能卡片,应改为按分类筛选的文章列表,让运营人员通过发布文章控制首页展示内容。
此外,当前系统缺少通用媒体库能力:
- 上传的文件没有数据库记录,无法搜索、分类、复用
- 文章编辑器和轮播图各自独立上传图片,无法共享资源
- 没有文件夹组织,上传量大后难以管理
1.2 目标
- 新增媒体库模块:上传图片 → 文件夹分类 → 搜索筛选 → 多处复用(轮播图、文章封面、文章插图)
- 新增轮播图管理:从媒体库选图 → 配置标题/副标题/跳转 → 排序/定时上下架
- 改造小程序访客首页:轮播图从 API 动态获取,"核心功能"改为文章卡片列表
- 公开/私有访问控制:媒体资源可标记为公开,未登录用户可查看
1.3 范围
- 媒体库放在
erp-health模块内(当前只有健康管理使用媒体库) - 支持图片类型(JPEG/PNG/GIF/WebP),文档和视频不在本期范围
- 在线编辑仅支持裁剪+缩放,不做复杂图片编辑
- 缩略图自动生成,尺寸固定 200×200
1.4 依赖
- 现有
POST /api/v1/upload端点和本地文件存储 - 现有文章管理模块(entity/handler/service/dto)
- 现有 Wangeditor 富文本编辑器
- 现有 Ant Design 组件库和主题系统
2. 数据模型
2.1 media_folder — 媒体文件夹
树形结构,支持嵌套分类。
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | UUID v7 | PK | 主键 |
| tenant_id | UUID | NOT NULL, IDX | 租户隔离 |
| name | String(100) | NOT NULL | 文件夹名称 |
| parent_id | UUID | NULL, FK→media_folder.id | 父文件夹(NULL 表示根级) |
| sort_order | i32 | DEFAULT 0 | 同级排序 |
| created_at | DateTime | NOT NULL | 创建时间 |
| updated_at | DateTime | NOT NULL | 更新时间 |
| created_by | UUID | NULL | 创建人 |
| updated_by | UUID | NULL | 更新人 |
| deleted_at | DateTime | NULL | 软删除标记 |
| version | i32 | NOT NULL, DEFAULT 1 | 乐观锁 |
约束:
- 同一
tenant_id+parent_id下name唯一(未删除的) - 删除文件夹时,必须先清空其下所有子文件夹和媒体资源(或级联移动到父级)
2.2 media_item — 媒体资源
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | UUID v7 | PK | 主键 |
| tenant_id | UUID | NOT NULL, IDX | 租户隔离 |
| folder_id | UUID | NULL, FK→media_folder.id | 所属文件夹(NULL 表示未分类) |
| filename | String(255) | NOT NULL | 原始文件名 |
| storage_path | String(500) | NOT NULL, UNIQUE | 存储路径 /uploads/{tenant}/{uuid}.ext |
| thumbnail_path | String(500) | NULL | 缩略图路径 /uploads/{tenant}/thumb_{uuid}.ext |
| content_type | String(100) | NOT NULL | MIME 类型 |
| file_size | i64 | NOT NULL | 文件大小(bytes) |
| width | i32 | NULL | 图片宽度(px) |
| height | i32 | NULL | 图片高度(px) |
| alt_text | String(255) | NULL | 替代文本(用于无障碍和 SEO) |
| is_public | bool | NOT NULL, DEFAULT false | 公开标记(公开资源无需认证即可访问) |
| created_at | DateTime | NOT NULL | 创建时间 |
| updated_at | DateTime | NOT NULL | 更新时间 |
| created_by | UUID | NULL | 上传人 |
| updated_by | UUID | NULL | 更新人 |
| deleted_at | DateTime | NULL | 软删除标记 |
| version | i32 | NOT NULL, DEFAULT 1 | 乐观锁 |
索引:
idx_media_item_tenant_folderON (tenant_id, folder_id) — 按文件夹查询idx_media_item_tenant_publicON (tenant_id, is_public) — 筛选公开资源idx_media_item_content_typeON (content_type) — 按类型筛选
2.3 banner — 轮播图
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | UUID v7 | PK | 主键 |
| tenant_id | UUID | NOT NULL, IDX | 租户隔离 |
| media_item_id | UUID | NOT NULL, FK→media_item.id | 关联媒体资源 |
| title | String(100) | NULL | 标题(覆盖媒体库 alt_text) |
| subtitle | String(255) | NULL | 副标题 |
| link_type | String(20) | NULL | 跳转类型:article / external / none |
| link_target | String(500) | NULL | 跳转目标(文章 ID 或 URL) |
| sort_order | i32 | NOT NULL, DEFAULT 0 | 展示排序 |
| status | String(20) | NOT NULL, DEFAULT 'active' | 状态:active / inactive |
| start_time | DateTime | NULL | 定时上架时间(NULL 表示立即) |
| end_time | DateTime | NULL | 定时下架时间(NULL 表示永久) |
| created_at | DateTime | NOT NULL | 创建时间 |
| updated_at | DateTime | NOT NULL | 更新时间 |
| created_by | UUID | NULL | 创建人 |
| updated_by | UUID | NULL | 更新人 |
| deleted_at | DateTime | NULL | 软删除标记 |
| version | i32 | NOT NULL, DEFAULT 1 | 乐观锁 |
索引:
idx_banner_tenant_statusON (tenant_id, status) — 按状态筛选idx_banner_sortON (tenant_id, sort_order) — 排序查询
约束:
link_type仅允许article/external/nonestatus仅允许active/inactiveend_time>start_time(当两者都不为 NULL 时)
3. API 设计
3.1 媒体库 — /api/v1/health/media
| 方法 | 路径 | 说明 | 权限码 |
|---|---|---|---|
| GET | /media |
分页列表,支持 folder_id / content_type / keyword / is_public 筛选 | health.media.list |
| POST | /media/upload |
上传文件到媒体库(multipart/form-data),可选指定 folder_id 和 is_public | health.media.manage |
| GET | /media/{id} |
获取单个资源详情(含完整 URL) | health.media.list |
| PUT | /media/{id} |
更新元数据(alt_text / folder_id / is_public),含 version 乐观锁 | health.media.manage |
| DELETE | /media/{id} |
软删除,同时删除存储文件和缩略图 | health.media.manage |
| POST | /media/{id}/move |
移动到指定文件夹(body: { folder_id, version }) |
health.media.manage |
| POST | /media/batch-delete |
批量软删除(body: { ids: [] }) |
health.media.manage |
上传流程:
- 客户端
POST /media/upload,multipart 包含file+ 可选folder_id+is_public - 后端调用现有上传逻辑保存文件到
/uploads/{tenant_id}/{uuid}.ext - 读取图片尺寸(width/height),生成 200×200 缩略图保存到
/uploads/{tenant_id}/thumb_{uuid}.ext - 创建
media_item数据库记录 - 返回
MediaItemResp
列表查询参数:
folder_id— 按文件夹筛选(NULL 表示未分类,不传表示全部)content_type— 按 MIME 类型前缀筛选(如image/)keyword— 按文件名模糊搜索is_public— 按公开状态筛选page/page_size— 分页sort_by— 排序字段(created_at / file_size / filename),默认 created_at DESC
3.2 文件夹 — /api/v1/health/media-folders
| 方法 | 路径 | 说明 | 权限码 |
|---|---|---|---|
| GET | /media-folders |
返回当前租户的完整文件夹树 | health.media.list |
| POST | /media-folders |
创建文件夹(name + parent_id) | health.media.manage |
| PUT | /media-folders/{id} |
更新(重命名 / 移动 parent_id),含 version | health.media.manage |
| DELETE | /media-folders/{id} |
删除空文件夹(文件夹内有资源时拒绝) | health.media.manage |
文件夹树响应格式:
{
"data": [
{
"id": "uuid",
"name": "轮播图",
"parent_id": null,
"sort_order": 0,
"children": [],
"item_count": 5
}
]
}
3.3 轮播图管理 — /api/v1/health/banners
| 方法 | 路径 | 说明 | 权限码 |
|---|---|---|---|
| GET | /banners |
管理列表(含所有状态,支持 status 筛选) | health.banners.manage |
| POST | /banners |
创建轮播图 | health.banners.manage |
| PUT | /banners/{id} |
更新,含 version 乐观锁 | health.banners.manage |
| DELETE | /banners/{id} |
软删除 | health.banners.manage |
| PUT | /banners/sort |
批量更新排序(body: [{ id, sort_order }]) |
health.banners.manage |
创建/更新 DTO:
{
"media_item_id": "uuid",
"title": "专业血透中心",
"subtitle": "三甲级医护团队全程守护",
"link_type": "article",
"link_target": "article-uuid",
"sort_order": 1,
"status": "active",
"start_time": null,
"end_time": null
}
3.4 公开端点(无需认证)
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /public/banners |
返回当前生效的轮播图列表 |
| GET | /public/articles |
返回指定分类下的已发布文章(小程序访客页用) |
GET /public/banners 逻辑:
- 不需要 JWT 认证
- 查询条件:
status = 'active'ANDdeleted_at IS NULLAND 当前时间在[start_time, end_time]范围内(start_time/end_time 为 NULL 时不限) - 按
sort_order ASC排序 - 返回图片 URL 时附带短时效签名 token(1 小时有效)
- 返回格式:
{
"data": [
{
"id": "uuid",
"title": "专业血透中心",
"subtitle": "三甲级医护团队全程守护",
"image_url": "/uploads/{tenant}/xxx.jpg?token=xxx&expires=xxx",
"link_type": "article",
"link_target": "article-uuid"
}
]
}
GET /public/articles 逻辑:
- 复用现有
list_articlesservice 逻辑,但跳过认证 - 固定筛选
status = 'published' - 必须传
category_id参数(指定"首页推荐"分类) - 分页:默认 page_size=4
- 返回
ArticleListItem[](含 cover_image)
3.5 新增权限码
| 权限码 | 说明 | 默认角色 |
|---|---|---|
health.media.list |
查看媒体库 | admin, operator |
health.media.manage |
上传/编辑/删除媒体资源 | admin, operator |
health.banners.manage |
管理轮播图 | admin, operator |
4. 公开访问机制
4.1 问题
当前 /uploads/ 路径受 JWT 中间件保护,所有文件访问需要认证。但小程序访客页面的轮播图需要未登录用户可见。
4.2 方案:公开标记 + 签名 URL
数据库层: media_item.is_public 标记资源是否可公开访问。
访问流程:
GET /public/banners返回图片 URL 时,对is_public = true的资源生成签名 URL:- 格式:
/uploads/{tenant}/{file}?token={hmac_signature}&expires={unix_timestamp} - HMAC-SHA256 签名,密钥从配置读取(
storage.secret_key) - 有效期 1 小时
- 格式:
- 对
is_public = false的资源,仍通过标准 JWT 认证访问
文件服务中间件改动:
- 现有
upload_auth_middleware增加签名 URL 验证分支:- 请求携带
?token=&expires=参数时,验证签名和过期时间 - 验证通过则放行,不通过返回 401
- 不携带签名参数时,走原有 JWT 认证逻辑
- 请求携带
安全考虑:
- 签名密钥不暴露给客户端,仅后端生成和验证
- 签名 URL 有时效性,过期自动失效
- 私有资源不受影响,仍需 JWT
- 每个签名 URL 绑定具体文件路径,无法篡改
4.3 配置新增
[storage]
upload_dir = "./uploads"
max_file_size = "10MB"
secret_key = "change-me-in-production" # 签名 URL 密钥
5. 管理后台 UI
5.1 媒体库页面 — MediaLibrary
路径: apps/web/src/pages/health/MediaLibrary.tsx
布局:方案 A(左树 + 右网格)
采用 PageContainer 包裹,内部水平分割:
左侧栏(200px):
- 标题"文件夹" + 新建按钮
- 树形列表,每项显示图标 + 名称 + 资源数量
- "全部资源"为默认选中项(不对应 folder_id)
- 右键/更多菜单:重命名、删除
- 支持拖拽排序
右侧主区域:
- 工具栏:上传按钮(primary) + 搜索框 + 类型下拉 + 公开状态下拉 + 视图切换(网格/列表)
- 网格视图:4 列缩略图卡片,每张 200×200
- 卡片内容:缩略图 + 左上角公开标记 badge + 右上角 hover 操作按钮(复制 URL、编辑、删除)
- 下方显示文件名和尺寸信息
- 点击卡片打开大图预览 Modal
- 列表视图:Ant Design Table,列为缩略图(40×40)/文件名/类型/大小/公开状态/创建时间/操作
- 分页:底部分页条
上传交互:
- 点击"上传图片"按钮打开文件选择器(accept="image/*",multiple)
- 上传过程中显示进度条
- 上传完成自动刷新列表
编辑 Modal:
- alt_text 文本输入
- 文件夹下拉选择(移动到...)
- 公开/私有开关
- 保存后刷新列表
批量操作:
- 勾选多个卡片 → 底部浮动操作栏:批量删除 / 批量移动 / 批量设置公开
5.2 轮播图管理页面 — BannerManage
路径: apps/web/src/pages/health/BannerManage.tsx
布局:PageContainer + Table + Drawer
- 标题栏:标题 + "新增轮播图"按钮
- 表格列:排序 / 缩略图(40×40) / 标题 / 副标题 / 跳转(类型+目标) / 状态(Tag) / 生效时间 / 操作(编辑/删除)
- 拖拽排序:使用
@dnd-kit实现行拖拽,松手后调用PUT /banners/sort - Drawer 表单(新增/编辑):
- 图片选择:点击区域打开 MediaPicker 弹窗
- 标题、副标题文本输入
- 跳转设置:类型下拉 + 目标输入(选择"文章"时弹出文章搜索选择器)
- 生效时间:DateRangePicker
- 排序值:数字输入
- 状态开关
5.3 MediaPicker 组件
路径: apps/web/src/components/MediaPicker/index.tsx
从媒体库页面抽取的独立选图组件:
- Props:
visible: boolean— 显示/隐藏onSelect: (item: MediaItemResp) => void— 选中回调accept?: string— 文件类型限制multiple?: boolean— 是否多选
- 布局: Modal 弹窗,内部复用网格视图(不带左侧树,用下拉选择文件夹)
- 交互: 点击图片卡片选中(高亮边框),点击确认按钮触发 onSelect
- 集成上传: 弹窗内可直接上传新图片,上传后自动出现在列表中
- 使用场景:
- 轮播图管理 — 选择图片
- 文章编辑器 — 选择封面图
- 文章编辑器 — Wangeditor 插入图片时选图
5.4 ArticleEditor 增强
改造现有 ArticleEditor.tsx:
- 封面图:从当前的上传组件改为 MediaPicker 组件。点击封面图区域打开 MediaPicker,选中后回填 URL。
- 富文本插图:改造 Wangeditor 的
MENU_CONF.uploadImage:customUpload改为调用POST /media/upload(上传到媒体库并返回 media_item)- 新增
customBrowse按钮打开 MediaPicker,选中后插入<img>标签 - 插入的图片 URL 附加
?token=xxx用于认证
6. 小程序改动
6.1 访客首页轮播图
文件: apps/miniprogram/src/pages/index/index.tsx
改动:
- 删除
CAROUSEL_SLIDES硬编码常量 - 新增
useEffect在组件挂载时调用GET /public/banners - 数据存入组件 state(
banners数组) <SwiperItem>从 banners 数据渲染:- 背景:从
image_url加载<Image>组件 - 标题/副标题:从 banner.title / banner.subtitle 渲染
- 跳转:点击 SwiperItem 时根据 link_type 跳转(article → navigateTo 文章详情,external → Taro 导航)
- 背景:从
- 加载中显示骨架屏(Skeleton),加载失败不显示轮播图(降级处理)
6.2 文章列表区域
文件: apps/miniprogram/src/pages/index/index.tsx
改动:
- 删除"核心功能"区域的 3 张硬编码卡片
- 新增
useEffect调用GET /public/articles?category_id={首页推荐分类ID}&page_size=4 - 渲染文章卡片列表:
- 前 2 篇使用两列网格布局(
mp-article-grid),显示封面图 + 标题 + 摘要 - 后 2 篇使用横向列表布局(左图右文),节省空间
- 底部"查看更多"链接,跳转到文章列表页(需登录)
- 前 2 篇使用两列网格布局(
- 区域标题从"核心功能"改为"健康资讯"
- 无数据时显示空状态占位
6.3 小程序 API 服务
文件: apps/miniprogram/src/services/article.ts
新增公开 API 调用(不需要 token):
// 获取公开轮播图(无需认证)
export async function getPublicBanners(): Promise<Banner[]> {
const { data } = await client.get('/public/banners');
return data;
}
// 获取公开文章列表(无需认证)
export async function getPublicArticles(categoryId: string, pageSize = 4): Promise<ArticleListItem[]> {
const { data } = await client.get('/public/articles', { params: { category_id: categoryId, page_size: pageSize } });
return data;
}
需要在 request.ts 中增加对 /public/ 路径的跳过认证逻辑(不附加 Authorization header)。
7. 缩略图与在线编辑
7.1 缩略图生成
时机: 上传图片后,在 media_service 中自动生成。
规格:
- 尺寸:200×200px,居中裁剪(cover 模式)
- 格式:保持原格式(JPEG→JPEG, PNG→PNG)
- 质量:JPEG 85%
- 命名:
thumb_{uuid}.ext,与原文件同目录 - 库:使用
imagecrate(Rust 纯实现,无系统依赖)
流程:
- 接收上传文件 → 保存原文件
- 读取图片 → 获取原始 width/height
- 居中裁剪到正方形 → 缩放到 200×200
- 保存缩略图
- 数据库记录 storage_path + thumbnail_path + width + height
7.2 在线编辑
范围: 仅支持裁剪 + 缩放,不做旋转、滤镜、文字标注等。
实现方案:
- 前端:使用
react-image-crop库 - 后端:接收裁剪参数
{ x, y, width, height },裁剪原图并保存为新版本 - 替换策略:裁剪后覆盖原文件(保留原文件名),同时重新生成缩略图
- 不保留编辑历史(简化实现)
交互:
- 媒体库中点击"编辑"按钮
- 打开 Modal,显示原图 + 裁剪框
- 调整裁剪区域 → 确认
- 调用
PUT /media/{id}/crop,传裁剪参数 - 后端裁剪 → 覆盖原文件 → 重新生成缩略图
- 前端刷新列表
7.3 新增 API
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /media/{id}/crop |
裁剪图片(body: { x, y, width, height, version }) |
8. 权限与安全
8.1 权限码注册
在 erp-health/src/module.rs 的 ErpModule::permissions() 中注册:
("health.media.list", "查看媒体库"),
("health.media.manage", "管理媒体资源(上传/编辑/删除)"),
("health.banners.manage", "管理轮播图"),
默认分配给 admin 和 operator 角色。
8.2 安全考虑
| 风险 | 缓解措施 |
|---|---|
| 上传恶意文件 | 复用现有 Content-Type 白名单 + Magic Bytes 验证 |
| 路径遍历攻击 | 文件名使用 UUID v7,不使用用户输入的文件名 |
| 签名 URL 泄露 | 1 小时有效期 + 绑定具体文件路径 + HMAC 验证 |
| 公开资源被枚举 | UUID v7 不可预测,不暴露列表端点给未认证用户 |
| 批量删除误操作 | 前端二次确认 + 软删除可恢复 |
| 磁盘空间耗尽 | 配置 max_file_size 限制单文件大小,监控 uploads 目录 |
| 跨租户访问 | 所有查询强制 tenant_id 过滤,文件路径包含 tenant_id |
8.3 文件类型限制
上传端点仅允许图片类型:
| 类型 | MIME | Magic Bytes |
|---|---|---|
| JPEG | image/jpeg |
FF D8 FF |
| PNG | image/png |
89 50 4E 47 |
| GIF | image/gif |
GIF87a / GIF89a |
| WebP | image/webp |
RIFF....WEBP |
现有上传端点已支持文档类型(PDF/Word/Excel),媒体库上传端点在此基础上进一步限制为仅图片。
9. 关键文件清单
9.1 后端新增
| 文件 | 说明 |
|---|---|
crates/erp-health/src/entity/media_folder.rs |
文件夹实体 |
crates/erp-health/src/entity/media_item.rs |
媒体资源实体 |
crates/erp-health/src/entity/banner.rs |
轮播图实体 |
crates/erp-health/src/handler/media_handler.rs |
媒体库 HTTP 处理 |
crates/erp-health/src/handler/banner_handler.rs |
轮播图 HTTP 处理 |
crates/erp-health/src/service/media_service.rs |
媒体库业务逻辑(CRUD + 缩略图生成 + 裁剪) |
crates/erp-health/src/service/banner_service.rs |
轮播图业务逻辑(CRUD + 排序 + 定时生效) |
crates/erp-health/src/dto/media_dto.rs |
媒体库 DTO |
crates/erp-health/src/dto/banner_dto.rs |
轮播图 DTO |
| 迁移文件(3 个) | media_folder / media_item / banner 建表 |
9.2 后端修改
| 文件 | 改动 |
|---|---|
crates/erp-health/src/module.rs |
注册路由 + 权限码 |
crates/erp-health/src/entity/mod.rs |
导出新实体 |
crates/erp-health/src/handler/mod.rs |
导出新 handler |
crates/erp-health/src/service/mod.rs |
导出新 service |
crates/erp-health/src/dto/mod.rs |
导出新 DTO |
crates/erp-server/src/main.rs |
注册公开路由组(/public/) |
crates/erp-server/src/handlers/upload.rs |
upload_auth_middleware 增加签名 URL 验证 |
crates/erp-server/src/config.rs |
StorageConfig 增加 secret_key 字段 |
crates/erp-server/config/default.toml |
增加 secret_key 配置 |
9.3 前端新增
| 文件 | 说明 |
|---|---|
apps/web/src/pages/health/MediaLibrary.tsx |
媒体库页面 |
apps/web/src/pages/health/BannerManage.tsx |
轮播图管理页面 |
apps/web/src/components/MediaPicker/index.tsx |
媒体选择器组件 |
apps/web/src/api/health/media.ts |
媒体库 API 客户端 |
apps/web/src/api/health/banners.ts |
轮播图 API 客户端 |
9.4 前端修改
| 文件 | 改动 |
|---|---|
apps/web/src/pages/health/ArticleEditor.tsx |
封面图和插图改用 MediaPicker |
apps/web/src/router/ |
添加媒体库和轮播图路由 |
apps/web/src/api/upload.ts |
可选:上传后自动创建 media_item |
9.5 小程序修改
| 文件 | 改动 |
|---|---|
apps/miniprogram/src/pages/index/index.tsx |
轮播图和文章列表接入 API |
apps/miniprogram/src/services/article.ts |
新增公开 API 调用 |
apps/miniprogram/src/services/request.ts |
/public/ 路径跳过认证 |
10. 验证清单
10.1 后端验证
cargo check全 workspace 通过cargo test --workspace全部通过- 数据库迁移正反执行成功
POST /media/upload上传图片 → 返回 media_item 含缩略图GET /media列表筛选(按文件夹/类型/关键词/公开状态)PUT /media/{id}更新元数据 → 乐观锁生效DELETE /media/{id}软删除 → 文件从磁盘移除POST /media/{id}/crop裁剪 → 原文件和缩略图更新POST /banners创建轮播图 → 关联 media_itemPUT /banners/sort排序 → sort_order 更新GET /public/banners无认证 → 返回签名 URL- 签名 URL 访问文件 → 成功返回图片
- 过期签名 URL → 返回 401
- 私有资源不附加 JWT → 返回 401
10.2 管理后台验证
- 媒体库页面:文件夹树加载/新建/重命名/删除
- 媒体库页面:上传图片 → 列表显示缩略图
- 媒体库页面:搜索/筛选/分页正常
- 媒体库页面:编辑 alt_text / 公开状态 → 保存成功
- 媒体库页面:裁剪图片 → 结果正确
- 轮播图管理:新增 → 从 MediaPicker 选图 → 保存
- 轮播图管理:拖拽排序 → sort_order 更新
- 轮播图管理:设置定时上下架 → 状态正确切换
- 文章编辑器:封面图从 MediaPicker 选图
- 文章编辑器:插图从 MediaPicker 选图或直接上传
10.3 小程序验证
- 访客首页轮播图:未登录 → 动态显示后台配置的轮播图
- 访客首页轮播图:点击跳转正确(文章详情 / 外部链接)
- 访客首页文章列表:显示"健康资讯"分类下已发布文章
- 访客首页文章列表:点击"查看更多"跳转(需登录引导)
- 轮播图加载失败 → 不显示轮播区域(降级处理)
- 文章为空 → 显示空状态占位
- 关怀模式下轮播图和文章列表显示正常