feat: 更新内容管理方案
This commit is contained in:
+5
-607
@@ -1,608 +1,6 @@
|
||||
# Doodle Mini — Debug 模式内容发布方案
|
||||
# Doodle Mini - Debug 发布与内容管理方案
|
||||
|
||||
> 版本:v1.0
|
||||
> 最后更新:2026-04-23
|
||||
> 配套文档:[技术架构设计文档](./技术架构设计文档.md) | [小程序云开发方案](./小程序云开发方案.md) | [产品设计文档](./产品设计文档.md)
|
||||
|
||||
---
|
||||
|
||||
## 一、方案定位与动机
|
||||
|
||||
### 1.1 当前痛点
|
||||
|
||||
每新增一种可打印资料,从开发完成到上线需要经历以下**手动步骤**:
|
||||
|
||||
```
|
||||
开发页面 + Draw 逻辑
|
||||
→ ① 在预览中截图或导出入口图原图
|
||||
→ ② 放入 node-tools/entranceInput/<分类>/
|
||||
→ ③ 运行 processEntrancePicture.js 裁剪缩放
|
||||
→ ④ 从 entranceOuput/ 拷贝到 miniprogram/assets/entrancePicture/
|
||||
→ ⑤ 手动编辑 category.data.ts 添加元数据(title、desc、path、previewImg 等)
|
||||
→ ⑥ 提交代码 → 审核 → 发版
|
||||
```
|
||||
|
||||
其中 ①~⑤ 是重复性机械劳动,且容易出错(路径拼错、忘记更新数据文件、图片尺寸不一致等)。
|
||||
|
||||
### 1.2 核心洞察
|
||||
|
||||
**当一个页面开发调试完成时,所有需要的信息已经就绪**:
|
||||
|
||||
- **页面路径** — 就是当前正在调试的页面 URL
|
||||
- **标题 / 副标题** — 已写在绘制逻辑或配置中
|
||||
- **入口图片** — 预览 Canvas 上正在渲染的就是
|
||||
- **分类 / 难度 / 年龄段 / 标签** — 已在页面配置中定义
|
||||
|
||||
在这个时刻一键上传,是**成本最低、数据最准确**的做法。
|
||||
|
||||
### 1.3 方案目标
|
||||
|
||||
在 **develop / devtools 环境**的预览页中增加「发布到云端」按钮,点击后自动:
|
||||
|
||||
1. 从 Canvas 导出入口图 → 裁剪 → 压缩
|
||||
2. 上传图片到云存储
|
||||
3. 组装页面元数据 → 写入云数据库 `worksheets` 集合
|
||||
4. 线上小程序通过缓存策略拉取云端数据,新内容**无需发版即可上线**
|
||||
|
||||
**不需要开发独立后台系统,不需要手动录入数据。**
|
||||
|
||||
---
|
||||
|
||||
## 二、整体流程
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ Debug 模式发布流程 │
|
||||
│ │
|
||||
│ ① 开发者在 develop / devtools 环境下完成页面开发 │
|
||||
│ (绘制逻辑调试完毕,预览效果满意) │
|
||||
│ │
|
||||
│ ② 预览区底部出现 [📤 发布到云端] 按钮 │
|
||||
│ (仅 develop / devtools 环境可见,线上版本不显示) │
|
||||
│ │
|
||||
│ ③ 点击后弹出确认面板,预览即将上传的数据: │
|
||||
│ ┌─────────────────────────────────────────┐ │
|
||||
│ │ ID: letter-tracing-daily-checkin │ │
|
||||
│ │ 标题: 每日打卡 │ │
|
||||
│ │ 副标题: 四宫格每日字母打卡练习 │ │
|
||||
│ │ 分类: english │ │
|
||||
│ │ 路径: /englishPages/letterTracing/... │ │
|
||||
│ │ 难度: 1 │ │
|
||||
│ │ 年龄: 4-7岁 │ │
|
||||
│ │ 标签: [字母, 描红, 打卡] │ │
|
||||
│ │ [预览图缩略图] │ │
|
||||
│ │ 裁剪图标:头部和底部、仅头部、不裁剪 │ │
|
||||
│ │ │ │
|
||||
│ │ ─ 可编辑字段(允许微调) ─ │ │
|
||||
│ │ │ │
|
||||
│ │ 状态: ○ draft(默认) ○ active │ │
|
||||
│ │ │ │
|
||||
│ │ [取消] [确认发布] │ │
|
||||
│ └─────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ④ 确认发布后,自动执行: │
|
||||
│ a. Canvas 导出 → 裁剪页眉页脚 → 压缩至 ≤200KB │
|
||||
│ b. 上传图片到云存储 │
|
||||
│ cloud://xxx/assets/previews/<category>/<id>.jpg │
|
||||
│ c. 组装 WorksheetConfig → upsert 到云数据库 worksheets 集合 │
|
||||
│ d. 显示「发布成功 ✅」 │
|
||||
│ │
|
||||
│ ⑤ 线上小程序按缓存策略拉取新数据,内容自动出现 │
|
||||
│ (或在 debug 管理页手动将 status 从 draft 切为 active) │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、技术设计
|
||||
|
||||
### 3.1 环境门控
|
||||
|
||||
发布功能**仅在开发环境**可见,**绝不暴露给普通用户**。
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 判断当前是否为开发环境,可执行 debug 发布操作。
|
||||
* 复用 downloadPrint.ts 中 isDevBypassLimits 的判断模式。
|
||||
*/
|
||||
function isDebugPublishEnabled(): boolean {
|
||||
const accountInfo = wx.getAccountInfoSync();
|
||||
const { envVersion } = accountInfo.miniProgram;
|
||||
// develop: 开发版;trial: 体验版;release: 正式版
|
||||
return envVersion === 'develop';
|
||||
}
|
||||
```
|
||||
|
||||
WXML 中条件渲染:
|
||||
|
||||
```xml
|
||||
<!-- 仅开发环境显示发布按钮 -->
|
||||
<view wx:if="{{isDevEnv}}" class="debug-publish-bar">
|
||||
<button bind:tap="onDebugPublish">📤 发布到云端</button>
|
||||
</view>
|
||||
```
|
||||
|
||||
### 3.2 页面元数据约定
|
||||
|
||||
每个 draw 页面需提供 `getPublishMeta()` 方法,返回标准化的发布元数据。
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 发布元数据接口
|
||||
* 各 draw 页面实现此接口,提供自身的配置信息
|
||||
*/
|
||||
interface PublishMeta {
|
||||
// ─── 必填字段 ───
|
||||
id: string; // 唯一标识,如 'letter-tracing-daily-checkin'
|
||||
title: string; // 显示标题
|
||||
desc: string; // 显示描述
|
||||
category: 'math' | 'chinese' | 'english' | 'puzzle' | 'craft';
|
||||
subcategory: string; // 子分类
|
||||
path: string; // 页面完整路径(含参数)
|
||||
ageMin: number; // 适龄最小值
|
||||
ageMax: number; // 适龄最大值
|
||||
difficulty: 1 | 2 | 3 | 4; // 1-4:入门、基础、进阶、挑战
|
||||
|
||||
// ─── 选填字段(有默认值)───
|
||||
previewImg?: string; // 上传入口图后回填的云存储 fileID
|
||||
tags?: string[]; // 搜索标签
|
||||
sortOrder?: number; // 排序权重
|
||||
}
|
||||
```
|
||||
|
||||
在 `pageMixin` 中约定调用方式:
|
||||
|
||||
```typescript
|
||||
// pageMixin 中新增
|
||||
debugPublishMixin: {
|
||||
getPublishMeta(): PublishMeta {
|
||||
// 子类覆写此方法
|
||||
throw new Error('页面未实现 getPublishMeta()');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
各页面实现示例(letterTracing):
|
||||
|
||||
```typescript
|
||||
getPublishMeta(): PublishMeta {
|
||||
const drawService = this.drawService;
|
||||
return {
|
||||
id: this.data.currentId,
|
||||
title: drawService.title,
|
||||
desc: drawService.desc,
|
||||
category: 'english',
|
||||
subcategory: 'letter-tracing',
|
||||
path: `/englishPages/letterTracing/letterTracing?id=${this.data.currentId}`,
|
||||
ageMin: 4,
|
||||
ageMax: 7,
|
||||
difficulty: 1,
|
||||
tags: ['字母', '描红', '英语'],
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 入口图导出与处理
|
||||
|
||||
复用已有的 `preview-card` 组件的 `exportToTempFile` 能力,再做裁剪处理。
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 从预览 Canvas 导出入口图
|
||||
*
|
||||
* 处理流程:
|
||||
* 1. 从 preview-card 导出完整 A4 临时文件
|
||||
* 2. 用离屏 Canvas 裁剪掉页眉/页脚区域(与 processEntrancePicture.js 逻辑对齐)
|
||||
* 3. 缩放至入口图标准宽度(600px)
|
||||
* 4. 导出为 JPEG(quality: 0.85)
|
||||
*/
|
||||
async function exportEntranceImage(
|
||||
canvas: WechatMiniprogram.Canvas,
|
||||
ctx: CanvasRenderingContext2D,
|
||||
paperConfig: {
|
||||
headerHeight: number;
|
||||
footerHeight: number;
|
||||
width: number;
|
||||
height: number;
|
||||
},
|
||||
): Promise<string> {
|
||||
// 裁剪参数 — 与 node-tools/processEntrancePicture.js 保持一致
|
||||
const cropTop = paperConfig.headerHeight;
|
||||
const cropBottom = paperConfig.footerHeight;
|
||||
const sourceW = paperConfig.width;
|
||||
const sourceH = paperConfig.height - cropTop - cropBottom;
|
||||
|
||||
const ENTRANCE_WIDTH = 600;
|
||||
const scale = ENTRANCE_WIDTH / sourceW;
|
||||
const targetH = Math.round(sourceH * scale);
|
||||
|
||||
// 调整 Canvas 尺寸用于裁剪输出
|
||||
canvas.width = ENTRANCE_WIDTH;
|
||||
canvas.height = targetH;
|
||||
ctx.drawImage(
|
||||
canvas, // 自身作为源(需先 toDataURL 再 loadImage,实际实现需用临时文件中转)
|
||||
0,
|
||||
cropTop,
|
||||
sourceW,
|
||||
sourceH, // 源区域:去掉页眉页脚
|
||||
0,
|
||||
0,
|
||||
ENTRANCE_WIDTH,
|
||||
targetH, // 目标区域:缩放到标准宽度
|
||||
);
|
||||
|
||||
const tempPath = await canvasToTempFilePath(canvas, {
|
||||
fileType: 'jpg',
|
||||
quality: 0.85,
|
||||
destWidth: ENTRANCE_WIDTH,
|
||||
destHeight: targetH,
|
||||
});
|
||||
|
||||
return tempPath;
|
||||
}
|
||||
```
|
||||
|
||||
> **实际实现说明**:小程序 Canvas 不能直接自引用 `drawImage`,需要先导出为临时文件(`canvasToTempFilePath`),再用 `canvas.createImage()` 加载临时文件,然后在清空的 Canvas 上绘制裁剪区域。具体实现参考 `preview-card` 已有的 `exportToTempFile` 方法。
|
||||
|
||||
### 3.4 云端上传
|
||||
|
||||
#### 3.4.1 图片上传到云存储
|
||||
|
||||
```typescript
|
||||
async function uploadEntranceImage(
|
||||
tempFilePath: string,
|
||||
category: string,
|
||||
id: string,
|
||||
): Promise<string> {
|
||||
const cloudPath = `assets/previews/${category}/${id}.jpg`;
|
||||
const res = await wx.cloud.uploadFile({
|
||||
cloudPath,
|
||||
filePath: tempFilePath,
|
||||
});
|
||||
return res.fileID; // cloud://doodle-xxx/assets/previews/english/letter-tracing-daily-checkin.jpg
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.4.2 元数据写入云数据库
|
||||
|
||||
```typescript
|
||||
async function publishWorksheet(
|
||||
meta: PublishMeta,
|
||||
imageFileID: string,
|
||||
): Promise<void> {
|
||||
const db = wx.cloud.database();
|
||||
const collection = db.collection('worksheets');
|
||||
|
||||
const doc = {
|
||||
...meta,
|
||||
previewImage: imageFileID,
|
||||
status: meta.status || 'draft',
|
||||
publishedAt: db.serverDate(),
|
||||
updatedAt: db.serverDate(),
|
||||
version: 1,
|
||||
publishedBy: 'debug', // 标记为 debug 发布
|
||||
};
|
||||
|
||||
// upsert:如果已存在则更新,不存在则创建
|
||||
const existing = await collection.where({ id: meta.id }).get();
|
||||
if (existing.data.length > 0) {
|
||||
const oldDoc = existing.data[0];
|
||||
await collection.doc(oldDoc._id).update({
|
||||
data: {
|
||||
...doc,
|
||||
version: (oldDoc.version || 0) + 1,
|
||||
updatedAt: db.serverDate(),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await collection.add({ data: doc });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.5 发布流程整合
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Debug 发布入口 — 挂载在 pageMixin 上
|
||||
* 由预览页的「发布到云端」按钮触发
|
||||
*/
|
||||
async function onDebugPublish(this: any): Promise<void> {
|
||||
if (!isDebugPublishEnabled()) return;
|
||||
|
||||
// 1. 获取页面元数据
|
||||
const meta: PublishMeta = this.getPublishMeta();
|
||||
|
||||
// 2. 弹出确认面板(展示即将发布的数据,允许微调)
|
||||
const confirmed = await showPublishConfirmDialog(meta);
|
||||
if (!confirmed) return;
|
||||
|
||||
wx.showLoading({ title: '发布中...' });
|
||||
try {
|
||||
// 3. 导出入口图
|
||||
const tempPath = await exportEntranceImage(
|
||||
this.canvas,
|
||||
this.ctx,
|
||||
this.paperConfig,
|
||||
);
|
||||
|
||||
// 4. 上传图片到云存储
|
||||
const fileID = await uploadEntranceImage(
|
||||
tempPath,
|
||||
meta.category,
|
||||
meta.id,
|
||||
);
|
||||
|
||||
// 5. 写入云数据库
|
||||
await publishWorksheet(meta, fileID);
|
||||
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: '发布成功 ✅', icon: 'success' });
|
||||
} catch (err) {
|
||||
wx.hideLoading();
|
||||
wx.showModal({
|
||||
title: '发布失败',
|
||||
content: JSON.stringify(err),
|
||||
showCancel: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、云端数据与现有架构的衔接
|
||||
|
||||
### 4.1 数据加载优先级(保持不变)
|
||||
|
||||
与 [技术架构设计文档](./技术架构设计文档.md) 第 6.3 节、[小程序云开发方案](./小程序云开发方案.md) 第五节一致:
|
||||
|
||||
```
|
||||
优先级 1: 本地缓存(wx.Storage)
|
||||
优先级 2: 云端拉取(worksheets 集合) ← debug 发布的数据在此
|
||||
优先级 3: 前端内置兜底(category.data.ts)← 保持稳定兜底
|
||||
```
|
||||
|
||||
### 4.2 category.data.ts 的角色变化
|
||||
|
||||
| 阶段 | category.data.ts 的作用 |
|
||||
| -------------- | ------------------------------------------------- |
|
||||
| **当前** | 唯一数据源(硬编码所有题型信息) |
|
||||
| **方案实施后** | 兜底数据源 + 离线保障(云端不可用时生效) |
|
||||
| **长期** | 通过 `syncFromCloud` 脚本自动同步,保持与云端一致 |
|
||||
|
||||
### 4.3 worksheets 集合字段映射
|
||||
|
||||
debug 发布写入的字段,与 [小程序云开发方案](./小程序云开发方案.md) §3.3 `worksheets` 集合的字段**完全对齐**:
|
||||
|
||||
| PublishMeta 字段 | worksheets 集合字段 | 说明 |
|
||||
| ---------------- | ------------------- | ------------------------ |
|
||||
| `id` | `_id` | 业务唯一标识 |
|
||||
| `title` | `title` | 显示标题 |
|
||||
| `desc` | `desc` | 副标题 |
|
||||
| `category` | `category` | 所属大类 |
|
||||
| `subcategory` | `subcategory` | 子分类,当前默认为空 |
|
||||
| `path` | `path` | 小程序页面路径 |
|
||||
| `previewImg` | `previewImg` | 云存储 fileID |
|
||||
| `ageMin` | `ageMin` | 适龄最小值 |
|
||||
| `ageMax` | `ageMax` | 适龄最大值 |
|
||||
| `difficulty` | `difficulty` | 难度 1–4(入门、基础、) |
|
||||
| `tags` | `tags` | 搜索标签 |
|
||||
| `sortOrder` | `sortOrder` | 排列顺序 |
|
||||
| — | `createdAt` | 创建时间 |
|
||||
| — | `updatedAt` | 更新时间 |
|
||||
|
||||
---
|
||||
|
||||
## 五、云存储目录规划
|
||||
|
||||
与 [小程序云开发方案](./小程序云开发方案.md) §6 一致:
|
||||
|
||||
```
|
||||
cloud://doodle-xxx/
|
||||
├── assets/
|
||||
│ └── previews/ ← debug 发布的入口图存放于此
|
||||
│ ├── math/
|
||||
│ │ ├── number-find.jpg
|
||||
│ │ ├── addition-10.jpg
|
||||
│ │ └── ...
|
||||
│ ├── english/
|
||||
│ │ ├── letter-tracing-single.jpg
|
||||
│ │ ├── letter-tracing-daily-checkin.jpg
|
||||
│ │ └── ...
|
||||
│ ├── puzzle/
|
||||
│ ├── chinese/
|
||||
│ └── craft/
|
||||
```
|
||||
|
||||
命名规则:`<id>.jpg`,与 `PublishMeta.id` 一致,便于查找和管理。
|
||||
|
||||
---
|
||||
|
||||
## 六、安全与权限控制
|
||||
|
||||
### 6.1 客户端门控
|
||||
|
||||
```typescript
|
||||
// 三重保障
|
||||
const canPublish =
|
||||
isDebugPublishEnabled() && // ① envVersion === 'develop'
|
||||
isDevBypassLimits() && // ② 与下载绕过逻辑一致
|
||||
wx.getStorageSync('enableDebug'); // ③ debug 页面手动开启
|
||||
```
|
||||
|
||||
### 6.2 云端安全规则
|
||||
|
||||
在云数据库安全规则中,`worksheets` 集合限制写入权限:
|
||||
|
||||
```json
|
||||
{
|
||||
"worksheets": {
|
||||
".write": false,
|
||||
".read": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
debug 发布通过**云函数**中转,云函数内校验 `openId` 白名单:
|
||||
|
||||
```javascript
|
||||
// 云函数 debugPublish
|
||||
exports.main = async (event, context) => {
|
||||
const { OPENID } = cloud.getWXContext();
|
||||
const ADMIN_OPENIDS = ['开发者的openid'];
|
||||
|
||||
if (!ADMIN_OPENIDS.includes(OPENID)) {
|
||||
throw new Error('无权限执行此操作');
|
||||
}
|
||||
|
||||
// 执行 upsert 操作...
|
||||
};
|
||||
```
|
||||
|
||||
### 6.3 数据保护
|
||||
|
||||
| 措施 | 说明 |
|
||||
| ---------------- | --------------------------------------------------- |
|
||||
| 默认 draft 状态 | 发布后默认 `status: 'draft'`,需手动激活为 `active` |
|
||||
| 版本递增 | 每次更新 `version + 1`,可追踪变更历史 |
|
||||
| publishedBy 标记 | `publishedBy: 'debug'` 区分来源 |
|
||||
| 时间戳 | `publishedAt` / `updatedAt` 记录操作时间 |
|
||||
|
||||
---
|
||||
|
||||
## 七、辅助工具
|
||||
|
||||
### 7.1 syncFromCloud 脚本
|
||||
|
||||
在 `node-tools/` 中新增脚本,发版前从云数据库拉取最新数据,同步回 `category.data.ts` 作为兜底数据。
|
||||
|
||||
```
|
||||
Debug 发布 → 云端数据 → 线上可见
|
||||
↓
|
||||
syncFromCloud.js ← 发版前运行
|
||||
↓
|
||||
category.data.ts 更新 ← 兜底数据同步
|
||||
↓
|
||||
下次发版包含
|
||||
```
|
||||
|
||||
```javascript
|
||||
// node-tools/src/syncFromCloud.js(伪代码)
|
||||
// 通过云开发 HTTP API 或管理端 SDK 拉取 worksheets 集合
|
||||
// 按 category 分组 → 生成 TypeScript 代码 → 写入 category.data.ts
|
||||
```
|
||||
|
||||
### 7.2 Debug 管理页扩展
|
||||
|
||||
在已有的 `supportPages/debug/debug` 页面中增加 tab,提供简易内容管理:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Debug 工具页 │
|
||||
│ │
|
||||
│ [调试配置] [已发布内容] [系统信息] │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────┐ │
|
||||
│ │ 已发布内容列表 │ │
|
||||
│ │ │ │
|
||||
│ │ ● letter-tracing-single │ │
|
||||
│ │ 状态: active 版本: 3 英语 │ │
|
||||
│ │ [编辑] [下架] │ │
|
||||
│ │ │ │
|
||||
│ │ ● letter-tracing-daily-checkin │ │
|
||||
│ │ 状态: draft 版本: 1 英语 │ │
|
||||
│ │ [激活] [编辑] [删除] │ │
|
||||
│ │ │ │
|
||||
│ │ ● addition-10 │ │
|
||||
│ │ 状态: active 版本: 5 数学 │ │
|
||||
│ │ [编辑] [下架] │ │
|
||||
│ └─────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、实施计划
|
||||
|
||||
### Phase 1:基础发布能力(约 0.5~1 天)
|
||||
|
||||
- [ ] `pageMixin` 中新增 `getPublishMeta()` 接口约定
|
||||
- [ ] 实现 `isDebugPublishEnabled()` 环境判断
|
||||
- [ ] 实现入口图导出 + 裁剪逻辑
|
||||
- [ ] 实现云存储上传 + 云数据库写入
|
||||
- [ ] 在 `preview-card` 或 `preview-footer-actions` 中添加发布按钮(条件渲染)
|
||||
|
||||
### Phase 2:确认面板与安全(约 0.5 天)
|
||||
|
||||
- [ ] 发布前确认弹窗(数据预览 + 可编辑字段)
|
||||
- [ ] 云函数安全校验(openId 白名单)
|
||||
- [ ] 默认 draft 状态 + 版本管理
|
||||
|
||||
### Phase 3:数据消费侧适配(约 0.5~1 天)
|
||||
|
||||
- [ ] `category.ts`(或 `worksheet-service`)增加从云 DB 拉取逻辑
|
||||
- [ ] 实现缓存策略(本地缓存 → 云端 → 内置兜底)
|
||||
- [ ] 入口图从云存储 fileID 获取临时 URL 展示
|
||||
|
||||
### Phase 4:辅助工具(约 0.5 天)
|
||||
|
||||
- [ ] `syncFromCloud.js` 脚本
|
||||
- [ ] Debug 管理页扩展(列表 + 状态管理)
|
||||
|
||||
**总计预估:2~3 天**
|
||||
|
||||
---
|
||||
|
||||
## 九、与模板引擎的衔接(远期)
|
||||
|
||||
当 [技术架构设计文档](./技术架构设计文档.md) 第五章所述模板引擎落地后,debug 发布方案可进一步升级:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 模板引擎 + Debug 发布(远期形态) │
|
||||
│ │
|
||||
│ ① 在通用 worksheet 页面中: │
|
||||
│ 选择模板 + 配置生成器参数 → 实时预览 │
|
||||
│ │
|
||||
│ ② 效果满意后点击「发布」: │
|
||||
│ 自动上传: │
|
||||
│ • JSON 配置(template + generator + generatorConfig + layout)│
|
||||
│ • 入口预览图 │
|
||||
│ • 元数据(title、tags、ageRange 等) │
|
||||
│ │
|
||||
│ ③ 新题型无需任何前端代码改动即可上线 │
|
||||
│ (前提:使用已有的 TemplateRenderer + Generator 组合) │
|
||||
│ │
|
||||
│ ④ 配合 80% 新题型可动态上线的目标 │
|
||||
│ debug 发布成为主要的内容上线方式 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 十、风险与应对
|
||||
|
||||
| 风险 | 影响 | 应对 |
|
||||
| ---------------------- | ----------------------- | --------------------------------------------------------- |
|
||||
| **误操作发布测试数据** | 污染线上列表 | 默认 `status: 'draft'`;确认面板二次确认 |
|
||||
| **入口图质量不一致** | 不同设备/DPR 下渲染差异 | 统一使用开发者工具发布;导出时固定 `destWidth/destHeight` |
|
||||
| **云端数据丢失** | 已发布内容消失 | `category.data.ts` 兜底;`syncFromCloud` 定期同步 |
|
||||
| **安全:非授权上传** | 恶意写入数据 | 云函数白名单校验;客户端三重门控 |
|
||||
| **双数据源不一致** | 本地兜底与云端数据冲突 | 云端优先,发版前 `syncFromCloud` 对齐 |
|
||||
| **云存储额度** | 图片累积占用存储 | 入口图约 50~200KB/张,100 张仅 ~20MB,远低于免费额度 |
|
||||
|
||||
---
|
||||
|
||||
## 附录 A:与现有代码的关系
|
||||
|
||||
| 现有模块 | 本方案的关联 |
|
||||
| ------------------------------------------ | ------------------------------------------------------ |
|
||||
| `preview-card` 组件 | 复用 `exportToTempFile`,新增裁剪逻辑 |
|
||||
| `pageMixin.ts` | 新增 `getPublishMeta()` 约定和 `onDebugPublish()` 方法 |
|
||||
| `downloadPrint.ts` / `isDevBypassLimits()` | 复用环境判断模式 |
|
||||
| `category.data.ts` | 角色从「唯一数据源」变为「兜底数据源」 |
|
||||
| `supportPages/debug/debug` | 扩展内容管理 tab |
|
||||
| `node-tools/processEntrancePicture.js` | 裁剪参数对齐;流程被 debug 发布替代 |
|
||||
| 云数据库 `worksheets` 集合 | 写入发布数据(字段与云开发方案对齐) |
|
||||
| 云存储 `assets/previews/` | 存放入口图 |
|
||||
> **本文档已拆分,请查看:**
|
||||
>
|
||||
> - [Worksheet 发布方案](./Worksheet发布方案.md) — worksheet 的 Debug 发布流程、图片处理、入库字段
|
||||
> - [页面内容管理方案](./页面内容管理方案.md) — 首页 / 分类页 / 分龄页的内容配置、数据预拉取、管理方案
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
# Worksheet 发布方案
|
||||
|
||||
> 版本:v1.0 | 最后更新:2026-04-29
|
||||
> 配套文档:[页面内容管理方案](./页面内容管理方案.md) | [小程序云开发方案](./小程序云开发方案.md)
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
在小程序**开发版**的 worksheet 绘制页面中,开发者完成调试后可直接将当前 worksheet 发布为云端内容。发布能力只在 `develop` 环境可见,正式版不展示入口。
|
||||
|
||||
**核心链路:**
|
||||
|
||||
```
|
||||
预览组件导出 A4 图 → 裁剪压缩为入口图 → 上传云存储 → 云函数 upsert worksheets → draft 入库
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 已接入页面
|
||||
|
||||
| 页面 | 元数据配置 | category | path 示例 |
|
||||
|------|-----------|----------|-----------|
|
||||
| 数学 worksheet | `mathPages/mathDraw/mathDraw.config.ts` | `math` | `/mathPages/mathDraw/mathDraw?id=number-find` |
|
||||
| 专注力 / 益智 | `focusPages/focusDraw/focusDraw.config.ts` | `puzzle` | `/focusPages/focusDraw/focusDraw?id=dot-connect` |
|
||||
| 英语字母描红 | `englishPages/letterTracing/letterTracing.config.ts` | `english` | `/englishPages/letterTracing/letterTracing?id=letter-tracing-single` |
|
||||
|
||||
> 新增 worksheet 页面只需复用 `pageMixin` 并实现 `getPublishMeta()` 即可接入。
|
||||
|
||||
---
|
||||
|
||||
## 发布流程
|
||||
|
||||
流程在 `miniprogram/base/pageMixin.ts` 中完成:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 开发版页面 │
|
||||
│ │ │
|
||||
│ ├─ syncDebugPublishEnv 判断 envVersion === develop │
|
||||
│ ├─ 点击 debug-publish-tools 悬浮发布按钮 │
|
||||
│ ├─ getPublishMeta() 读取当前 worksheet 元数据 │
|
||||
│ ├─ 弹窗微调 title / subtitle / tags / status / 图片参数 │
|
||||
│ ├─ preview-card.exportToTempFile() 导出预览图 │
|
||||
│ ├─ debug-publish-tools.processImage() 裁剪压缩入口图 │
|
||||
│ ├─ wx.cloud.uploadFile → assets/previews/<category>/<id>.jpg │
|
||||
│ └─ wx.cloud.callFunction('worksheetsPublish') upsert │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 图片处理规则
|
||||
|
||||
处理由 `components3.0/debug-publish-tools` 完成,参数定义在 `utils/debugPublish.ts`。
|
||||
|
||||
### 默认参数
|
||||
|
||||
| 参数 | 默认值 | 说明 |
|
||||
|------|--------|------|
|
||||
| `cropMode` | `header-footer` | 裁掉页眉页脚,保留主体内容 |
|
||||
| `width` | `600` | 输出入口图宽度(px) |
|
||||
| `quality` | `90` | 初始 JPEG 质量 |
|
||||
| `maxSizeKB` | `200` | 超出后自动降低质量压缩 |
|
||||
| 云存储路径 | `assets/previews/<category>/<id>.jpg` | 由 `buildWorksheetPreviewCloudPath` 生成 |
|
||||
|
||||
### 裁剪模式
|
||||
|
||||
| 模式 | 用途 |
|
||||
|------|------|
|
||||
| `header-footer` | **默认** — 只保留 worksheet 主体内容 |
|
||||
| `header-only` | 保留底部品牌区 |
|
||||
| `none` | 完整 A4 预览图压缩上传 |
|
||||
|
||||
---
|
||||
|
||||
## 入库字段
|
||||
|
||||
`DebugPublishMeta` 与 `worksheetsPublish` 云函数字段已对齐:
|
||||
|
||||
| 字段 | 来源 | 说明 |
|
||||
|------|------|------|
|
||||
| `_id` / `id` | 页面元数据 | worksheet 业务唯一 ID |
|
||||
| `title` | 页面元数据(弹窗可改) | 展示标题 |
|
||||
| `subtitle` | 页面元数据(弹窗可改) | 展示副标题 |
|
||||
| `category` | 页面元数据 | 必须存在于 `categories` 集合 |
|
||||
| `subcategory` | 页面元数据 | 如 `math-draw`、`focus-draw`、`letter-tracing` |
|
||||
| `path` | 页面元数据 | 小程序跳转路径 |
|
||||
| `previewImg` | 云存储上传结果 | 入口图 fileID |
|
||||
| `ageMin` / `ageMax` | 页面元数据 | 适龄范围 |
|
||||
| `grade` | `inferGradeFromAge` 推断 | 年级映射 |
|
||||
| `difficulty` | 页面元数据 | 1=入门 2=基础 3=进阶 4=挑战 |
|
||||
| `tags` | 页面元数据(弹窗可改) | 搜索和运营标签 |
|
||||
| `isNew` / `isHot` | 页面元数据 | 首页和推荐位复用 |
|
||||
| `sortOrder` | 页面元数据 | 列表排序权重 |
|
||||
| `downloads` / `likes` | 发布 payload 或默认值 | 运营统计展示 |
|
||||
| `status` | 弹窗选择(默认 `draft`) | `draft` / `active` / `hidden` |
|
||||
| `createdAt` / `updatedAt` | 云函数 | 创建和更新时间 |
|
||||
|
||||
> `worksheetsPublish` 是 **upsert** 语义:同 ID 已存在时更新,不存在时创建。
|
||||
|
||||
---
|
||||
|
||||
## 前置条件
|
||||
|
||||
发布前需确保:
|
||||
|
||||
1. 云开发环境已初始化
|
||||
2. `categories` 集合中存在对应大类(`math`、`puzzle`、`english` 等)
|
||||
3. 当前页面实现了 `getPublishMeta()`
|
||||
4. 页面 WXML 已挂载 `debug-publish-tools` 组件
|
||||
|
||||
> 如果 `categories` 中没有对应分类,`worksheetsPublish` 会返回"分类不合法"。
|
||||
|
||||
---
|
||||
|
||||
## 关键文件索引
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `miniprogram/base/pageMixin.ts` | 发布流程主逻辑 |
|
||||
| `miniprogram/components3.0/debug-publish-tools/` | 发布 UI 组件、图片处理 |
|
||||
| `miniprogram/utils/debugPublish.ts` | 图片参数、云路径生成 |
|
||||
| `cloudfunctions/worksheetsPublish/` | upsert `worksheets` 集合 |
|
||||
+1
-1
@@ -84,7 +84,7 @@
|
||||
| `sortOrder` | int | 是 | 排序权重,默认 `0`。 |
|
||||
| `downloads` | int | 是 | 下载次数,默认 `0`。 |
|
||||
| `likes` | int | 是 | 收藏此时,默认 `0`。 |
|
||||
| `status` | string | 是 | `active` \| `draft` \| `hidden`,默认 `draft`。 |
|
||||
| `status` | string | 是 | `active` \| `draft`,默认 `draft`。 |
|
||||
| `createdAt` | date | 是 | 创建时间。 |
|
||||
| `updatedAt` | date | 是 | 更新时间。 |
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# 推广方案
|
||||
|
||||
## 分享激励制度
|
||||
|
||||
用户每下载三次需要看广告,但分享成功一次(被分享者打开并下载一次)可以获得 10 次免广告下载次数,且可以累加。
|
||||
@@ -0,0 +1,454 @@
|
||||
# 页面内容管理方案
|
||||
|
||||
> 版本:v1.0 | 最后更新:2026-04-29
|
||||
> 配套文档:[Worksheet 发布方案](./Worksheet发布方案.md) | [小程序云开发方案](./小程序云开发方案.md)
|
||||
|
||||
---
|
||||
|
||||
## 一、背景与目标
|
||||
|
||||
worksheet 已可通过 Debug 发布进入云数据库,但三个展示页仍使用本地静态数据:
|
||||
|
||||
|
||||
| 页面 | 当前数据来源 | 问题 |
|
||||
| --- | --------------------------------- | ---------------------- |
|
||||
| 首页 | `pages/home/home.data.ts` | featured、hot、分区靠代码手动维护 |
|
||||
| 分类页 | `pages/category/category.data.ts` | 新 worksheet 发布后不会自动出现 |
|
||||
| 分龄页 | `pages/age/age.ts` 内 Mock | 推荐内容和周路线无法运营配置 |
|
||||
|
||||
|
||||
**目标:** 通过 Debug 管理页配置三个页面的内容,生成统一 JSON 上传云存储,小程序启动时通过**数据预拉取**获取并渲染。
|
||||
|
||||
---
|
||||
|
||||
## 二、整体架构
|
||||
|
||||
### 2.1 数据流
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ Debug 内容管理页(三个独立 tab) │
|
||||
│ │
|
||||
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
|
||||
│ │ 首页管理 │ │ 分类页管理 │ │ 分龄页管理 │ │
|
||||
│ └────┬────┘ └────┬────┘ └────┬────┘ │
|
||||
│ │ │ │ │
|
||||
│ └──────┬──────┘──────┬──────┘ │
|
||||
│ ▼ ▼ │
|
||||
│ 点击「更新」按钮 → 调用云函数 pageContentUpdate │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ 更新云存储 content/page-config.json 中对应页面的配置 │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
|
||||
▼
|
||||
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ 小程序启动 │
|
||||
│ │
|
||||
│ app.onLaunch │
|
||||
│ └─ 数据预拉取 → 获取 content/page-config.json │
|
||||
│ │
|
||||
│ 页面 onLoad │
|
||||
│ ├─ 1. 使用本地 *.data.ts / age.config.ts 渲染兜底首屏 │
|
||||
│ ├─ 2. 读取预拉取结果中对应页面的配置 │
|
||||
│ └─ 3. 成功则 setData 更新页面 │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 统一 JSON 格式
|
||||
|
||||
云存储路径:`content/page-config.json`
|
||||
|
||||
```ts
|
||||
// content/page-config.json 结构
|
||||
type PageConfig = {
|
||||
home: HomePageData; // 首页配置
|
||||
category: CategoryPageData; // 分类页配置
|
||||
age: AgePageData; // 分龄页配置
|
||||
version: number; // 版本号,每次更新递增
|
||||
updatedAt: string; // 最后更新时间
|
||||
};
|
||||
```
|
||||
|
||||
每个管理页更新时,只修改 JSON 中自己负责的字段,通过云函数读取当前 JSON → 合并更新 → 写回云存储。
|
||||
|
||||
### 2.3 页面加载策略
|
||||
|
||||
三个页面统一采用**数据预拉取 + 本地兜底**的加载策略:
|
||||
|
||||
```
|
||||
页面 onLoad
|
||||
│
|
||||
├─ Step 1: 使用本地静态数据渲染首屏(零延迟)
|
||||
│ 首页 → home.data.ts
|
||||
│ 分类页 → category.data.ts
|
||||
│ 分龄页 → age.config.ts
|
||||
│
|
||||
├─ Step 2: 读取数据预拉取结果
|
||||
│ wx.getBackgroundFetchData('pre') 获取 page-config.json
|
||||
│ 取出当前页面对应的配置(home / category / age)
|
||||
│
|
||||
└─ Step 3: 成功 → setData 更新页面内容
|
||||
失败 → 保持本地兜底数据,用户无感知
|
||||
```
|
||||
|
||||
> **数据预拉取**在 `app.json` 中配置 `fetchDataUrl`,小程序冷启动时由微信客户端自动发起,不占用页面加载时间。预拉取的数据指向云存储中的 `content/page-config.json`。
|
||||
|
||||
### 2.4 云函数设计
|
||||
|
||||
新增一个统一云函数 `pageContentUpdate`:
|
||||
|
||||
```ts
|
||||
// cloudfunctions/pageContentUpdate/index.js
|
||||
type PageContentUpdatePayload = {
|
||||
page: 'home' | 'category' | 'age'; // 要更新的页面
|
||||
data: HomePageData | CategoryPageData | AgePageData; // 该页面的完整配置
|
||||
};
|
||||
```
|
||||
|
||||
执行逻辑:
|
||||
|
||||
```
|
||||
接收参数 { page, data }
|
||||
→ 从云存储读取当前 content/page-config.json
|
||||
→ 合并:config[page] = data
|
||||
→ config.version++
|
||||
→ config.updatedAt = new Date()
|
||||
→ 写回云存储 content/page-config.json
|
||||
→ 返回 { success, version }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、首页内容管理
|
||||
|
||||
### 3.1 管理 UI
|
||||
|
||||
首页管理 tab 分为四个区块:
|
||||
|
||||
|
||||
| 区块 | 对应字段 | 管理方式 |
|
||||
| ------ | -------------- | ----------------------------------- |
|
||||
| 分类 tab | `categoryTabs` | 跟随 `CATEGORY_LIST_WITH_ALL`,一般不单独管理 |
|
||||
| 年龄入口 | `ageBands` | 跟随 `AGE_BANDS`,可管理描述文案 |
|
||||
| 今日推荐 | `featured` | 手动选择 3-5 个 active worksheet |
|
||||
| 热门推荐 | `hot` | 手动选择或按 downloads 自动生成 |
|
||||
| 分类分区 | `sections` | 每个分类选择若干 active worksheet,支持排序 |
|
||||
|
||||
|
||||
交互流程:
|
||||
|
||||
```
|
||||
首页 tab 展示当前配置
|
||||
→ 每个位置点击「选择内容」
|
||||
→ 弹出 worksheet 选择器(筛选 active 内容)
|
||||
→ 保存配置
|
||||
→ 点击「更新首页数据」
|
||||
→ 调用 pageContentUpdate({ page: 'home', data: ... })
|
||||
```
|
||||
|
||||
### 3.2 数据结构
|
||||
|
||||
```ts
|
||||
type HomePageData = {
|
||||
searchPlaceholder: string;
|
||||
categoryTabs: Array<{ id: string; name: string; path?: string }>;
|
||||
ageBands: Array<{ key: string; label: string; desc: string; path: string }>;
|
||||
featured: HomeDisplayItem[];
|
||||
hot: HomeDisplayItem[];
|
||||
sections: HomeDisplaySection[];
|
||||
};
|
||||
```
|
||||
|
||||
自动生成规则:
|
||||
|
||||
|
||||
| 模块 | 默认规则 |
|
||||
| -------------- | ---------------------------------------------- |
|
||||
| `categoryTabs` | 由 `CATEGORY_LIST_WITH_ALL` 生成 |
|
||||
| `ageBands` | 由 `AGE_BANDS` 生成 |
|
||||
| `featured` | 优先使用手动选择,不足时补 `isNew == true` |
|
||||
| `hot` | 优先使用手动选择,不足时补 `isHot == true` 或 downloads 高的内容 |
|
||||
| `sections` | 使用配置中的 worksheetIds,不足时从该分类 active 内容补齐 |
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 四、分类页内容管理
|
||||
|
||||
### 4.1 管理 UI
|
||||
|
||||
分类页管理 tab 顶部展示分类选择器(来源 `CATEGORY_LIST`),默认选中 `math`。
|
||||
|
||||
分类头部统计:
|
||||
|
||||
|
||||
| 指标 | 查询规则 |
|
||||
| --- | ----------------------------- |
|
||||
| 总数 | `worksheets.category == 当前分类` |
|
||||
| 草稿 | `status == draft` |
|
||||
| 线上 | `status == active` |
|
||||
| 已隐藏 | `status == hidden` |
|
||||
|
||||
|
||||
列表展示当前分类下所有 worksheet(按 `status` → `sortOrder` → `updatedAt` 排序),每项显示预览图、标题、ID、年龄/难度/标签、状态、排序权重。
|
||||
|
||||
操作按钮:
|
||||
|
||||
|
||||
| 当前状态 | 可执行操作 |
|
||||
| -------- | --------------- |
|
||||
| `draft` | 激活上线 |
|
||||
| `active` | 下架为 hidden |
|
||||
| `hidden` | 恢复为 draft 或直接激活 |
|
||||
| 任意 | 调整排序、预览跳转、刷新 |
|
||||
|
||||
|
||||
### 4.2 数据结构
|
||||
|
||||
```ts
|
||||
type CategoryPageData = {
|
||||
searchPlaceholder: string;
|
||||
categories: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
items: CategoryItem[];
|
||||
}>;
|
||||
};
|
||||
```
|
||||
|
||||
生成时从 `worksheets` 集合查询 `status == active` 的内容,按分类分组、按 `sortOrder` 排序,转换为 `CategoryItem`。
|
||||
|
||||
### 4.3 更新流程
|
||||
|
||||
```
|
||||
分类页管理 tab → 管理 worksheet 状态和排序
|
||||
→ 点击「更新分类数据」
|
||||
→ 查询所有 active worksheets,按 category 分组
|
||||
→ 调用 pageContentUpdate({ page: 'category', data: ... })
|
||||
→ 云函数更新 page-config.json 中的 category 字段
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、分龄页内容管理
|
||||
|
||||
### 5.1 新增 `age.config.ts`
|
||||
|
||||
在 `miniprogram/pages/age/` 下新增 `age.config.ts`,将年龄段划分、能力目标、默认 worksheet 写死在配置中,作为兜底数据和管理页的基础结构。
|
||||
|
||||
```ts
|
||||
// miniprogram/pages/age/age.config.ts
|
||||
|
||||
import { AGE_BANDS, type AgeBandKey } from '../../core/data/difficulty';
|
||||
|
||||
export type AbilityItem = {
|
||||
icon: string;
|
||||
title: string;
|
||||
desc: string;
|
||||
};
|
||||
|
||||
export type WeekPlan = {
|
||||
week: number;
|
||||
theme: string;
|
||||
worksheetIds: string[]; // worksheet 业务 ID
|
||||
worksheetTitles: string[]; // 兜底展示标题
|
||||
};
|
||||
|
||||
export type AgeBandConfig = {
|
||||
key: AgeBandKey;
|
||||
label: string;
|
||||
subLabel: string;
|
||||
abilities: AbilityItem[];
|
||||
weeks: WeekPlan[];
|
||||
};
|
||||
|
||||
export const AGE_CONFIG: AgeBandConfig[] = [
|
||||
{
|
||||
key: '3-4',
|
||||
label: '3-4 岁',
|
||||
subLabel: '启蒙认知',
|
||||
abilities: [
|
||||
{ icon: '🔢', title: '数感', desc: '认读 1-5、点数对应' },
|
||||
{ icon: '✏️', title: '书写', desc: '涂鸦线条、简单描红' },
|
||||
{ icon: '🧩', title: '思维', desc: '找相同、简单配对' },
|
||||
],
|
||||
weeks: [
|
||||
{ week: 1, theme: '数感启蒙',
|
||||
worksheetIds: [], worksheetTitles: ['找数字涂一涂', '5以内加法', '数数连一连'] },
|
||||
{ week: 2, theme: '形状与连线',
|
||||
worksheetIds: [], worksheetTitles: ['识别形状', '线条识别', '数字点连线'] },
|
||||
{ week: 3, theme: '趣味专注',
|
||||
worksheetIds: [], worksheetTitles: ['颜色找规律', '方格推理', '格子仿画'] },
|
||||
{ week: 4, theme: '综合练习',
|
||||
worksheetIds: [], worksheetTitles: ['10以内加减法', '识字卡', '连连看'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: '4-5',
|
||||
label: '4-5 岁',
|
||||
subLabel: '基础练习',
|
||||
abilities: [
|
||||
{ icon: '🔢', title: '数感', desc: '10 以内数数、比大小' },
|
||||
{ icon: '✏️', title: '书写', desc: '笔画模仿、图形描边' },
|
||||
{ icon: '🧩', title: '思维', desc: '规律排序、图形分类' },
|
||||
],
|
||||
weeks: [
|
||||
{ week: 1, theme: '数感启蒙',
|
||||
worksheetIds: [], worksheetTitles: ['找数字涂一涂', '5以内加法', '数数连一连'] },
|
||||
{ week: 2, theme: '形状与连线',
|
||||
worksheetIds: [], worksheetTitles: ['识别形状', '线条识别', '数字点连线'] },
|
||||
{ week: 3, theme: '趣味专注',
|
||||
worksheetIds: [], worksheetTitles: ['颜色找规律', '方格推理', '格子仿画'] },
|
||||
{ week: 4, theme: '综合练习',
|
||||
worksheetIds: [], worksheetTitles: ['10以内加减法', '识字卡', '连连看'] },
|
||||
],
|
||||
},
|
||||
// 5-6、6-7、7-8 结构相同,abilities 和 weeks 内容不同
|
||||
// ... 完整配置见 age.config.ts 文件
|
||||
];
|
||||
```
|
||||
|
||||
> `worksheetIds` 初始为空数组,由分龄管理页选择后填入。`worksheetTitles` 作为兜底展示。
|
||||
|
||||
### 5.2 管理 UI
|
||||
|
||||
分龄管理 tab 按年龄段切换:`3-4 岁 | 4-5 岁 | 5-6 岁 | 6-7 岁 | 7-8 岁`
|
||||
|
||||
每个年龄段下的管理内容:
|
||||
|
||||
|
||||
| 模块 | 操作 | 说明 |
|
||||
| ---- | ------------ | -------------------------------- |
|
||||
| 能力目标 | 只读展示 | 来自 `age.config.ts`,不需要在管理页修改 |
|
||||
| 四周路线 | 选择 worksheet | 每周选择 4 个 worksheet,主题来自 config |
|
||||
| 推荐内容 | 选择 worksheet | 动态数据,展示 likes 数量最高的 6 个worksheet |
|
||||
|
||||
|
||||
worksheet 选择器默认筛选条件:
|
||||
|
||||
```
|
||||
status == active
|
||||
ageMin <= 当前年龄段 maxAge
|
||||
ageMax >= 当前年龄段 minAge
|
||||
```
|
||||
|
||||
**核心简化:** 分龄管理页不需要配置能力目标和周主题(这些写死在 `age.config.ts` 中),只需要为不同年龄段、不同周选择对应的 worksheet。
|
||||
|
||||
### 5.3 数据结构
|
||||
|
||||
```ts
|
||||
type AgePageData = {
|
||||
ageTabs: Array<{
|
||||
key: AgeBandKey;
|
||||
rangeText: string;
|
||||
subLabel: string;
|
||||
}>;
|
||||
bands: Record<AgeBandKey, {
|
||||
goalTitle: string;
|
||||
abilityItems: AbilityItem[];
|
||||
weekPlans: Array<{
|
||||
week: number;
|
||||
theme: string;
|
||||
exercises: Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
path: string;
|
||||
previewImg?: string;
|
||||
}>;
|
||||
}>;
|
||||
recommendedItems: Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
image?: string;
|
||||
path: string;
|
||||
}>;
|
||||
}>;
|
||||
};
|
||||
```
|
||||
|
||||
### 5.4 更新流程
|
||||
|
||||
```
|
||||
分龄管理 tab → 选择年龄段
|
||||
→ 为每周选择 worksheet
|
||||
→ 选择推荐内容
|
||||
→ 点击「更新分龄数据」
|
||||
→ 合并 age.config.ts 的能力目标 + 管理页选择的 worksheet 详情
|
||||
→ 调用 pageContentUpdate({ page: 'age', data: ... })
|
||||
→ 云函数更新 page-config.json 中的 age 字段
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、Debug 页入口结构
|
||||
|
||||
```
|
||||
supportPages/debug/debug
|
||||
├─ 分类基础数据管理 (已有:同步 CATEGORY_LIST → categories)
|
||||
├─ Worksheet 内容池 (已有:状态、分类、上下架、预览)
|
||||
└─ 页面内容管理 (新增)
|
||||
├─ Tab: 首页管理 → 编排 featured / hot / sections
|
||||
├─ Tab: 分类页管理 → 管理 worksheet 状态和排序
|
||||
└─ Tab: 分龄页管理 → 按年龄段选择周 worksheet
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、实施计划
|
||||
|
||||
### Phase 1:基础设施
|
||||
|
||||
1. 配置 `app.json` 数据预拉取,指向 `content/page-config.json`
|
||||
2. 实现 `pageContentUpdate` 云函数
|
||||
3. 上传初始 `page-config.json`(从现有 `*.data.ts` 转换)
|
||||
4. 三个页面统一接入预拉取加载逻辑
|
||||
|
||||
### Phase 2:分类页管理
|
||||
|
||||
1. 实现分类页管理 tab(worksheet 列表、状态管理、排序)
|
||||
2. 实现「更新分类数据」→ 生成 category 配置 → 调用云函数更新
|
||||
|
||||
### Phase 3:首页管理
|
||||
|
||||
1. 实现首页管理 tab(featured / hot / sections 编排)
|
||||
2. 实现 worksheet 选择器弹窗
|
||||
3. 实现「更新首页数据」→ 生成 home 配置 → 调用云函数更新
|
||||
|
||||
### Phase 4:分龄页管理
|
||||
|
||||
1. 新增 `age.config.ts` 配置文件
|
||||
2. 实现分龄管理 tab(年龄段切换、周 worksheet 选择)
|
||||
3. 实现「更新分龄数据」→ 合并 config + worksheet 详情 → 调用云函数更新
|
||||
|
||||
---
|
||||
|
||||
## 八、风险与约束
|
||||
|
||||
|
||||
| 风险 | 处理方式 |
|
||||
| ----------------- | ----------------------------------------- |
|
||||
| 预拉取失败 | 本地 `*.data.ts` / `age.config.ts` 兜底,用户无感知 |
|
||||
| JSON 生成失败 | 云函数返回错误,管理页提示重试,线上数据不受影响 |
|
||||
| 首页配置引用了 hidden 内容 | 生成时只允许 active 内容,不足时返回警告 |
|
||||
| 分龄内容不适龄 | 选择器按年龄段默认过滤 |
|
||||
| 并发更新冲突 | 云函数使用 version 乐观锁,冲突时提示重新加载后重试 |
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 九、关键文件索引
|
||||
|
||||
|
||||
| 文件 / 模块 | 职责 |
|
||||
| ----------------------------------- | ------------------------------ |
|
||||
| `supportPages/contentManage/` | 三 tab 内容管理页 |
|
||||
| `cloudfunctions/pageContentUpdate/` | 更新 `page-config.json` 中指定页面的配置 |
|
||||
| `pages/age/age.config.ts` | 分龄页兜底配置(年龄段、能力目标、默认 worksheet) |
|
||||
| `pages/home/home.data.ts` | 首页兜底数据 |
|
||||
| `pages/category/category.data.ts` | 分类页兜底数据 |
|
||||
| `content/page-config.json`(云存储) | 三个页面的统一配置 JSON |
|
||||
|
||||
|
||||
Reference in New Issue
Block a user