diff --git a/docs/Debug发布方案.md b/docs/Debug发布方案.md index f3dac5d..30ce5a4 100644 --- a/docs/Debug发布方案.md +++ b/docs/Debug发布方案.md @@ -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//.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 - - - - -``` - -### 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 { - // 裁剪参数 — 与 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 { - 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 { - 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 { - 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/ -``` - -命名规则:`.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) — 首页 / 分类页 / 分龄页的内容配置、数据预拉取、管理方案 diff --git a/docs/Worksheet发布方案.md b/docs/Worksheet发布方案.md new file mode 100644 index 0000000..8df81b8 --- /dev/null +++ b/docs/Worksheet发布方案.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//.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//.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` 集合 | diff --git a/docs/小程序云开发方案.md b/docs/小程序云开发方案.md index dfdb1cb..518a34a 100644 --- a/docs/小程序云开发方案.md +++ b/docs/小程序云开发方案.md @@ -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 | 是 | 更新时间。 | diff --git a/docs/推广方案.md b/docs/推广方案.md new file mode 100644 index 0000000..31eb2b7 --- /dev/null +++ b/docs/推广方案.md @@ -0,0 +1,5 @@ +# 推广方案 + +## 分享激励制度 + +用户每下载三次需要看广告,但分享成功一次(被分享者打开并下载一次)可以获得 10 次免广告下载次数,且可以累加。 diff --git a/docs/资源存放规则.md b/docs/资源存放规则.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/页面内容管理方案.md b/docs/页面内容管理方案.md new file mode 100644 index 0000000..e1ec1a0 --- /dev/null +++ b/docs/页面内容管理方案.md @@ -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; + }>; + 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 | + + diff --git a/miniprogram/core/data/difficulty.ts b/miniprogram/core/data/difficulty.ts index 679d73f..6344b55 100644 --- a/miniprogram/core/data/difficulty.ts +++ b/miniprogram/core/data/difficulty.ts @@ -1,29 +1,21 @@ -import type { DifficultyLevel } from '../models/worksheet'; +import type { Difficulty } from '../models/worksheet'; /** 产品文档:难度 入门 / 基础 / 进阶 / 挑战 → 云库 difficulty 1–4 */ -export const DIFFICULTY_LEVEL_TO_STARS: Record< - DifficultyLevel, - 1 | 2 | 3 | 4 -> = { +export const DIFFICULTY_LEVEL_TO_STARS: Record = { beginner: 1, basic: 2, intermediate: 3, advanced: 4, }; -export const STARS_TO_DIFFICULTY_LEVEL: Record< - 1 | 2 | 3 | 4, - DifficultyLevel -> = { +export const STARS_TO_DIFFICULTY_LEVEL: Record<1 | 2 | 3 | 4, Difficulty> = { 1: 'beginner', 2: 'basic', 3: 'intermediate', 4: 'advanced', }; -export function difficultyToStars( - level?: DifficultyLevel, -): 1 | 2 | 3 | 4 { +export function difficultyToStars(level?: Difficulty): 1 | 2 | 3 | 4 { return level ? DIFFICULTY_LEVEL_TO_STARS[level] : 2; } diff --git a/miniprogram/core/models/worksheet.ts b/miniprogram/core/models/worksheet.ts index c557129..d819895 100644 --- a/miniprogram/core/models/worksheet.ts +++ b/miniprogram/core/models/worksheet.ts @@ -3,6 +3,9 @@ import { CategoryId } from './category'; * 题型与模板引擎相关模型:对齐技术架构 §5.4 与云开发 worksheets 集合 §3.3 */ +/** 难度等级文案键(用于前端展示与映射) */ +export type Difficulty = 'beginner' | 'basic' | 'intermediate' | 'advanced'; + /** 与架构文档 WorksheetConfig 一致,供云端下发 / 本地内置 */ export interface WorksheetConfig { id: string; diff --git a/miniprogram/englishPages/letterTracing/generators/letter-tracing-generator.ts b/miniprogram/englishPages/letterTracing/generators/letter-tracing-generator.ts index d7a0567..c8b4261 100644 --- a/miniprogram/englishPages/letterTracing/generators/letter-tracing-generator.ts +++ b/miniprogram/englishPages/letterTracing/generators/letter-tracing-generator.ts @@ -319,7 +319,7 @@ export function generateLetterTracing( 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 = { upper, diff --git a/miniprogram/englishPages/shared/assets/letterImgs/Apple.png b/miniprogram/englishPages/shared/assets/letterImgs/Apple.png index 8266b0a..31d8578 100644 Binary files a/miniprogram/englishPages/shared/assets/letterImgs/Apple.png and b/miniprogram/englishPages/shared/assets/letterImgs/Apple.png differ diff --git a/miniprogram/englishPages/shared/assets/letterImgs/Ball.png b/miniprogram/englishPages/shared/assets/letterImgs/Ball.png new file mode 100644 index 0000000..ab9cb31 Binary files /dev/null and b/miniprogram/englishPages/shared/assets/letterImgs/Ball.png differ diff --git a/miniprogram/englishPages/shared/assets/letterImgs/Cat.png b/miniprogram/englishPages/shared/assets/letterImgs/Cat.png new file mode 100644 index 0000000..f56efd2 Binary files /dev/null and b/miniprogram/englishPages/shared/assets/letterImgs/Cat.png differ diff --git a/miniprogram/englishPages/shared/assets/letterImgs/Dog.png b/miniprogram/englishPages/shared/assets/letterImgs/Dog.png new file mode 100644 index 0000000..51bf7af Binary files /dev/null and b/miniprogram/englishPages/shared/assets/letterImgs/Dog.png differ diff --git a/miniprogram/englishPages/shared/assets/letterImgs/Egg.png b/miniprogram/englishPages/shared/assets/letterImgs/Egg.png new file mode 100644 index 0000000..395ac7c Binary files /dev/null and b/miniprogram/englishPages/shared/assets/letterImgs/Egg.png differ diff --git a/miniprogram/englishPages/shared/assets/letterImgs/Fish.png b/miniprogram/englishPages/shared/assets/letterImgs/Fish.png new file mode 100644 index 0000000..234d3c0 Binary files /dev/null and b/miniprogram/englishPages/shared/assets/letterImgs/Fish.png differ diff --git a/miniprogram/englishPages/shared/assets/letterImgs/Girl.png b/miniprogram/englishPages/shared/assets/letterImgs/Girl.png new file mode 100644 index 0000000..9ea1d1e Binary files /dev/null and b/miniprogram/englishPages/shared/assets/letterImgs/Girl.png differ diff --git a/miniprogram/englishPages/shared/assets/letterImgs/House.png b/miniprogram/englishPages/shared/assets/letterImgs/House.png new file mode 100644 index 0000000..2095713 Binary files /dev/null and b/miniprogram/englishPages/shared/assets/letterImgs/House.png differ diff --git a/miniprogram/englishPages/shared/assets/letterImgs/Icecream.png b/miniprogram/englishPages/shared/assets/letterImgs/Icecream.png new file mode 100644 index 0000000..961eac0 Binary files /dev/null and b/miniprogram/englishPages/shared/assets/letterImgs/Icecream.png differ diff --git a/miniprogram/englishPages/shared/assets/letterImgs/Juice.png b/miniprogram/englishPages/shared/assets/letterImgs/Juice.png new file mode 100644 index 0000000..0da7bde Binary files /dev/null and b/miniprogram/englishPages/shared/assets/letterImgs/Juice.png differ diff --git a/miniprogram/englishPages/shared/assets/letterImgs/Kite.png b/miniprogram/englishPages/shared/assets/letterImgs/Kite.png new file mode 100644 index 0000000..d63718e Binary files /dev/null and b/miniprogram/englishPages/shared/assets/letterImgs/Kite.png differ diff --git a/miniprogram/englishPages/shared/assets/letterImgs/Lion.png b/miniprogram/englishPages/shared/assets/letterImgs/Lion.png new file mode 100644 index 0000000..4b136dd Binary files /dev/null and b/miniprogram/englishPages/shared/assets/letterImgs/Lion.png differ diff --git a/miniprogram/englishPages/shared/assets/letterImgs/Moon.png b/miniprogram/englishPages/shared/assets/letterImgs/Moon.png new file mode 100644 index 0000000..7095269 Binary files /dev/null and b/miniprogram/englishPages/shared/assets/letterImgs/Moon.png differ diff --git a/miniprogram/englishPages/shared/assets/letterImgs/Nest.png b/miniprogram/englishPages/shared/assets/letterImgs/Nest.png new file mode 100644 index 0000000..6a55538 Binary files /dev/null and b/miniprogram/englishPages/shared/assets/letterImgs/Nest.png differ diff --git a/miniprogram/englishPages/shared/assets/letterImgs/Orange.png b/miniprogram/englishPages/shared/assets/letterImgs/Orange.png new file mode 100644 index 0000000..133e6a8 Binary files /dev/null and b/miniprogram/englishPages/shared/assets/letterImgs/Orange.png differ diff --git a/miniprogram/englishPages/shared/assets/letterImgs/Pig.png b/miniprogram/englishPages/shared/assets/letterImgs/Pig.png new file mode 100644 index 0000000..8ae0176 Binary files /dev/null and b/miniprogram/englishPages/shared/assets/letterImgs/Pig.png differ diff --git a/miniprogram/englishPages/shared/assets/letterImgs/Queen.png b/miniprogram/englishPages/shared/assets/letterImgs/Queen.png new file mode 100644 index 0000000..54d682c Binary files /dev/null and b/miniprogram/englishPages/shared/assets/letterImgs/Queen.png differ diff --git a/miniprogram/englishPages/shared/assets/letterImgs/Rain.png b/miniprogram/englishPages/shared/assets/letterImgs/Rain.png new file mode 100644 index 0000000..11b39ca Binary files /dev/null and b/miniprogram/englishPages/shared/assets/letterImgs/Rain.png differ diff --git a/miniprogram/englishPages/shared/assets/letterImgs/Sun.png b/miniprogram/englishPages/shared/assets/letterImgs/Sun.png new file mode 100644 index 0000000..3a20ff0 Binary files /dev/null and b/miniprogram/englishPages/shared/assets/letterImgs/Sun.png differ diff --git a/miniprogram/englishPages/shared/assets/letterImgs/Tree.png b/miniprogram/englishPages/shared/assets/letterImgs/Tree.png new file mode 100644 index 0000000..46b115c Binary files /dev/null and b/miniprogram/englishPages/shared/assets/letterImgs/Tree.png differ diff --git a/miniprogram/englishPages/shared/assets/letterImgs/Umbrella.png b/miniprogram/englishPages/shared/assets/letterImgs/Umbrella.png new file mode 100644 index 0000000..8c4e707 Binary files /dev/null and b/miniprogram/englishPages/shared/assets/letterImgs/Umbrella.png differ diff --git a/miniprogram/englishPages/shared/assets/letterImgs/Vegetable.png b/miniprogram/englishPages/shared/assets/letterImgs/Vegetable.png new file mode 100644 index 0000000..1e80b68 Binary files /dev/null and b/miniprogram/englishPages/shared/assets/letterImgs/Vegetable.png differ diff --git a/miniprogram/englishPages/shared/assets/letterImgs/Water.png b/miniprogram/englishPages/shared/assets/letterImgs/Water.png new file mode 100644 index 0000000..cefb633 Binary files /dev/null and b/miniprogram/englishPages/shared/assets/letterImgs/Water.png differ diff --git a/miniprogram/englishPages/shared/assets/letterImgs/Xylophone.png b/miniprogram/englishPages/shared/assets/letterImgs/Xylophone.png new file mode 100644 index 0000000..8f8ce1c Binary files /dev/null and b/miniprogram/englishPages/shared/assets/letterImgs/Xylophone.png differ diff --git a/miniprogram/englishPages/shared/assets/letterImgs/Yak.png b/miniprogram/englishPages/shared/assets/letterImgs/Yak.png new file mode 100644 index 0000000..fc130c8 Binary files /dev/null and b/miniprogram/englishPages/shared/assets/letterImgs/Yak.png differ diff --git a/miniprogram/englishPages/shared/assets/letterImgs/Zebra.png b/miniprogram/englishPages/shared/assets/letterImgs/Zebra.png new file mode 100644 index 0000000..68957b0 Binary files /dev/null and b/miniprogram/englishPages/shared/assets/letterImgs/Zebra.png differ diff --git a/miniprogram/englishPages/shared/data/alphabet.ts b/miniprogram/englishPages/shared/data/alphabet.ts index fefedf5..68d2736 100644 --- a/miniprogram/englishPages/shared/data/alphabet.ts +++ b/miniprogram/englishPages/shared/data/alphabet.ts @@ -16,7 +16,7 @@ export const ALPHABET_BY_LETTER: Record = { F: { word: 'Fish', emoji: '🐟' }, G: { word: 'Girl', emoji: '👧' }, H: { word: 'House', emoji: '🏠' }, - I: { word: 'Ice', emoji: '🧊' }, + I: { word: 'Icecream', emoji: '🍦' }, J: { word: 'Juice', emoji: '🧃' }, K: { word: 'Kite', emoji: '🪁' }, L: { word: 'Lion', emoji: '🦁' }, @@ -29,10 +29,10 @@ export const ALPHABET_BY_LETTER: Record = { S: { word: 'Sun', emoji: '☀️' }, T: { word: 'Tree', emoji: '🌳' }, U: { word: 'Umbrella', emoji: '☂️' }, - V: { word: 'Van', emoji: '🚐' }, + V: { word: 'Vegetable', emoji: '🥬' }, W: { word: 'Water', emoji: '💧' }, - X: { word: 'X-ray', emoji: '🦴' }, - Y: { word: 'Yacht', emoji: '⛵' }, + X: { word: 'Xylophone', emoji: '�' }, + Y: { word: 'Yak', emoji: '⛵' }, Z: { word: 'Zebra', emoji: '🦓' }, }; diff --git a/miniprogram/focusPages/focusDraw/focusDraw.config.ts b/miniprogram/focusPages/focusDraw/focusDraw.config.ts new file mode 100644 index 0000000..3e08fb5 --- /dev/null +++ b/miniprogram/focusPages/focusDraw/focusDraw.config.ts @@ -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; + +type FocusDrawDefinitionItem = (typeof FOCUS_DRAW_DEFINITIONS)[number]; + +const FOCUS_DRAW_BY_ID = Object.fromEntries( + FOCUS_DRAW_DEFINITIONS.map((item) => [item.id, item]), +) as Record; + +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', + }; +} diff --git a/miniprogram/focusPages/focusDraw/focusDraw.json b/miniprogram/focusPages/focusDraw/focusDraw.json index 2478c2d..c952f52 100644 --- a/miniprogram/focusPages/focusDraw/focusDraw.json +++ b/miniprogram/focusPages/focusDraw/focusDraw.json @@ -10,6 +10,7 @@ "share-guide-popup": "../../components/share-guide-popup/share-guide-popup", "draw-ad": "../../components/draw-ad/draw-ad", "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", "preview-card": "../../components3.0/preview-card/preview-card" } diff --git a/miniprogram/focusPages/focusDraw/focusDraw.ts b/miniprogram/focusPages/focusDraw/focusDraw.ts index 32e432f..6bdd23e 100644 --- a/miniprogram/focusPages/focusDraw/focusDraw.ts +++ b/miniprogram/focusPages/focusDraw/focusDraw.ts @@ -6,6 +6,8 @@ import { type FocusTypeConfig, type FocusTypeAction, } from './registry'; +import { getPublishMetaByFocusState } from './focusDraw.config'; +import type { DebugPublishMeta } from '../../utils/debugPublish'; const TYPE_LIST = FOCUS_TYPE_CONFIGS.map((t) => ({ id: t.id, @@ -36,6 +38,10 @@ createFocusPage({ currentActions: [] as FocusTypeAction[], currentMode: '', isPreviewFavorite: false, + isDevEnv: false, + debugPublishVisible: false, + debugPublishLoading: false, + debugPublishMeta: null as DebugPublishMeta | null, }, onLoad(options: { id?: string; mode?: string }) { @@ -43,6 +49,8 @@ createFocusPage({ const result = findTypeByRouteId(routeId); if (!result) return; + this.syncDebugPublishEnv(); + const { typeConfig, mode } = result; const initialMode = options.mode || mode || typeConfig.defaultMode || ''; @@ -196,4 +204,16 @@ createFocusPage({ icon: 'none', }); }, + + getPublishMeta(): DebugPublishMeta { + const meta = getPublishMetaByFocusState( + this.data.functionId, + this.data.selectedTypeId, + this.data.currentMode, + ); + if (!meta) { + throw new Error('当前题型配置不存在'); + } + return meta; + }, }); diff --git a/miniprogram/focusPages/focusDraw/focusDraw.wxml b/miniprogram/focusPages/focusDraw/focusDraw.wxml index 79b7fa3..4c3c61c 100644 --- a/miniprogram/focusPages/focusDraw/focusDraw.wxml +++ b/miniprogram/focusPages/focusDraw/focusDraw.wxml @@ -75,6 +75,16 @@ bind:primary="exportToPrint" bind:secondary="onShare" /> + + ; + +type MathDrawDefinitionItem = (typeof MATH_DRAW_DEFINITIONS)[number]; + +const MATH_DRAW_BY_ID = Object.fromEntries( + MATH_DRAW_DEFINITIONS.map((item) => [item.id, item]), +) as Record; + +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', + }; +} diff --git a/miniprogram/mathPages/mathDraw/mathDraw.json b/miniprogram/mathPages/mathDraw/mathDraw.json index 6ee6982..6b30741 100644 --- a/miniprogram/mathPages/mathDraw/mathDraw.json +++ b/miniprogram/mathPages/mathDraw/mathDraw.json @@ -10,6 +10,7 @@ "share-guide-popup": "../../components/share-guide-popup/share-guide-popup", "draw-ad": "../../components/draw-ad/draw-ad", "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", "preview-card": "../../components3.0/preview-card/preview-card" } diff --git a/miniprogram/mathPages/mathDraw/mathDraw.ts b/miniprogram/mathPages/mathDraw/mathDraw.ts index 7d7cdfe..2daa0c8 100644 --- a/miniprogram/mathPages/mathDraw/mathDraw.ts +++ b/miniprogram/mathPages/mathDraw/mathDraw.ts @@ -6,6 +6,8 @@ import { type MathTypeConfig, type MathTypeAction, } from './registry'; +import { getPublishMetaByMathState } from './mathDraw.config'; +import type { DebugPublishMeta } from '../../utils/debugPublish'; const TYPE_LIST = MATH_TYPE_CONFIGS.map((t) => ({ id: t.id, @@ -40,6 +42,10 @@ createMathPage({ numberList: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], selectedNumber: 0, isPreviewFavorite: false, + isDevEnv: false, + debugPublishVisible: false, + debugPublishLoading: false, + debugPublishMeta: null as DebugPublishMeta | null, }, onLoad(options: { id?: string; mode?: string }) { @@ -47,6 +53,8 @@ createMathPage({ const result = findTypeByRouteId(routeId); if (!result) return; + this.syncDebugPublishEnv(); + const { typeConfig, mode, extra } = result; const initialMode = options.mode || mode || typeConfig.defaultMode || ''; @@ -260,4 +268,16 @@ createMathPage({ icon: 'none', }); }, + + getPublishMeta(): DebugPublishMeta { + const meta = getPublishMetaByMathState( + this.data.functionId, + this.data.selectedTypeId, + this.data.currentMode, + ); + if (!meta) { + throw new Error('当前题型配置不存在'); + } + return meta; + }, }); diff --git a/miniprogram/mathPages/mathDraw/mathDraw.wxml b/miniprogram/mathPages/mathDraw/mathDraw.wxml index 45275e5..b7d31a9 100644 --- a/miniprogram/mathPages/mathDraw/mathDraw.wxml +++ b/miniprogram/mathPages/mathDraw/mathDraw.wxml @@ -94,6 +94,16 @@ bind:primary="exportToPrint" bind:secondary="onShare" /> + +