feat: 更新内容管理方案
@@ -1,608 +1,6 @@
|
|||||||
# Doodle Mini — Debug 模式内容发布方案
|
# Doodle Mini - Debug 发布与内容管理方案
|
||||||
|
|
||||||
> 版本:v1.0
|
> **本文档已拆分,请查看:**
|
||||||
> 最后更新:2026-04-23
|
>
|
||||||
> 配套文档:[技术架构设计文档](./技术架构设计文档.md) | [小程序云开发方案](./小程序云开发方案.md) | [产品设计文档](./产品设计文档.md)
|
> - [Worksheet 发布方案](./Worksheet发布方案.md) — worksheet 的 Debug 发布流程、图片处理、入库字段
|
||||||
|
> - [页面内容管理方案](./页面内容管理方案.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/` | 存放入口图 |
|
|
||||||
|
|||||||
@@ -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` 集合 |
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
| `sortOrder` | int | 是 | 排序权重,默认 `0`。 |
|
| `sortOrder` | int | 是 | 排序权重,默认 `0`。 |
|
||||||
| `downloads` | int | 是 | 下载次数,默认 `0`。 |
|
| `downloads` | int | 是 | 下载次数,默认 `0`。 |
|
||||||
| `likes` | int | 是 | 收藏此时,默认 `0`。 |
|
| `likes` | int | 是 | 收藏此时,默认 `0`。 |
|
||||||
| `status` | string | 是 | `active` \| `draft` \| `hidden`,默认 `draft`。 |
|
| `status` | string | 是 | `active` \| `draft`,默认 `draft`。 |
|
||||||
| `createdAt` | date | 是 | 创建时间。 |
|
| `createdAt` | date | 是 | 创建时间。 |
|
||||||
| `updatedAt` | 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 |
|
||||||
|
|
||||||
|
|
||||||
@@ -1,29 +1,21 @@
|
|||||||
import type { DifficultyLevel } from '../models/worksheet';
|
import type { Difficulty } from '../models/worksheet';
|
||||||
|
|
||||||
/** 产品文档:难度 入门 / 基础 / 进阶 / 挑战 → 云库 difficulty 1–4 */
|
/** 产品文档:难度 入门 / 基础 / 进阶 / 挑战 → 云库 difficulty 1–4 */
|
||||||
export const DIFFICULTY_LEVEL_TO_STARS: Record<
|
export const DIFFICULTY_LEVEL_TO_STARS: Record<Difficulty, 1 | 2 | 3 | 4> = {
|
||||||
DifficultyLevel,
|
|
||||||
1 | 2 | 3 | 4
|
|
||||||
> = {
|
|
||||||
beginner: 1,
|
beginner: 1,
|
||||||
basic: 2,
|
basic: 2,
|
||||||
intermediate: 3,
|
intermediate: 3,
|
||||||
advanced: 4,
|
advanced: 4,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const STARS_TO_DIFFICULTY_LEVEL: Record<
|
export const STARS_TO_DIFFICULTY_LEVEL: Record<1 | 2 | 3 | 4, Difficulty> = {
|
||||||
1 | 2 | 3 | 4,
|
|
||||||
DifficultyLevel
|
|
||||||
> = {
|
|
||||||
1: 'beginner',
|
1: 'beginner',
|
||||||
2: 'basic',
|
2: 'basic',
|
||||||
3: 'intermediate',
|
3: 'intermediate',
|
||||||
4: 'advanced',
|
4: 'advanced',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function difficultyToStars(
|
export function difficultyToStars(level?: Difficulty): 1 | 2 | 3 | 4 {
|
||||||
level?: DifficultyLevel,
|
|
||||||
): 1 | 2 | 3 | 4 {
|
|
||||||
return level ? DIFFICULTY_LEVEL_TO_STARS[level] : 2;
|
return level ? DIFFICULTY_LEVEL_TO_STARS[level] : 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import { CategoryId } from './category';
|
|||||||
* 题型与模板引擎相关模型:对齐技术架构 §5.4 与云开发 worksheets 集合 §3.3
|
* 题型与模板引擎相关模型:对齐技术架构 §5.4 与云开发 worksheets 集合 §3.3
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/** 难度等级文案键(用于前端展示与映射) */
|
||||||
|
export type Difficulty = 'beginner' | 'basic' | 'intermediate' | 'advanced';
|
||||||
|
|
||||||
/** 与架构文档 WorksheetConfig 一致,供云端下发 / 本地内置 */
|
/** 与架构文档 WorksheetConfig 一致,供云端下发 / 本地内置 */
|
||||||
export interface WorksheetConfig {
|
export interface WorksheetConfig {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -319,7 +319,7 @@ export function generateLetterTracing(
|
|||||||
|
|
||||||
const letterImgPath = `/englishPages/shared/assets/letter/${key}.png`;
|
const letterImgPath = `/englishPages/shared/assets/letter/${key}.png`;
|
||||||
|
|
||||||
const wordImgPath = `/englishPages/shared/assets/letterImgs/Apple.png`;
|
const wordImgPath = `/englishPages/shared/assets/letterImgs/${meta.word}.png`;
|
||||||
|
|
||||||
const teaching: TeachingBlock = {
|
const teaching: TeachingBlock = {
|
||||||
upper,
|
upper,
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 23 KiB |
@@ -16,7 +16,7 @@ export const ALPHABET_BY_LETTER: Record<string, AlphabetEntry> = {
|
|||||||
F: { word: 'Fish', emoji: '🐟' },
|
F: { word: 'Fish', emoji: '🐟' },
|
||||||
G: { word: 'Girl', emoji: '👧' },
|
G: { word: 'Girl', emoji: '👧' },
|
||||||
H: { word: 'House', emoji: '🏠' },
|
H: { word: 'House', emoji: '🏠' },
|
||||||
I: { word: 'Ice', emoji: '🧊' },
|
I: { word: 'Icecream', emoji: '🍦' },
|
||||||
J: { word: 'Juice', emoji: '🧃' },
|
J: { word: 'Juice', emoji: '🧃' },
|
||||||
K: { word: 'Kite', emoji: '🪁' },
|
K: { word: 'Kite', emoji: '🪁' },
|
||||||
L: { word: 'Lion', emoji: '🦁' },
|
L: { word: 'Lion', emoji: '🦁' },
|
||||||
@@ -29,10 +29,10 @@ export const ALPHABET_BY_LETTER: Record<string, AlphabetEntry> = {
|
|||||||
S: { word: 'Sun', emoji: '☀️' },
|
S: { word: 'Sun', emoji: '☀️' },
|
||||||
T: { word: 'Tree', emoji: '🌳' },
|
T: { word: 'Tree', emoji: '🌳' },
|
||||||
U: { word: 'Umbrella', emoji: '☂️' },
|
U: { word: 'Umbrella', emoji: '☂️' },
|
||||||
V: { word: 'Van', emoji: '🚐' },
|
V: { word: 'Vegetable', emoji: '🥬' },
|
||||||
W: { word: 'Water', emoji: '💧' },
|
W: { word: 'Water', emoji: '💧' },
|
||||||
X: { word: 'X-ray', emoji: '🦴' },
|
X: { word: 'Xylophone', emoji: '�' },
|
||||||
Y: { word: 'Yacht', emoji: '⛵' },
|
Y: { word: 'Yak', emoji: '⛵' },
|
||||||
Z: { word: 'Zebra', emoji: '🦓' },
|
Z: { word: 'Zebra', emoji: '🦓' },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,187 @@
|
|||||||
|
import type { DebugPublishMeta } from '../../utils/debugPublish';
|
||||||
|
import { inferGradeFromAge } from '../../utils/debugPublish';
|
||||||
|
|
||||||
|
interface FocusDrawDefinition {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
ageMin: number;
|
||||||
|
ageMax: number;
|
||||||
|
difficulty: 1 | 2 | 3 | 4;
|
||||||
|
tags: string[];
|
||||||
|
sortOrder: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FOCUS_DRAW_DEFINITIONS = [
|
||||||
|
{
|
||||||
|
id: 'color-shape-match',
|
||||||
|
title: '根据颜色画图形',
|
||||||
|
subtitle: '根据颜色画出对应图形',
|
||||||
|
ageMin: 4,
|
||||||
|
ageMax: 6,
|
||||||
|
difficulty: 2,
|
||||||
|
tags: ['专注力', '颜色', '图形'],
|
||||||
|
sortOrder: 201,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'shape-symbol',
|
||||||
|
title: '图形符号配对',
|
||||||
|
subtitle: '根据图形画对应符号',
|
||||||
|
ageMin: 4,
|
||||||
|
ageMax: 6,
|
||||||
|
difficulty: 2,
|
||||||
|
tags: ['专注力', '图形', '符号'],
|
||||||
|
sortOrder: 202,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'position-coloring',
|
||||||
|
title: '方位涂涂乐',
|
||||||
|
subtitle: '观察位置,在方格中涂色',
|
||||||
|
ageMin: 4,
|
||||||
|
ageMax: 6,
|
||||||
|
difficulty: 2,
|
||||||
|
tags: ['专注力', '方位', '涂色'],
|
||||||
|
sortOrder: 203,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'color-pattern',
|
||||||
|
title: '颜色找规律',
|
||||||
|
subtitle: '观察颜色规律,在空白图形中涂色',
|
||||||
|
ageMin: 4,
|
||||||
|
ageMax: 6,
|
||||||
|
difficulty: 2,
|
||||||
|
tags: ['专注力', '颜色', '规律'],
|
||||||
|
sortOrder: 204,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'match-connect',
|
||||||
|
title: '连连看',
|
||||||
|
subtitle: '根据物品连一连',
|
||||||
|
ageMin: 3,
|
||||||
|
ageMax: 6,
|
||||||
|
difficulty: 1,
|
||||||
|
tags: ['专注力', '连线', '匹配'],
|
||||||
|
sortOrder: 205,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'line-recognition',
|
||||||
|
title: '线条识别',
|
||||||
|
subtitle: '认识不同线条,画出颜色对应的线条',
|
||||||
|
ageMin: 3,
|
||||||
|
ageMax: 5,
|
||||||
|
difficulty: 1,
|
||||||
|
tags: ['专注力', '线条', '识别', '控笔'],
|
||||||
|
sortOrder: 206,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'grid-reasoning',
|
||||||
|
title: '方格推理',
|
||||||
|
subtitle: '推理出合并方格并连线',
|
||||||
|
ageMin: 5,
|
||||||
|
ageMax: 7,
|
||||||
|
difficulty: 3,
|
||||||
|
tags: ['专注力', '方格', '推理', '逻辑思维'],
|
||||||
|
sortOrder: 207,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'code-connect',
|
||||||
|
title: '译码连线',
|
||||||
|
subtitle: '按数字顺序将数字对应颜色连线',
|
||||||
|
ageMin: 5,
|
||||||
|
ageMax: 7,
|
||||||
|
difficulty: 3,
|
||||||
|
tags: ['专注力', '译码', '连线', '逻辑思维'],
|
||||||
|
sortOrder: 208,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dot-connect',
|
||||||
|
title: '数字点连线',
|
||||||
|
subtitle: '按数字顺序连点成图',
|
||||||
|
ageMin: 3,
|
||||||
|
ageMax: 6,
|
||||||
|
difficulty: 1,
|
||||||
|
tags: ['专注力', '数字', '连线'],
|
||||||
|
sortOrder: 209,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'grid-drawing-3x3',
|
||||||
|
title: '格子仿画 3×3',
|
||||||
|
subtitle: '简单有趣,培养专注力',
|
||||||
|
ageMin: 3,
|
||||||
|
ageMax: 5,
|
||||||
|
difficulty: 1,
|
||||||
|
tags: ['专注力', '格子仿画', '观察'],
|
||||||
|
sortOrder: 210,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'grid-drawing-5x5',
|
||||||
|
title: '格子仿画 5×5',
|
||||||
|
subtitle: '创意挑战,提升观察力',
|
||||||
|
ageMin: 4,
|
||||||
|
ageMax: 6,
|
||||||
|
difficulty: 2,
|
||||||
|
tags: ['专注力', '格子仿画', '观察'],
|
||||||
|
sortOrder: 211,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'grid-drawing-7x7',
|
||||||
|
title: '格子仿画 7×7',
|
||||||
|
subtitle: '大师挑战,锻炼耐心',
|
||||||
|
ageMin: 5,
|
||||||
|
ageMax: 8,
|
||||||
|
difficulty: 3,
|
||||||
|
tags: ['专注力', '格子仿画', '耐心'],
|
||||||
|
sortOrder: 212,
|
||||||
|
},
|
||||||
|
] as const satisfies ReadonlyArray<FocusDrawDefinition>;
|
||||||
|
|
||||||
|
type FocusDrawDefinitionItem = (typeof FOCUS_DRAW_DEFINITIONS)[number];
|
||||||
|
|
||||||
|
const FOCUS_DRAW_BY_ID = Object.fromEntries(
|
||||||
|
FOCUS_DRAW_DEFINITIONS.map((item) => [item.id, item]),
|
||||||
|
) as Record<string, FocusDrawDefinitionItem>;
|
||||||
|
|
||||||
|
function resolveFocusPublishId(
|
||||||
|
routeId: string,
|
||||||
|
selectedTypeId: string,
|
||||||
|
mode?: string,
|
||||||
|
): string {
|
||||||
|
if (selectedTypeId === 'grid-drawing') {
|
||||||
|
return `grid-drawing-${mode || '3x3'}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (routeId in FOCUS_DRAW_BY_ID) {
|
||||||
|
return routeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedTypeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPublishMetaByFocusState(
|
||||||
|
routeId: string,
|
||||||
|
selectedTypeId: string,
|
||||||
|
mode?: string,
|
||||||
|
): DebugPublishMeta | null {
|
||||||
|
const id = resolveFocusPublishId(routeId, selectedTypeId, mode);
|
||||||
|
const item = FOCUS_DRAW_BY_ID[id];
|
||||||
|
if (!item) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
title: item.title,
|
||||||
|
subtitle: item.subtitle,
|
||||||
|
category: 'puzzle',
|
||||||
|
subcategory: 'focus-draw',
|
||||||
|
path: `/focusPages/focusDraw/focusDraw?id=${item.id}`,
|
||||||
|
ageMin: item.ageMin,
|
||||||
|
ageMax: item.ageMax,
|
||||||
|
grade: inferGradeFromAge(item.ageMin, item.ageMax),
|
||||||
|
difficulty: item.difficulty,
|
||||||
|
previewImg: '',
|
||||||
|
tags: [...item.tags],
|
||||||
|
isNew: false,
|
||||||
|
isHot: false,
|
||||||
|
sortOrder: item.sortOrder,
|
||||||
|
status: 'draft',
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
||||||
"draw-ad": "../../components/draw-ad/draw-ad",
|
"draw-ad": "../../components/draw-ad/draw-ad",
|
||||||
"preview-footer-actions": "../../components3.0/preview-footer-actions/preview-footer-actions",
|
"preview-footer-actions": "../../components3.0/preview-footer-actions/preview-footer-actions",
|
||||||
|
"debug-publish-tools": "../../components3.0/debug-publish-tools/debug-publish-tools",
|
||||||
"toy-icon": "../../toy/icon/icon",
|
"toy-icon": "../../toy/icon/icon",
|
||||||
"preview-card": "../../components3.0/preview-card/preview-card"
|
"preview-card": "../../components3.0/preview-card/preview-card"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import {
|
|||||||
type FocusTypeConfig,
|
type FocusTypeConfig,
|
||||||
type FocusTypeAction,
|
type FocusTypeAction,
|
||||||
} from './registry';
|
} from './registry';
|
||||||
|
import { getPublishMetaByFocusState } from './focusDraw.config';
|
||||||
|
import type { DebugPublishMeta } from '../../utils/debugPublish';
|
||||||
|
|
||||||
const TYPE_LIST = FOCUS_TYPE_CONFIGS.map((t) => ({
|
const TYPE_LIST = FOCUS_TYPE_CONFIGS.map((t) => ({
|
||||||
id: t.id,
|
id: t.id,
|
||||||
@@ -36,6 +38,10 @@ createFocusPage({
|
|||||||
currentActions: [] as FocusTypeAction[],
|
currentActions: [] as FocusTypeAction[],
|
||||||
currentMode: '',
|
currentMode: '',
|
||||||
isPreviewFavorite: false,
|
isPreviewFavorite: false,
|
||||||
|
isDevEnv: false,
|
||||||
|
debugPublishVisible: false,
|
||||||
|
debugPublishLoading: false,
|
||||||
|
debugPublishMeta: null as DebugPublishMeta | null,
|
||||||
},
|
},
|
||||||
|
|
||||||
onLoad(options: { id?: string; mode?: string }) {
|
onLoad(options: { id?: string; mode?: string }) {
|
||||||
@@ -43,6 +49,8 @@ createFocusPage({
|
|||||||
const result = findTypeByRouteId(routeId);
|
const result = findTypeByRouteId(routeId);
|
||||||
if (!result) return;
|
if (!result) return;
|
||||||
|
|
||||||
|
this.syncDebugPublishEnv();
|
||||||
|
|
||||||
const { typeConfig, mode } = result;
|
const { typeConfig, mode } = result;
|
||||||
const initialMode =
|
const initialMode =
|
||||||
options.mode || mode || typeConfig.defaultMode || '';
|
options.mode || mode || typeConfig.defaultMode || '';
|
||||||
@@ -196,4 +204,16 @@ createFocusPage({
|
|||||||
icon: 'none',
|
icon: 'none',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getPublishMeta(): DebugPublishMeta {
|
||||||
|
const meta = getPublishMetaByFocusState(
|
||||||
|
this.data.functionId,
|
||||||
|
this.data.selectedTypeId,
|
||||||
|
this.data.currentMode,
|
||||||
|
);
|
||||||
|
if (!meta) {
|
||||||
|
throw new Error('当前题型配置不存在');
|
||||||
|
}
|
||||||
|
return meta;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -75,6 +75,16 @@
|
|||||||
bind:primary="exportToPrint"
|
bind:primary="exportToPrint"
|
||||||
bind:secondary="onShare" />
|
bind:secondary="onShare" />
|
||||||
|
|
||||||
|
<debug-publish-tools
|
||||||
|
wx:if="{{isDevEnv && hasContent}}"
|
||||||
|
id="debugPublishTools"
|
||||||
|
visible="{{debugPublishVisible}}"
|
||||||
|
loading="{{debugPublishLoading}}"
|
||||||
|
meta="{{debugPublishMeta}}"
|
||||||
|
bind:open="onOpenDebugPublish"
|
||||||
|
bind:close="onCloseDebugPublish"
|
||||||
|
bind:confirm="onConfirmDebugPublish" />
|
||||||
|
|
||||||
<!-- 分享引导弹窗 -->
|
<!-- 分享引导弹窗 -->
|
||||||
<share-guide-popup
|
<share-guide-popup
|
||||||
show="{{showShareDialog}}"
|
show="{{showShareDialog}}"
|
||||||
|
|||||||
@@ -0,0 +1,331 @@
|
|||||||
|
import type { DebugPublishMeta } from '../../utils/debugPublish';
|
||||||
|
import { inferGradeFromAge } from '../../utils/debugPublish';
|
||||||
|
|
||||||
|
interface MathDrawDefinition {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
ageMin: number;
|
||||||
|
ageMax: number;
|
||||||
|
difficulty: 1 | 2 | 3 | 4;
|
||||||
|
tags: string[];
|
||||||
|
sortOrder: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MATH_DRAW_DEFINITIONS = [
|
||||||
|
{
|
||||||
|
id: 'number-find',
|
||||||
|
title: '找数字,涂一涂',
|
||||||
|
subtitle: '在数字方阵中找出目标数字并涂色',
|
||||||
|
ageMin: 3,
|
||||||
|
ageMax: 5,
|
||||||
|
difficulty: 1,
|
||||||
|
tags: ['数学', '数字认知', '涂色'],
|
||||||
|
sortOrder: 101,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'number-write',
|
||||||
|
title: '看数字,写一写',
|
||||||
|
subtitle: '按笔画顺序练习书写数字',
|
||||||
|
ageMin: 3,
|
||||||
|
ageMax: 5,
|
||||||
|
difficulty: 1,
|
||||||
|
tags: ['数学', '数字书写', '描红'],
|
||||||
|
sortOrder: 102,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'number-coloring',
|
||||||
|
title: '按数字,涂颜色',
|
||||||
|
subtitle: '按指定数字给对应圆圈涂色',
|
||||||
|
ageMin: 3,
|
||||||
|
ageMax: 5,
|
||||||
|
difficulty: 1,
|
||||||
|
tags: ['数学', '数字认知', '涂色'],
|
||||||
|
sortOrder: 103,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'counting-matching',
|
||||||
|
title: '数一数,连一连',
|
||||||
|
subtitle: '连线配对数字和对应数量图形',
|
||||||
|
ageMin: 3,
|
||||||
|
ageMax: 5,
|
||||||
|
difficulty: 1,
|
||||||
|
tags: ['数学', '计数', '连线'],
|
||||||
|
sortOrder: 104,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'number-object-match',
|
||||||
|
title: '数物连线',
|
||||||
|
subtitle: '连线相同数量的物品和数字',
|
||||||
|
ageMin: 3,
|
||||||
|
ageMax: 5,
|
||||||
|
difficulty: 1,
|
||||||
|
tags: ['数学', '数物对应', '连线'],
|
||||||
|
sortOrder: 105,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'number-object-fill',
|
||||||
|
title: '数物填写',
|
||||||
|
subtitle: '数物品数量,填写对应数字',
|
||||||
|
ageMin: 3,
|
||||||
|
ageMax: 5,
|
||||||
|
difficulty: 1,
|
||||||
|
tags: ['数学', '数物对应', '填写'],
|
||||||
|
sortOrder: 106,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'counting-select',
|
||||||
|
title: '数一数,选一选',
|
||||||
|
subtitle: '数出物品数量,圈出正确答案',
|
||||||
|
ageMin: 3,
|
||||||
|
ageMax: 5,
|
||||||
|
difficulty: 1,
|
||||||
|
tags: ['数学', '计数', '选择'],
|
||||||
|
sortOrder: 107,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'counting-fill',
|
||||||
|
title: '数一数,填一填',
|
||||||
|
subtitle: '数出物品数量,填写数字',
|
||||||
|
ageMin: 3,
|
||||||
|
ageMax: 5,
|
||||||
|
difficulty: 1,
|
||||||
|
tags: ['数学', '计数', '填写'],
|
||||||
|
sortOrder: 108,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'compare',
|
||||||
|
title: '数一数,比大小',
|
||||||
|
subtitle: '比较数量,填入 ><=',
|
||||||
|
ageMin: 4,
|
||||||
|
ageMax: 6,
|
||||||
|
difficulty: 2,
|
||||||
|
tags: ['数学', '比较', '数量'],
|
||||||
|
sortOrder: 109,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'number-sort',
|
||||||
|
title: '数字排序',
|
||||||
|
subtitle: '写出正确的数字顺序',
|
||||||
|
ageMin: 4,
|
||||||
|
ageMax: 6,
|
||||||
|
difficulty: 2,
|
||||||
|
tags: ['数学', '数字排序', '序列'],
|
||||||
|
sortOrder: 110,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'missing-number',
|
||||||
|
title: '填上缺少的数字',
|
||||||
|
subtitle: '在数列中找出并填写缺失数字',
|
||||||
|
ageMin: 4,
|
||||||
|
ageMax: 6,
|
||||||
|
difficulty: 2,
|
||||||
|
tags: ['数学', '数字序列', '填写'],
|
||||||
|
sortOrder: 111,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'number-decompose',
|
||||||
|
title: '10以内数的分与合',
|
||||||
|
subtitle: '把数字分一分,合一合',
|
||||||
|
ageMin: 4,
|
||||||
|
ageMax: 6,
|
||||||
|
difficulty: 1,
|
||||||
|
tags: ['数学', '分与合', '10以内'],
|
||||||
|
sortOrder: 112,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'number-decompose-20',
|
||||||
|
title: '20以内数的分与合',
|
||||||
|
subtitle: '把数字分一分,合一合',
|
||||||
|
ageMin: 5,
|
||||||
|
ageMax: 7,
|
||||||
|
difficulty: 2,
|
||||||
|
tags: ['数学', '分与合', '20以内'],
|
||||||
|
sortOrder: 113,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'one-digit-addition',
|
||||||
|
title: '一位数加法',
|
||||||
|
subtitle: '通过圆点学习一位数加法运算',
|
||||||
|
ageMin: 4,
|
||||||
|
ageMax: 6,
|
||||||
|
difficulty: 2,
|
||||||
|
tags: ['数学', '加法', '一位数'],
|
||||||
|
sortOrder: 114,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'addition-5',
|
||||||
|
title: '5以内加法',
|
||||||
|
subtitle: '图形化展示 5 以内加法',
|
||||||
|
ageMin: 3,
|
||||||
|
ageMax: 5,
|
||||||
|
difficulty: 1,
|
||||||
|
tags: ['数学', '加法', '5以内', '计算题'],
|
||||||
|
sortOrder: 115,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'addition-10',
|
||||||
|
title: '10以内加法',
|
||||||
|
subtitle: '图形化展示 10 以内加法',
|
||||||
|
ageMin: 4,
|
||||||
|
ageMax: 6,
|
||||||
|
difficulty: 2,
|
||||||
|
tags: ['数学', '加法', '10以内', '计算题'],
|
||||||
|
sortOrder: 116,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'subtraction-10',
|
||||||
|
title: '10以内减法',
|
||||||
|
subtitle: '图形化展示 10 以内减法',
|
||||||
|
ageMin: 4,
|
||||||
|
ageMax: 6,
|
||||||
|
difficulty: 2,
|
||||||
|
tags: ['数学', '减法', '10以内', '计算题'],
|
||||||
|
sortOrder: 117,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'addition-subtraction-10',
|
||||||
|
title: '10以内加减法',
|
||||||
|
subtitle: '加减法混合运算',
|
||||||
|
ageMin: 5,
|
||||||
|
ageMax: 7,
|
||||||
|
difficulty: 2,
|
||||||
|
tags: ['数学', '加减法', '10以内', '计算题'],
|
||||||
|
sortOrder: 118,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'make-ten',
|
||||||
|
title: '凑十法练习',
|
||||||
|
subtitle: '20 以内进位加法',
|
||||||
|
ageMin: 5,
|
||||||
|
ageMax: 7,
|
||||||
|
difficulty: 3,
|
||||||
|
tags: ['数学', '凑十法', '进位加法', '计算题'],
|
||||||
|
sortOrder: 119,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'break-ten',
|
||||||
|
title: '破十法练习',
|
||||||
|
subtitle: '20 以内退位减法',
|
||||||
|
ageMin: 5,
|
||||||
|
ageMax: 7,
|
||||||
|
difficulty: 3,
|
||||||
|
tags: ['数学', '破十法', '退位减法', '计算题'],
|
||||||
|
sortOrder: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'flat-ten',
|
||||||
|
title: '平十法练习',
|
||||||
|
subtitle: '20 以内退位减法',
|
||||||
|
ageMin: 5,
|
||||||
|
ageMax: 7,
|
||||||
|
difficulty: 3,
|
||||||
|
tags: ['数学', '平十法', '退位减法', '计算题'],
|
||||||
|
sortOrder: 121,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'borrow-ten',
|
||||||
|
title: '借十法练习',
|
||||||
|
subtitle: '20 以上退位减法',
|
||||||
|
ageMin: 6,
|
||||||
|
ageMax: 8,
|
||||||
|
difficulty: 4,
|
||||||
|
tags: ['数学', '借十法', '退位减法', '计算题'],
|
||||||
|
sortOrder: 122,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'practice-addition',
|
||||||
|
title: '加法运算',
|
||||||
|
subtitle: '10/20/50/100 以内加法',
|
||||||
|
ageMin: 5,
|
||||||
|
ageMax: 8,
|
||||||
|
difficulty: 3,
|
||||||
|
tags: ['数学', '口算', '加法', '计算题'],
|
||||||
|
sortOrder: 123,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'practice-subtraction',
|
||||||
|
title: '减法运算',
|
||||||
|
subtitle: '10/20/50/100 以内减法',
|
||||||
|
ageMin: 5,
|
||||||
|
ageMax: 8,
|
||||||
|
difficulty: 3,
|
||||||
|
tags: ['数学', '口算', '减法', '计算题'],
|
||||||
|
sortOrder: 124,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'practice-mixed',
|
||||||
|
title: '混合运算',
|
||||||
|
subtitle: '10/20/50/100 以内加减法混合',
|
||||||
|
ageMin: 5,
|
||||||
|
ageMax: 8,
|
||||||
|
difficulty: 3,
|
||||||
|
tags: ['数学', '口算', '加减法', '计算题'],
|
||||||
|
sortOrder: 125,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'multiplication-table',
|
||||||
|
title: '九九乘法表',
|
||||||
|
subtitle: '学习九九乘法口诀',
|
||||||
|
ageMin: 6,
|
||||||
|
ageMax: 8,
|
||||||
|
difficulty: 3,
|
||||||
|
tags: ['数学', '乘法', '九九乘法表'],
|
||||||
|
sortOrder: 126,
|
||||||
|
},
|
||||||
|
] as const satisfies ReadonlyArray<MathDrawDefinition>;
|
||||||
|
|
||||||
|
type MathDrawDefinitionItem = (typeof MATH_DRAW_DEFINITIONS)[number];
|
||||||
|
|
||||||
|
const MATH_DRAW_BY_ID = Object.fromEntries(
|
||||||
|
MATH_DRAW_DEFINITIONS.map((item) => [item.id, item]),
|
||||||
|
) as Record<string, MathDrawDefinitionItem>;
|
||||||
|
|
||||||
|
function resolveMathPublishId(
|
||||||
|
routeId: string,
|
||||||
|
selectedTypeId: string,
|
||||||
|
mode?: string,
|
||||||
|
): string {
|
||||||
|
if (selectedTypeId === 'addition') {
|
||||||
|
return mode || 'addition-5';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedTypeId === 'number-object-match' && mode === 'fill') {
|
||||||
|
return 'number-object-fill';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (routeId in MATH_DRAW_BY_ID) {
|
||||||
|
return routeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedTypeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPublishMetaByMathState(
|
||||||
|
routeId: string,
|
||||||
|
selectedTypeId: string,
|
||||||
|
mode?: string,
|
||||||
|
): DebugPublishMeta | null {
|
||||||
|
const id = resolveMathPublishId(routeId, selectedTypeId, mode);
|
||||||
|
const item = MATH_DRAW_BY_ID[id];
|
||||||
|
if (!item) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
title: item.title,
|
||||||
|
subtitle: item.subtitle,
|
||||||
|
category: 'math',
|
||||||
|
subcategory: 'math-draw',
|
||||||
|
path: `/mathPages/mathDraw/mathDraw?id=${item.id}`,
|
||||||
|
ageMin: item.ageMin,
|
||||||
|
ageMax: item.ageMax,
|
||||||
|
grade: inferGradeFromAge(item.ageMin, item.ageMax),
|
||||||
|
difficulty: item.difficulty,
|
||||||
|
previewImg: '',
|
||||||
|
tags: [...item.tags],
|
||||||
|
isNew: false,
|
||||||
|
isHot: false,
|
||||||
|
sortOrder: item.sortOrder,
|
||||||
|
status: 'draft',
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
||||||
"draw-ad": "../../components/draw-ad/draw-ad",
|
"draw-ad": "../../components/draw-ad/draw-ad",
|
||||||
"preview-footer-actions": "../../components3.0/preview-footer-actions/preview-footer-actions",
|
"preview-footer-actions": "../../components3.0/preview-footer-actions/preview-footer-actions",
|
||||||
|
"debug-publish-tools": "../../components3.0/debug-publish-tools/debug-publish-tools",
|
||||||
"toy-icon": "../../toy/icon/icon",
|
"toy-icon": "../../toy/icon/icon",
|
||||||
"preview-card": "../../components3.0/preview-card/preview-card"
|
"preview-card": "../../components3.0/preview-card/preview-card"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import {
|
|||||||
type MathTypeConfig,
|
type MathTypeConfig,
|
||||||
type MathTypeAction,
|
type MathTypeAction,
|
||||||
} from './registry';
|
} from './registry';
|
||||||
|
import { getPublishMetaByMathState } from './mathDraw.config';
|
||||||
|
import type { DebugPublishMeta } from '../../utils/debugPublish';
|
||||||
|
|
||||||
const TYPE_LIST = MATH_TYPE_CONFIGS.map((t) => ({
|
const TYPE_LIST = MATH_TYPE_CONFIGS.map((t) => ({
|
||||||
id: t.id,
|
id: t.id,
|
||||||
@@ -40,6 +42,10 @@ createMathPage({
|
|||||||
numberList: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
numberList: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
||||||
selectedNumber: 0,
|
selectedNumber: 0,
|
||||||
isPreviewFavorite: false,
|
isPreviewFavorite: false,
|
||||||
|
isDevEnv: false,
|
||||||
|
debugPublishVisible: false,
|
||||||
|
debugPublishLoading: false,
|
||||||
|
debugPublishMeta: null as DebugPublishMeta | null,
|
||||||
},
|
},
|
||||||
|
|
||||||
onLoad(options: { id?: string; mode?: string }) {
|
onLoad(options: { id?: string; mode?: string }) {
|
||||||
@@ -47,6 +53,8 @@ createMathPage({
|
|||||||
const result = findTypeByRouteId(routeId);
|
const result = findTypeByRouteId(routeId);
|
||||||
if (!result) return;
|
if (!result) return;
|
||||||
|
|
||||||
|
this.syncDebugPublishEnv();
|
||||||
|
|
||||||
const { typeConfig, mode, extra } = result;
|
const { typeConfig, mode, extra } = result;
|
||||||
const initialMode =
|
const initialMode =
|
||||||
options.mode || mode || typeConfig.defaultMode || '';
|
options.mode || mode || typeConfig.defaultMode || '';
|
||||||
@@ -260,4 +268,16 @@ createMathPage({
|
|||||||
icon: 'none',
|
icon: 'none',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getPublishMeta(): DebugPublishMeta {
|
||||||
|
const meta = getPublishMetaByMathState(
|
||||||
|
this.data.functionId,
|
||||||
|
this.data.selectedTypeId,
|
||||||
|
this.data.currentMode,
|
||||||
|
);
|
||||||
|
if (!meta) {
|
||||||
|
throw new Error('当前题型配置不存在');
|
||||||
|
}
|
||||||
|
return meta;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -94,6 +94,16 @@
|
|||||||
bind:primary="exportToPrint"
|
bind:primary="exportToPrint"
|
||||||
bind:secondary="onShare" />
|
bind:secondary="onShare" />
|
||||||
|
|
||||||
|
<debug-publish-tools
|
||||||
|
wx:if="{{isDevEnv && hasContent}}"
|
||||||
|
id="debugPublishTools"
|
||||||
|
visible="{{debugPublishVisible}}"
|
||||||
|
loading="{{debugPublishLoading}}"
|
||||||
|
meta="{{debugPublishMeta}}"
|
||||||
|
bind:open="onOpenDebugPublish"
|
||||||
|
bind:close="onCloseDebugPublish"
|
||||||
|
bind:confirm="onConfirmDebugPublish" />
|
||||||
|
|
||||||
<!-- 分享引导弹窗 -->
|
<!-- 分享引导弹窗 -->
|
||||||
<share-guide-popup
|
<share-guide-popup
|
||||||
show="{{showShareDialog}}"
|
show="{{showShareDialog}}"
|
||||||
|
|||||||